Fix chat, brain stability, MariaDB reconnect, suppress warnings
- Listen on raw 'text' packet for Bedrock chat (pattern-based chat event doesn't fire reliably on Bedrock) - Brain: add safety reset for stuck pending_status flag - MariaDB: add retry-on-disconnect for all query methods - Suppress harmless punycode deprecation warning from Node.js - Add mineflayer-bedrock lib packages (mineflayer, prismarine-chunk, prismarine-registry) for movement support - Exclude minecraft-data from git (278MB, installed via npm) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d0a96ce028
commit
8f616598fd
217 changed files with 36399 additions and 17 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,6 +3,8 @@ __pycache__/
|
|||
.env
|
||||
node_modules/
|
||||
bridge/dist/
|
||||
bridge/lib/minecraft-data/
|
||||
bridge/lib/*/node_modules/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
dist/
|
||||
|
|
|
|||
2
bridge/.gitignore
vendored
Normal file
2
bridge/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
bridge/lib/
|
||||
bridge/node_modules/
|
||||
4
bridge/lib/mineflayer/.github/FUNDING.yml
vendored
Normal file
4
bridge/lib/mineflayer/.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# These are supported funding model platforms
|
||||
open_collective: prismarinejs
|
||||
custom: https://rysolv.com/repos/detail/74691b23-938d-4b2f-b65a-5c47bf5b3f0f
|
||||
github: PrismarineJS
|
||||
40
bridge/lib/mineflayer/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
bridge/lib/mineflayer/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: possible bug,Stage1
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
- [ ] The [FAQ](https://github.com/PrismarineJS/mineflayer/blob/master/docs/FAQ.md) doesn't contain a resolution to my issue
|
||||
|
||||
## Versions
|
||||
- mineflayer: #.#.#
|
||||
- server: vanilla/spigot/paper #.#.#
|
||||
- node: #.#.#
|
||||
|
||||
## Detailed description of a problem
|
||||
A clear and concise description of what the problem is, with as much context as possible.
|
||||
What are you building? What problem are you trying to solve?
|
||||
|
||||
## What did you try yet?
|
||||
|
||||
Did you try any method from the API?
|
||||
Did you try any example? Any error from those?
|
||||
|
||||
## Your current code
|
||||
```js
|
||||
|
||||
/*
|
||||
Some code here, replace this
|
||||
*/
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
5
bridge/lib/mineflayer/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
bridge/lib/mineflayer/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Github Discussions
|
||||
url: https://github.com/PrismarineJS/mineflayer/discussions
|
||||
about: Please make a post in our Github Discussions for questions and support requests.
|
||||
20
bridge/lib/mineflayer/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
bridge/lib/mineflayer/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: new feature,Stage1
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Is your feature request related to a problem? Please describe.
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
## Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
## Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
11
bridge/lib/mineflayer/.github/dependabot.yml
vendored
Normal file
11
bridge/lib/mineflayer/.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
versions:
|
||||
- 15.0.0
|
||||
60
bridge/lib/mineflayer/.github/workflows/ci.yml
vendored
Normal file
60
bridge/lib/mineflayer/.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
Lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v1.4.4
|
||||
with:
|
||||
node-version: 22.x
|
||||
- run: npm i && npm run lint
|
||||
|
||||
PrepareSupportedVersions:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v1.4.4
|
||||
with:
|
||||
node-version: 22.x
|
||||
- id: set-matrix
|
||||
run: |
|
||||
node -e "
|
||||
const testedVersions = require('./lib/version').testedVersions;
|
||||
console.log('matrix='+JSON.stringify({'include': testedVersions.map(mcVersion => ({mcVersion}))}))
|
||||
" >> $GITHUB_OUTPUT
|
||||
|
||||
MinecraftServer:
|
||||
needs: PrepareSupportedVersions
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix: ${{fromJson(needs.PrepareSupportedVersions.outputs.matrix)}}
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v1.4.4
|
||||
with:
|
||||
node-version: 22.x
|
||||
- name: Setup Java JDK
|
||||
uses: actions/setup-java@v1.4.3
|
||||
with:
|
||||
java-version: 21
|
||||
java-package: jre
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
- name: Start Tests
|
||||
run: npm run mocha_test -- -g ${{ matrix.mcVersion }}v
|
||||
22
bridge/lib/mineflayer/.github/workflows/commands.yml
vendored
Normal file
22
bridge/lib/mineflayer/.github/workflows/commands.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: Repo Commands
|
||||
|
||||
on:
|
||||
issue_comment: # Handle comment commands
|
||||
types: [created]
|
||||
pull_request: # Handle renamed PRs
|
||||
types: [edited]
|
||||
|
||||
jobs:
|
||||
comment-trigger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Run command handlers
|
||||
uses: PrismarineJS/prismarine-repo-actions@master
|
||||
with:
|
||||
# NOTE: You must specify a Personal Access Token (PAT) with repo access here. While you can use the default GITHUB_TOKEN, actions taken with it will not trigger other actions, so if you have a CI workflow, commits created by this action will not trigger it.
|
||||
token: ${{ secrets.PAT_PASSWORD }}
|
||||
# See `Options` section below for more info on these options
|
||||
install-command: npm install
|
||||
/fixlint.fix-command: npm run fix
|
||||
32
bridge/lib/mineflayer/.github/workflows/npm-publish.yml
vendored
Normal file
32
bridge/lib/mineflayer/.github/workflows/npm-publish.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: npm-publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master # Change this to your default branch
|
||||
jobs:
|
||||
npm-publish:
|
||||
name: npm-publish
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 22.0.0
|
||||
- id: publish
|
||||
uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_AUTH_TOKEN }}
|
||||
- name: Create Release
|
||||
if: steps.publish.outputs.type != 'none'
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.publish.outputs.version }}
|
||||
release_name: Release ${{ steps.publish.outputs.version }}
|
||||
body: ${{ steps.publish.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
13
bridge/lib/mineflayer/.gitignore
vendored
Normal file
13
bridge/lib/mineflayer/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# shared with .npmignore
|
||||
# different than .npmignore
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
/README.md
|
||||
versions/
|
||||
server_jars/
|
||||
test/server_*
|
||||
.vscode
|
||||
.DS_Store
|
||||
launcher_accounts.json
|
||||
data
|
||||
2
bridge/lib/mineflayer/.gitpod.yml
Normal file
2
bridge/lib/mineflayer/.gitpod.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
tasks:
|
||||
- command: npm install && sdk install java
|
||||
3
bridge/lib/mineflayer/.npmignore
Normal file
3
bridge/lib/mineflayer/.npmignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# shared with .gitignore
|
||||
# different than .gitignore
|
||||
test
|
||||
2
bridge/lib/mineflayer/.npmrc
Normal file
2
bridge/lib/mineflayer/.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
engine-strict=true
|
||||
package-lock=false
|
||||
21
bridge/lib/mineflayer/LICENSE
Normal file
21
bridge/lib/mineflayer/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Andrew Kelley
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6351
bridge/lib/mineflayer/bedrock-types.d.ts
vendored
Normal file
6351
bridge/lib/mineflayer/bedrock-types.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
878
bridge/lib/mineflayer/index.d.ts
vendored
Normal file
878
bridge/lib/mineflayer/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,878 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
import { Client, ClientOptions } from 'minecraft-protocol';
|
||||
import { Vec3 } from 'vec3';
|
||||
import { Item } from 'prismarine-item';
|
||||
import { Window } from 'prismarine-windows';
|
||||
import { Recipe } from 'prismarine-recipe';
|
||||
import { Block } from 'prismarine-block';
|
||||
import { Entity } from 'prismarine-entity';
|
||||
|
||||
// Extend Entity interface for Bedrock-specific properties
|
||||
declare module 'prismarine-entity' {
|
||||
interface Entity {
|
||||
// Bedrock-specific entity properties
|
||||
attributes?: Record<
|
||||
string,
|
||||
{
|
||||
current: number;
|
||||
value?: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
default?: number;
|
||||
}
|
||||
>;
|
||||
unique_id?: bigint;
|
||||
nametag?: string;
|
||||
isInWater?: boolean;
|
||||
isCollidedVertically?: boolean;
|
||||
isCollidedHorizontally?: boolean;
|
||||
headYaw?: number;
|
||||
timeSinceOnGround?: number;
|
||||
}
|
||||
}
|
||||
import { ChatMessage } from 'prismarine-chat';
|
||||
import { world } from 'prismarine-world';
|
||||
import { Registry } from 'prismarine-registry';
|
||||
|
||||
// Extend Registry interface for Bedrock-specific properties
|
||||
declare module 'prismarine-registry' {
|
||||
interface Registry {
|
||||
// Bedrock-specific registry properties
|
||||
blocksByRuntimeId?: Record<number, any>;
|
||||
dimensionsByName?: Record<string, { minY: number; height: number }>;
|
||||
handleItemRegistry?: (packet: any) => void;
|
||||
handleStartGame?: (packet: any) => void;
|
||||
}
|
||||
}
|
||||
import { IndexedData } from 'minecraft-data';
|
||||
|
||||
export function createBot(options: BotOptions): Bot;
|
||||
export function createBot(options: { client: Client } & Partial<BotOptions>): Bot;
|
||||
|
||||
// Logger exports
|
||||
export { Logger, LogLevel, LoggerColors } from './lib/logger/logger.mts';
|
||||
|
||||
export interface BotOptions extends ClientOptions {
|
||||
logErrors?: boolean;
|
||||
hideErrors?: boolean;
|
||||
loadInternalPlugins?: boolean;
|
||||
plugins?: PluginOptions;
|
||||
chat?: ChatLevel;
|
||||
colorsEnabled?: boolean;
|
||||
viewDistance?: ViewDistance;
|
||||
mainHand?: MainHands;
|
||||
difficulty?: number;
|
||||
chatLengthLimit?: number;
|
||||
physicsEnabled?: boolean;
|
||||
/** @default 4 */
|
||||
maxCatchupTicks?: number;
|
||||
client?: Client;
|
||||
brand?: string;
|
||||
defaultChatPatterns?: boolean;
|
||||
respawn?: boolean;
|
||||
/** Enable debug logging */
|
||||
debug?: boolean;
|
||||
/** Set the log level (overrides debug option) */
|
||||
logLevel?: LogLevel;
|
||||
}
|
||||
|
||||
export type ChatLevel = 'enabled' | 'commandsOnly' | 'disabled';
|
||||
export type ViewDistance = 'far' | 'normal' | 'short' | 'tiny' | number;
|
||||
export type MainHands = 'left' | 'right';
|
||||
|
||||
export interface PluginOptions {
|
||||
[plugin: string]: boolean | Plugin;
|
||||
}
|
||||
|
||||
export type Plugin = (bot: Bot, options: BotOptions) => void;
|
||||
|
||||
export interface BotEvents {
|
||||
chat: (username: string, message: string, translate: string | null, jsonMsg: ChatMessage, matches: string[] | null) => Promise<void> | void;
|
||||
whisper: (username: string, message: string, translate: string | null, jsonMsg: ChatMessage, matches: string[] | null) => Promise<void> | void;
|
||||
actionBar: (jsonMsg: ChatMessage) => Promise<void> | void;
|
||||
error: (err: Error) => Promise<void> | void;
|
||||
message: (jsonMsg: ChatMessage, position: string) => Promise<void> | void;
|
||||
messagestr: (message: string, position: string, jsonMsg: ChatMessage) => Promise<void> | void;
|
||||
unmatchedMessage: (stringMsg: string, jsonMsg: ChatMessage) => Promise<void> | void;
|
||||
inject_allowed: () => Promise<void> | void;
|
||||
login: () => Promise<void> | void;
|
||||
/** When `respawn` option is disabled, you can call this method manually to respawn. */
|
||||
spawn: () => Promise<void> | void;
|
||||
respawn: () => Promise<void> | void;
|
||||
game: () => Promise<void> | void;
|
||||
title: (text: string, type: 'subtitle' | 'title') => Promise<void> | void;
|
||||
rain: () => Promise<void> | void;
|
||||
time: () => Promise<void> | void;
|
||||
kicked: (reason: string, loggedIn: boolean) => Promise<void> | void;
|
||||
end: (reason: string) => Promise<void> | void;
|
||||
spawnReset: () => Promise<void> | void;
|
||||
death: () => Promise<void> | void;
|
||||
health: () => Promise<void> | void;
|
||||
breath: () => Promise<void> | void;
|
||||
entitySwingArm: (entity: Entity) => Promise<void> | void;
|
||||
entityHurt: (entity: Entity, source: Entity) => Promise<void> | void;
|
||||
entityDead: (entity: Entity) => Promise<void> | void;
|
||||
entityTaming: (entity: Entity) => Promise<void> | void;
|
||||
entityTamed: (entity: Entity) => Promise<void> | void;
|
||||
entityShakingOffWater: (entity: Entity) => Promise<void> | void;
|
||||
entityEatingGrass: (entity: Entity) => Promise<void> | void;
|
||||
entityHandSwap: (entity: Entity) => Promise<void> | void;
|
||||
entityWake: (entity: Entity) => Promise<void> | void;
|
||||
entityEat: (entity: Entity) => Promise<void> | void;
|
||||
entityCriticalEffect: (entity: Entity) => Promise<void> | void;
|
||||
entityMagicCriticalEffect: (entity: Entity) => Promise<void> | void;
|
||||
entityCrouch: (entity: Entity) => Promise<void> | void;
|
||||
entityUncrouch: (entity: Entity) => Promise<void> | void;
|
||||
entityEquip: (entity: Entity) => Promise<void> | void;
|
||||
entitySleep: (entity: Entity) => Promise<void> | void;
|
||||
entitySpawn: (entity: Entity) => Promise<void> | void;
|
||||
entityElytraFlew: (entity: Entity) => Promise<void> | void;
|
||||
usedFirework: () => Promise<void> | void;
|
||||
itemDrop: (entity: Entity) => Promise<void> | void;
|
||||
playerCollect: (collector: Entity, collected: Entity) => Promise<void> | void;
|
||||
entityAttributes: (entity: Entity) => Promise<void> | void;
|
||||
entityGone: (entity: Entity) => Promise<void> | void;
|
||||
entityMoved: (entity: Entity) => Promise<void> | void;
|
||||
entityDetach: (entity: Entity, vehicle: Entity) => Promise<void> | void;
|
||||
entityAttach: (entity: Entity, vehicle: Entity) => Promise<void> | void;
|
||||
entityUpdate: (entity: Entity) => Promise<void> | void;
|
||||
entityEffect: (entity: Entity, effect: Effect) => Promise<void> | void;
|
||||
entityEffectEnd: (entity: Entity, effect: Effect) => Promise<void> | void;
|
||||
playerJoined: (player: Player) => Promise<void> | void;
|
||||
playerUpdated: (player: Player) => Promise<void> | void;
|
||||
playerLeft: (entity: Player) => Promise<void> | void;
|
||||
blockUpdate: (oldBlock: Block | null, newBlock: Block) => Promise<void> | void;
|
||||
'blockUpdate:(x, y, z)': (oldBlock: Block | null, newBlock: Block | null) => Promise<void> | void;
|
||||
chunkColumnLoad: (entity: Vec3) => Promise<void> | void;
|
||||
chunkColumnUnload: (entity: Vec3) => Promise<void> | void;
|
||||
soundEffectHeard: (soundName: string, position: Vec3, volume: number, pitch: number) => Promise<void> | void;
|
||||
hardcodedSoundEffectHeard: (soundId: number, soundCategory: number, position: Vec3, volume: number, pitch: number) => Promise<void> | void;
|
||||
noteHeard: (block: Block, instrument: Instrument, pitch: number) => Promise<void> | void;
|
||||
pistonMove: (block: Block, isPulling: number, direction: number) => Promise<void> | void;
|
||||
chestLidMove: (block: Block, isOpen: number | boolean, block2: Block | null) => Promise<void> | void;
|
||||
blockBreakProgressObserved: (block: Block, destroyStage: number, entity?: Entity) => Promise<void> | void;
|
||||
blockBreakProgressEnd: (block: Block, entity?: Entity) => Promise<void> | void;
|
||||
diggingCompleted: (block: Block) => Promise<void> | void;
|
||||
diggingAborted: (block: Block) => Promise<void> | void;
|
||||
move: (position: Vec3) => Promise<void> | void;
|
||||
forcedMove: () => Promise<void> | void;
|
||||
mount: () => Promise<void> | void;
|
||||
dismount: (vehicle: Entity) => Promise<void> | void;
|
||||
windowOpen: (window: Window) => Promise<void> | void;
|
||||
windowClose: (window: Window) => Promise<void> | void;
|
||||
sleep: () => Promise<void> | void;
|
||||
wake: () => Promise<void> | void;
|
||||
experience: () => Promise<void> | void;
|
||||
physicsTick: () => Promise<void> | void;
|
||||
physicTick: () => Promise<void> | void;
|
||||
scoreboardCreated: (scoreboard: ScoreBoard) => Promise<void> | void;
|
||||
scoreboardDeleted: (scoreboard: ScoreBoard) => Promise<void> | void;
|
||||
scoreboardTitleChanged: (scoreboard: ScoreBoard) => Promise<void> | void;
|
||||
scoreUpdated: (scoreboard: ScoreBoard, item: number) => Promise<void> | void;
|
||||
scoreRemoved: (scoreboard: ScoreBoard, item: number) => Promise<void> | void;
|
||||
scoreboardPosition: (position: DisplaySlot, scoreboard: ScoreBoard) => Promise<void> | void;
|
||||
teamCreated: (team: Team) => Promise<void> | void;
|
||||
teamRemoved: (team: Team) => Promise<void> | void;
|
||||
teamUpdated: (team: Team) => Promise<void> | void;
|
||||
teamMemberAdded: (team: Team) => Promise<void> | void;
|
||||
teamMemberRemoved: (team: Team) => Promise<void> | void;
|
||||
bossBarCreated: (bossBar: BossBar) => Promise<void> | void;
|
||||
bossBarDeleted: (bossBar: BossBar) => Promise<void> | void;
|
||||
bossBarUpdated: (bossBar: BossBar) => Promise<void> | void;
|
||||
resourcePack: (url: string, hash?: string, uuid?: string) => Promise<void> | void;
|
||||
particle: (particle: Particle) => Promise<void> | void;
|
||||
heldItemChanged: (heldItem: Item | null) => void;
|
||||
|
||||
// Node.js EventEmitter events
|
||||
newListener: (eventName: string, listener: Function) => void;
|
||||
removeListener: (eventName: string, listener: Function) => void;
|
||||
|
||||
// Bedrock-specific events
|
||||
update_attributes: (entity: Entity, attributes: any[]) => void;
|
||||
playerMoved: (player: Player) => void;
|
||||
weatherUpdate: () => void;
|
||||
subchunkContainingPlayerChanged: (oldChunk: Vec3, newChunk: Vec3) => void;
|
||||
|
||||
// Dynamic event patterns (use string index signature for these)
|
||||
[key: `chat:${string}`]: (username: string, message: string, translate: string | null, jsonMsg: ChatMessage, matches: string[] | null) => void;
|
||||
[key: `setWindowItems:${number}`]: (window: Window) => void;
|
||||
[key: `itemStackResponse:${number}`]: (response: any) => void;
|
||||
}
|
||||
|
||||
export interface CommandBlockOptions {
|
||||
mode: number;
|
||||
trackOutput: boolean;
|
||||
conditional: boolean;
|
||||
alwaysActive: boolean;
|
||||
}
|
||||
|
||||
export interface Bot extends TypedEmitter<BotEvents> {
|
||||
username: string;
|
||||
protocolVersion: string;
|
||||
majorVersion: string;
|
||||
version: string;
|
||||
entity: Entity;
|
||||
entities: { [id: string]: Entity };
|
||||
fireworkRocketDuration: number;
|
||||
spawnPoint: Vec3;
|
||||
game: GameState;
|
||||
player: Player;
|
||||
players: { [username: string]: Player };
|
||||
isRaining: boolean;
|
||||
thunderState: number;
|
||||
chatPatterns: ChatPattern[];
|
||||
settings: GameSettings;
|
||||
experience: Experience;
|
||||
health: number;
|
||||
food: number;
|
||||
foodSaturation: number;
|
||||
oxygenLevel: number;
|
||||
physics: PhysicsOptions;
|
||||
physicsEnabled: boolean;
|
||||
time: Time;
|
||||
quickBarSlot: number;
|
||||
inventory: Window<StorageEvents>;
|
||||
targetDigBlock: Block;
|
||||
isSleeping: boolean;
|
||||
scoreboards: { [name: string]: ScoreBoard };
|
||||
scoreboard: { [slot in DisplaySlot]: ScoreBoard };
|
||||
teams: { [name: string]: Team };
|
||||
teamMap: { [name: string]: Team };
|
||||
controlState: ControlStateStatus;
|
||||
creative: creativeMethods;
|
||||
world: world.WorldSync;
|
||||
_client: Client;
|
||||
heldItem: Item | null;
|
||||
usingHeldItem: boolean;
|
||||
currentWindow: Window | null;
|
||||
simpleClick: simpleClick;
|
||||
tablist: Tablist;
|
||||
registry: Registry;
|
||||
logger: import('./lib/logger/logger.mts').Logger;
|
||||
|
||||
connect: (options: BotOptions) => void;
|
||||
|
||||
supportFeature: IndexedData['supportFeature'];
|
||||
|
||||
end: (reason?: string) => void;
|
||||
|
||||
blockAt: (point: Vec3, extraInfos?: boolean) => Block | null;
|
||||
|
||||
blockInSight: (maxSteps: number, vectorLength: number) => Block | null;
|
||||
|
||||
blockAtCursor: (maxDistance?: number, matcher?: Function) => Block | null;
|
||||
blockAtEntityCursor: (entity?: Entity, maxDistance?: number, matcher?: Function) => Block | null;
|
||||
|
||||
canSeeBlock: (block: Block) => boolean;
|
||||
|
||||
findBlock: (options: FindBlockOptions) => Block | null;
|
||||
|
||||
findBlocks: (options: FindBlockOptions) => Vec3[];
|
||||
|
||||
canDigBlock: (block: Block) => boolean;
|
||||
|
||||
recipesFor: (itemType: number, metadata: number | null, minResultCount: number | null, craftingTable: Block | boolean | null) => Recipe[];
|
||||
|
||||
recipesAll: (itemType: number, metadata: number | null, craftingTable: Block | boolean | null) => Recipe[];
|
||||
|
||||
quit: (reason?: string) => void;
|
||||
|
||||
tabComplete: (str: string, assumeCommand?: boolean, sendBlockInSight?: boolean, timeout?: number) => Promise<string[]>;
|
||||
|
||||
chat: (message: string) => void;
|
||||
|
||||
whisper: (username: string, message: string) => void;
|
||||
|
||||
chatAddPattern: (pattern: RegExp, chatType: string, description?: string) => number;
|
||||
|
||||
setSettings: (options: Partial<GameSettings>) => void;
|
||||
|
||||
loadPlugin: (plugin: Plugin) => void;
|
||||
|
||||
loadPlugins: (plugins: Plugin[]) => void;
|
||||
|
||||
hasPlugin: (plugin: Plugin) => boolean;
|
||||
|
||||
sleep: (bedBlock: Block) => Promise<void>;
|
||||
|
||||
isABed: (bedBlock: Block) => boolean;
|
||||
|
||||
wake: () => Promise<void>;
|
||||
|
||||
elytraFly: () => Promise<void>;
|
||||
|
||||
setControlState: (control: ControlState, state: boolean) => void;
|
||||
|
||||
getControlState: (control: ControlState) => boolean;
|
||||
|
||||
clearControlStates: () => void;
|
||||
|
||||
getExplosionDamages: (targetEntity: Entity, position: Vec3, radius: number, rawDamages?: boolean) => number | null;
|
||||
|
||||
lookAt: (point: Vec3, force?: boolean) => Promise<void>;
|
||||
|
||||
look: (yaw: number, pitch: number, force?: boolean) => Promise<void>;
|
||||
|
||||
updateSign: (block: Block, text: string, back?: boolean) => void;
|
||||
|
||||
equip: (item: Item | number, destination: EquipmentDestination | null) => Promise<void>;
|
||||
|
||||
unequip: (destination: EquipmentDestination | null) => Promise<void>;
|
||||
|
||||
tossStack: (item: Item) => Promise<void>;
|
||||
|
||||
toss: (itemType: number, metadata: number | null, count: number | null) => Promise<void>;
|
||||
|
||||
dig: ((block: Block, forceLook?: boolean | 'ignore') => Promise<void>) & ((block: Block, forceLook: boolean | 'ignore', digFace: 'auto' | Vec3 | 'raycast') => Promise<void>);
|
||||
|
||||
stopDigging: () => void;
|
||||
|
||||
digTime: (block: Block) => number;
|
||||
|
||||
placeBlock: (referenceBlock: Block, faceVector: Vec3) => Promise<void>;
|
||||
|
||||
placeEntity: (referenceBlock: Block, faceVector: Vec3) => Promise<Entity>;
|
||||
|
||||
activateBlock: (block: Block, direction?: Vec3, cursorPos?: Vec3) => Promise<void>;
|
||||
|
||||
activateEntity: (entity: Entity) => Promise<void>;
|
||||
|
||||
activateEntityAt: (entity: Entity, position: Vec3) => Promise<void>;
|
||||
|
||||
consume: () => Promise<void>;
|
||||
|
||||
fish: () => Promise<void>;
|
||||
|
||||
activateItem: (offhand?: boolean) => void;
|
||||
|
||||
deactivateItem: () => void;
|
||||
|
||||
useOn: (targetEntity: Entity) => void;
|
||||
|
||||
attack: (entity: Entity) => void;
|
||||
|
||||
swingArm: (hand: 'left' | 'right' | undefined, showHand?: boolean) => void;
|
||||
|
||||
mount: (entity: Entity) => void;
|
||||
|
||||
dismount: () => void;
|
||||
|
||||
moveVehicle: (left: number, forward: number) => void;
|
||||
|
||||
setQuickBarSlot: (slot: number) => void;
|
||||
|
||||
craft: (recipe: Recipe, count?: number, craftingTable?: Block) => Promise<void>;
|
||||
|
||||
writeBook: (slot: number, pages: string[]) => Promise<void>;
|
||||
|
||||
openContainer: (chest: Block | Entity, direction?: Vec3, cursorPos?: Vec3) => Promise<Chest | Dispenser>;
|
||||
|
||||
openChest: (chest: Block | Entity, direction?: number, cursorPos?: Vec3) => Promise<Chest>;
|
||||
|
||||
openFurnace: (furnace: Block) => Promise<Furnace>;
|
||||
|
||||
openDispenser: (dispenser: Block) => Promise<Dispenser>;
|
||||
|
||||
openEnchantmentTable: (enchantmentTable: Block) => Promise<EnchantmentTable>;
|
||||
|
||||
openAnvil: (anvil: Block) => Promise<Anvil>;
|
||||
|
||||
openVillager: (villager: Entity) => Promise<Villager>;
|
||||
|
||||
trade: (villagerInstance: Villager, tradeIndex: string | number, times?: number) => Promise<void>;
|
||||
|
||||
setCommandBlock: (pos: Vec3, command: string, options: CommandBlockOptions) => void;
|
||||
|
||||
clickWindow: (slot: number, mouseButton: number, mode: number) => Promise<void>;
|
||||
|
||||
putSelectedItemRange: (start: number, end: number, window: Window, slot: any) => Promise<void>;
|
||||
|
||||
putAway: (slot: number) => Promise<void>;
|
||||
|
||||
closeWindow: (window: Window) => void;
|
||||
|
||||
transfer: (options: TransferOptions) => Promise<void>;
|
||||
|
||||
openBlock: (block: Block, direction?: Vec3, cursorPos?: Vec3) => Promise<Window>;
|
||||
|
||||
openEntity: (block: Entity, Class: new () => EventEmitter) => Promise<Window>;
|
||||
|
||||
openInventory: () => Promise<void>;
|
||||
|
||||
moveSlotItem: (sourceSlot: number, destSlot: number) => Promise<void>;
|
||||
|
||||
updateHeldItem: () => void;
|
||||
|
||||
getEquipmentDestSlot: (destination: string) => number;
|
||||
|
||||
getNextItemStackRequestId: () => number;
|
||||
|
||||
waitForChunksToLoad: () => Promise<void>;
|
||||
|
||||
entityAtCursor: (maxDistance?: number) => Entity | null;
|
||||
nearestEntity: (filter?: (entity: Entity) => boolean) => Entity | null;
|
||||
|
||||
waitForTicks: (ticks: number) => Promise<void>;
|
||||
|
||||
addChatPattern: (name: string, pattern: RegExp, options?: chatPatternOptions) => number;
|
||||
|
||||
addChatPatternSet: (name: string, patterns: RegExp[], options?: chatPatternOptions) => number;
|
||||
|
||||
removeChatPattern: (name: string | number) => void;
|
||||
|
||||
awaitMessage: (...args: string[] | RegExp[]) => Promise<string>;
|
||||
|
||||
acceptResourcePack: () => void;
|
||||
|
||||
denyResourcePack: () => void;
|
||||
|
||||
respawn: () => void;
|
||||
|
||||
close: () => void;
|
||||
|
||||
cameraState: { pitch: number; yaw: number };
|
||||
|
||||
item_registry_task: { promise: Promise<void> } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bedrock client with typed packet event handlers.
|
||||
* Use this for explicit typing when you need typed packet events.
|
||||
*/
|
||||
export interface BedrockClient extends TypedEmitter<protocolTypes.BedrockPacketEventMap & { login: () => void; join: () => void }> {
|
||||
username: string;
|
||||
|
||||
write<K extends keyof protocolTypes.BedrockPacketEventMap>(name: K, params: Parameters<protocolTypes.BedrockPacketEventMap[K]>[0]): void;
|
||||
|
||||
queue<K extends keyof protocolTypes.BedrockPacketEventMap>(name: K, params: Parameters<protocolTypes.BedrockPacketEventMap[K]>[0]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bedrock bot with typed client.
|
||||
* Use this instead of Bot when working with Bedrock Edition for typed packet events.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createBot, BedrockBot } from 'mineflayer';
|
||||
*
|
||||
* const bot = createBot({ ... }) as BedrockBot;
|
||||
*
|
||||
* bot._client.on('text', (packet) => {
|
||||
* console.log(packet.message); // Fully typed!
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export interface BedrockBot extends Bot {
|
||||
_client: BedrockClient;
|
||||
|
||||
// Bedrock-specific properties
|
||||
isAlive: boolean;
|
||||
rainState: number;
|
||||
jumpQueued: boolean;
|
||||
jumpTicks: number;
|
||||
selectedSlot: number;
|
||||
QUICK_BAR_START: number;
|
||||
|
||||
// Bedrock-specific mappings
|
||||
uuidToUsername: Record<string, string>;
|
||||
|
||||
// Bedrock-specific methods
|
||||
findPlayer: (filter: (player: Player) => boolean) => Player | null;
|
||||
findPlayers: (filter: (player: Player) => boolean) => Player[];
|
||||
_playerFromUUID: (uuid: string) => Player | null;
|
||||
|
||||
// Digging state
|
||||
targetDigFace: Vec3 | null;
|
||||
lastDigTime: number;
|
||||
|
||||
// Block update method
|
||||
_updateBlockState: (position: Vec3, stateId: number) => void;
|
||||
|
||||
// Player auth input transaction
|
||||
sendPlayerAuthInputTransaction: (transaction: any) => void;
|
||||
|
||||
// Item use
|
||||
useItem: () => void;
|
||||
}
|
||||
|
||||
export interface simpleClick {
|
||||
leftMouse: (slot: number) => Promise<void>;
|
||||
rightMouse: (slot: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface Tablist {
|
||||
header: ChatMessage;
|
||||
footer: ChatMessage;
|
||||
}
|
||||
|
||||
export interface chatPatternOptions {
|
||||
repeat?: boolean;
|
||||
parse?: boolean;
|
||||
deprecated?: boolean;
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
levelType: LevelType;
|
||||
gameMode: GameMode;
|
||||
hardcore: boolean;
|
||||
dimension: Dimension;
|
||||
difficulty: Difficulty;
|
||||
maxPlayers: number;
|
||||
serverBrand: string;
|
||||
/** Minimum Y coordinate of the world (Bedrock) */
|
||||
minY: number;
|
||||
/** Height of the world (Bedrock) */
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type LevelType = 'default' | 'flat' | 'largeBiomes' | 'amplified' | 'customized' | 'buffet' | 'default_1_1';
|
||||
export type GameMode = 'survival' | 'creative' | 'adventure' | 'spectator';
|
||||
export type Dimension = 'the_nether' | 'overworld' | 'the_end';
|
||||
export type Difficulty = 'peaceful' | 'easy' | 'normal' | 'hard';
|
||||
|
||||
export interface Player {
|
||||
uuid: string;
|
||||
username: string;
|
||||
displayName: ChatMessage;
|
||||
gamemode: number;
|
||||
ping: number;
|
||||
entity: Entity;
|
||||
skinData: SkinData | undefined;
|
||||
profileKeys?: {
|
||||
publicKey: Buffer;
|
||||
signature: Buffer;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SkinData {
|
||||
url: string;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export interface ChatPattern {
|
||||
pattern: RegExp;
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SkinParts {
|
||||
showCape: boolean;
|
||||
showJacket: boolean;
|
||||
showLeftSleeve: boolean;
|
||||
showRightSleeve: boolean;
|
||||
showLeftPants: boolean;
|
||||
showRightPants: boolean;
|
||||
showHat: boolean;
|
||||
}
|
||||
|
||||
export interface GameSettings {
|
||||
chat: ChatLevel;
|
||||
colorsEnabled: boolean;
|
||||
viewDistance: ViewDistance;
|
||||
difficulty: number;
|
||||
skinParts: SkinParts;
|
||||
mainHand: MainHands;
|
||||
}
|
||||
|
||||
export interface Experience {
|
||||
level: number;
|
||||
points: number;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
export interface PhysicsOptions {
|
||||
maxGroundSpeed: number;
|
||||
terminalVelocity: number;
|
||||
walkingAcceleration: number;
|
||||
gravity: number;
|
||||
groundFriction: number;
|
||||
playerApothem: number;
|
||||
playerHeight: number;
|
||||
jumpSpeed: number;
|
||||
yawSpeed: number;
|
||||
pitchSpeed: number;
|
||||
sprintSpeed: number;
|
||||
maxGroundSpeedSoulSand: number;
|
||||
maxGroundSpeedWater: number;
|
||||
}
|
||||
|
||||
export interface Time {
|
||||
doDaylightCycle: boolean;
|
||||
bigTime: BigInt;
|
||||
time: number;
|
||||
timeOfDay: number;
|
||||
day: number;
|
||||
isDay: boolean;
|
||||
moonPhase: number;
|
||||
bigAge: BigInt;
|
||||
age: number;
|
||||
}
|
||||
|
||||
export interface ControlStateStatus {
|
||||
forward: boolean;
|
||||
back: boolean;
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
jump: boolean;
|
||||
sprint: boolean;
|
||||
sneak: boolean;
|
||||
}
|
||||
|
||||
export type ControlState = 'forward' | 'back' | 'left' | 'right' | 'jump' | 'sprint' | 'sneak';
|
||||
|
||||
export interface Effect {
|
||||
id: number;
|
||||
amplifier: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface Instrument {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FindBlockOptions {
|
||||
point?: Vec3;
|
||||
matching: number | number[] | ((block: Block) => boolean);
|
||||
maxDistance?: number;
|
||||
count?: number;
|
||||
useExtraInfo?: boolean | ((block: Block) => boolean);
|
||||
}
|
||||
|
||||
export type EquipmentDestination = 'hand' | 'head' | 'torso' | 'legs' | 'feet' | 'off-hand';
|
||||
|
||||
export interface TransferOptions {
|
||||
window: Window;
|
||||
itemType: number;
|
||||
metadata: number | null;
|
||||
count?: number;
|
||||
sourceStart: number;
|
||||
sourceEnd: number;
|
||||
destStart: number;
|
||||
destEnd: number;
|
||||
}
|
||||
|
||||
export interface creativeMethods {
|
||||
setInventorySlot: (slot: number, item: Item | null) => Promise<void>;
|
||||
|
||||
clearSlot: (slot: number) => Promise<void>;
|
||||
|
||||
clearInventory: () => Promise<void>;
|
||||
|
||||
flyTo: (destination: Vec3) => Promise<void>;
|
||||
|
||||
startFlying: () => void;
|
||||
|
||||
stopFlying: () => void;
|
||||
}
|
||||
|
||||
export class Location {
|
||||
floored: Vec3;
|
||||
blockPoint: Vec3;
|
||||
chunkCorner: Vec3;
|
||||
blockIndex: number;
|
||||
biomeBlockIndex: number;
|
||||
chunkYIndex: number;
|
||||
|
||||
constructor(absoluteVector: Vec3);
|
||||
}
|
||||
|
||||
export class Painting {
|
||||
id: number;
|
||||
position: Vec3;
|
||||
name: string;
|
||||
direction: Vec3;
|
||||
|
||||
constructor(id: number, position: Vec3, name: string, direction: Vec3);
|
||||
}
|
||||
|
||||
interface StorageEvents {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
updateSlot: (slot: number, oldItem: Item | null, newItem: Item | null) => void;
|
||||
}
|
||||
|
||||
interface FurnaceEvents extends StorageEvents {
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
interface ConditionalStorageEvents extends StorageEvents {
|
||||
ready: () => void;
|
||||
}
|
||||
|
||||
export class Chest extends Window<StorageEvents> {
|
||||
constructor();
|
||||
|
||||
close(): void;
|
||||
|
||||
deposit(itemType: number, metadata: number | null, count: number | null): Promise<void>;
|
||||
|
||||
withdraw(itemType: number, metadata: number | null, count: number | null): Promise<void>;
|
||||
}
|
||||
|
||||
export class Furnace extends Window<FurnaceEvents> {
|
||||
fuel: number;
|
||||
progress: number;
|
||||
|
||||
constructor();
|
||||
|
||||
close(): void;
|
||||
|
||||
takeInput(): Promise<Item>;
|
||||
|
||||
takeFuel(): Promise<Item>;
|
||||
|
||||
takeOutput(): Promise<Item>;
|
||||
|
||||
putInput(itemType: number, metadata: number | null, count: number): Promise<void>;
|
||||
|
||||
putFuel(itemType: number, metadata: number | null, count: number): Promise<void>;
|
||||
|
||||
inputItem(): Item;
|
||||
|
||||
fuelItem(): Item;
|
||||
|
||||
outputItem(): Item;
|
||||
}
|
||||
|
||||
export class Dispenser extends Window<StorageEvents> {
|
||||
constructor();
|
||||
|
||||
close(): void;
|
||||
|
||||
deposit(itemType: number, metadata: number | null, count: number | null): Promise<void>;
|
||||
|
||||
withdraw(itemType: number, metadata: number | null, count: number | null): Promise<void>;
|
||||
}
|
||||
|
||||
export class EnchantmentTable extends Window<ConditionalStorageEvents> {
|
||||
enchantments: Enchantment[];
|
||||
|
||||
constructor();
|
||||
|
||||
close(): void;
|
||||
|
||||
targetItem(): Item;
|
||||
|
||||
enchant(choice: string | number): Promise<Item>;
|
||||
|
||||
takeTargetItem(): Promise<Item>;
|
||||
|
||||
putTargetItem(item: Item): Promise<Item>;
|
||||
|
||||
putLapis(item: Item): Promise<Item>;
|
||||
}
|
||||
|
||||
export class Anvil {
|
||||
combine(itemOne: Item, itemTwo: Item, name?: string): Promise<void>;
|
||||
rename(item: Item, name?: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface Enchantment {
|
||||
level: number;
|
||||
expected: { enchant: number; level: number };
|
||||
}
|
||||
|
||||
export class Villager extends Window<ConditionalStorageEvents> {
|
||||
trades: VillagerTrade[];
|
||||
|
||||
constructor();
|
||||
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export interface VillagerTrade {
|
||||
inputItem1: Item;
|
||||
outputItem: Item;
|
||||
inputItem2: Item | null;
|
||||
hasItem2: boolean;
|
||||
tradeDisabled: boolean;
|
||||
nbTradeUses: number;
|
||||
maximumNbTradeUses: number;
|
||||
xp?: number;
|
||||
specialPrice?: number;
|
||||
priceMultiplier?: number;
|
||||
demand?: number;
|
||||
realPrice?: number;
|
||||
}
|
||||
|
||||
export class ScoreBoard {
|
||||
name: string;
|
||||
title: string;
|
||||
itemsMap: { [name: string]: ScoreBoardItem };
|
||||
items: ScoreBoardItem[];
|
||||
|
||||
constructor(packet: object);
|
||||
|
||||
setTitle(title: string): void;
|
||||
|
||||
add(name: string, value: number): ScoreBoardItem;
|
||||
|
||||
remove(name: string): ScoreBoardItem;
|
||||
}
|
||||
|
||||
export interface ScoreBoardItem {
|
||||
name: string;
|
||||
displayName: ChatMessage;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export class Team {
|
||||
team: string;
|
||||
name: ChatMessage;
|
||||
friendlyFire: number;
|
||||
nameTagVisibility: string;
|
||||
collisionRule: string;
|
||||
color: string;
|
||||
prefix: ChatMessage;
|
||||
suffix: ChatMessage;
|
||||
memberMap: { [name: string]: '' };
|
||||
members: string[];
|
||||
|
||||
constructor(team: string, name: string, friendlyFire: boolean, nameTagVisibility: string, collisionRule: string, formatting: number, prefix: string, suffix: string);
|
||||
|
||||
parseMessage(value: string): ChatMessage;
|
||||
|
||||
add(name: string, value: number): void;
|
||||
|
||||
remove(name: string): void;
|
||||
|
||||
update(name: string, friendlyFire: boolean, nameTagVisibility: string, collisionRule: string, formatting: number, prefix: string, suffix: string): void;
|
||||
|
||||
displayName(member: string): ChatMessage;
|
||||
}
|
||||
|
||||
export type DisplaySlot = 'list' | 'sidebar' | 'belowName' | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18;
|
||||
|
||||
export class BossBar {
|
||||
entityUUID: string;
|
||||
title: ChatMessage;
|
||||
health: number;
|
||||
dividers: number;
|
||||
color: 'pink' | 'blue' | 'red' | 'green' | 'yellow' | 'purple' | 'white';
|
||||
shouldDarkenSky: boolean;
|
||||
isDragonBar: boolean;
|
||||
createFog: boolean;
|
||||
shouldCreateFog: boolean;
|
||||
|
||||
constructor(uuid: string, title: string, health: number, dividers: number, color: number, flags: number);
|
||||
}
|
||||
|
||||
export class Particle {
|
||||
id: number;
|
||||
position: Vec3;
|
||||
offset: Vec3;
|
||||
count: number;
|
||||
movementSpeed: number;
|
||||
longDistanceRender: boolean;
|
||||
static fromNetwork(packet: Object): Particle;
|
||||
|
||||
constructor(id: number, position: Vec3, offset: Vec3, count?: number, movementSpeed?: number, longDistanceRender?: boolean);
|
||||
}
|
||||
|
||||
export let testedVersions: string[];
|
||||
export let latestSupportedVersion: string;
|
||||
export let oldestSupportedVersion: string;
|
||||
|
||||
export function supportFeature(feature: string, version: string): boolean;
|
||||
7
bridge/lib/mineflayer/index.js
Normal file
7
bridge/lib/mineflayer/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
if (typeof process !== 'undefined' && !process.browser && process.platform !== 'browser' && parseInt(process.versions.node.split('.')[0]) < 18) {
|
||||
console.error('Your node version is currently', process.versions.node)
|
||||
console.error('Please update it to a version >= 22.x.x from https://nodejs.org/')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
module.exports = require('./lib/loader.js')
|
||||
54
bridge/lib/mineflayer/lib/BlobStore.js
Normal file
54
bridge/lib/mineflayer/lib/BlobStore.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
class BlobStore extends Map {
|
||||
pending = {}
|
||||
wanted = []
|
||||
|
||||
set(key, value) {
|
||||
const ret = super.set(key.toString(), value)
|
||||
this.wanted.forEach(wanted => wanted[0] = wanted[0].filter(hash => hash.toString() !== key.toString()))
|
||||
for (const i in this.wanted) {
|
||||
const [outstandingBlobs, cb] = this.wanted[i]
|
||||
if (!outstandingBlobs.length) {
|
||||
cb()
|
||||
delete this.wanted[i]
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return super.get(key.toString())
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return super.has(key.toString())
|
||||
}
|
||||
|
||||
addPending(hash, blob) {
|
||||
this.pending[hash.toString()] = blob
|
||||
}
|
||||
|
||||
updatePending(hash, value) {
|
||||
const name = hash.toString()
|
||||
if (this.pending[name]) {
|
||||
this.set(name, Object.assign(this.pending[name], value))
|
||||
} else {
|
||||
throw new Error('No pending blob for hash ' + name)
|
||||
}
|
||||
// todo: remove from pending
|
||||
}
|
||||
|
||||
once(wantedBlobs, cb) {
|
||||
const outstanding = []
|
||||
for (const wanted of wantedBlobs) {
|
||||
if (!this.has(wanted)) outstanding.push(wanted)
|
||||
}
|
||||
|
||||
if (outstanding.length) {
|
||||
this.wanted.push([outstanding, cb])
|
||||
} else {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlobStore
|
||||
301
bridge/lib/mineflayer/lib/bedrock/container.mts
Normal file
301
bridge/lib/mineflayer/lib/bedrock/container.mts
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
/**
|
||||
* Container Operations - Unified transfer utilities for Bedrock protocol
|
||||
*
|
||||
* Provides:
|
||||
* - Generic transferItems() function that handles both deposit and withdraw
|
||||
* - depositToContainer() - Move items from player inventory to container
|
||||
* - withdrawFromContainer() - Move items from container to player inventory
|
||||
*
|
||||
* Replaces ~300 lines of duplicated code in inventory.mts
|
||||
*/
|
||||
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { BedrockBot } from '../../index.js';
|
||||
import type { protocolTypes } from '../../bedrock-types.js';
|
||||
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds, cursor, type SlotLocation } from './item-stack-actions.mts';
|
||||
import { getContainerFromSlot, SlotRanges } from './slot-mapping.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Transfer Configuration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Configuration for generic transfer operation
|
||||
*/
|
||||
export interface TransferConfig {
|
||||
bot: BedrockBot;
|
||||
sourceWindow: Window;
|
||||
sourceContainerId: string;
|
||||
sourceStart: number;
|
||||
sourceEnd: number;
|
||||
destWindow: Window;
|
||||
destContainerId: string;
|
||||
destStart: number;
|
||||
destEnd: number;
|
||||
itemType: number;
|
||||
metadata: number | null;
|
||||
nbt?: object | null;
|
||||
count: number | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Generic Transfer Function
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Transfer items between windows/containers
|
||||
*
|
||||
* This is the core transfer function that unifies deposit and withdraw operations.
|
||||
* Uses two-step cursor-based transfer (take to cursor, place from cursor).
|
||||
*
|
||||
* @param config - Transfer configuration
|
||||
* @returns Number of items transferred
|
||||
*/
|
||||
export async function transferItems(config: TransferConfig): Promise<number> {
|
||||
const { bot, sourceWindow, sourceContainerId, sourceStart, sourceEnd, destWindow, destContainerId, destStart, destEnd, itemType, metadata, nbt, count } = config;
|
||||
|
||||
const totalToTransfer = count ?? 64;
|
||||
let transferred = 0;
|
||||
const maxStackSize = bot.registry.itemsArray.find((x: any) => x.id === itemType)?.stackSize ?? 64;
|
||||
|
||||
while (transferred < totalToTransfer) {
|
||||
// Find source item (re-find each iteration as slots change)
|
||||
const sourceItem = sourceWindow.findItemRange(sourceStart, sourceEnd, itemType, metadata, false, nbt);
|
||||
if (!sourceItem) {
|
||||
if (transferred === 0) {
|
||||
const mcDataEntry = bot.registry.itemsArray.find((x: any) => x.id === itemType);
|
||||
throw new Error(`Can't find ${mcDataEntry?.name || itemType} in source`);
|
||||
}
|
||||
break; // No more items to transfer
|
||||
}
|
||||
|
||||
// Find destination slot
|
||||
let destSlot: number | null = null;
|
||||
let destStackId = 0;
|
||||
let destCurrentCount = 0;
|
||||
|
||||
// First try to stack with existing items
|
||||
const existingItem = destWindow.findItemRange(destStart, destEnd, itemType, metadata, true, nbt);
|
||||
if (existingItem) {
|
||||
destSlot = existingItem.slot;
|
||||
destStackId = getStackId(existingItem);
|
||||
destCurrentCount = existingItem.count;
|
||||
} else {
|
||||
destSlot = destWindow.firstEmptySlotRange(destStart, destEnd);
|
||||
destCurrentCount = 0;
|
||||
}
|
||||
|
||||
if (destSlot === null) {
|
||||
if (transferred === 0) {
|
||||
throw new Error('Destination is full');
|
||||
}
|
||||
break; // Destination full, but we transferred some
|
||||
}
|
||||
|
||||
// Calculate how many items to transfer in this batch
|
||||
const availableInDest = maxStackSize - destCurrentCount;
|
||||
const remainingToTransfer = totalToTransfer - transferred;
|
||||
const availableInSource = sourceItem.count;
|
||||
const transferCount = Math.min(availableInDest, remainingToTransfer, availableInSource);
|
||||
|
||||
if (transferCount <= 0) {
|
||||
// Destination stack is full, find another slot
|
||||
destSlot = destWindow.firstEmptySlotRange(destStart, destEnd);
|
||||
if (destSlot === null) {
|
||||
break; // Destination full
|
||||
}
|
||||
destStackId = 0;
|
||||
destCurrentCount = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
const stackId = getStackId(sourceItem);
|
||||
|
||||
// Determine source slot info based on container type
|
||||
let sourceSlot: SlotLocation;
|
||||
if (sourceContainerId === ContainerIds.CONTAINER) {
|
||||
sourceSlot = { containerId: ContainerIds.CONTAINER, slot: sourceItem.slot, stackId };
|
||||
} else {
|
||||
// Player inventory - use proper container mapping
|
||||
const mapped = getContainerFromSlot(sourceItem.slot, sourceWindow);
|
||||
sourceSlot = { containerId: mapped.containerId, slot: mapped.slot, stackId };
|
||||
}
|
||||
|
||||
// Step 1: Take items to cursor
|
||||
const takeRequestId = getNextItemStackRequestId();
|
||||
let cursorStackId = stackId;
|
||||
|
||||
// Capture cursor stack ID from response
|
||||
const takeResponseHandler = (packet: protocolTypes.packet_item_stack_response) => {
|
||||
for (const response of packet.responses) {
|
||||
if (response.request_id === takeRequestId && response.status === 'ok') {
|
||||
for (const container of response.containers || []) {
|
||||
if (container.slot_type?.container_id === 'cursor' && container.slots?.length > 0) {
|
||||
cursorStackId = container.slots[0].item_stack_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
bot._client.on('item_stack_response', takeResponseHandler);
|
||||
|
||||
sendRequest(bot, takeRequestId, actions().takeToCursor(transferCount, sourceSlot).build());
|
||||
|
||||
let success = await waitForResponse(bot, takeRequestId);
|
||||
bot._client.removeListener('item_stack_response', takeResponseHandler);
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Transfer failed - take rejected');
|
||||
}
|
||||
|
||||
// Step 2: Place items from cursor to destination
|
||||
const placeRequestId = getNextItemStackRequestId();
|
||||
|
||||
// Determine destination slot info
|
||||
let destSlotInfo: SlotLocation;
|
||||
if (destContainerId === ContainerIds.CONTAINER) {
|
||||
destSlotInfo = { containerId: ContainerIds.CONTAINER, slot: destSlot, stackId: destStackId };
|
||||
} else {
|
||||
// Player inventory - use proper container mapping
|
||||
const mapped = getContainerFromSlot(destSlot, destWindow);
|
||||
destSlotInfo = { containerId: mapped.containerId, slot: mapped.slot, stackId: destStackId };
|
||||
}
|
||||
|
||||
sendRequest(bot, placeRequestId, actions().placeFromCursor(transferCount, cursorStackId, destSlotInfo).build());
|
||||
|
||||
success = await waitForResponse(bot, placeRequestId);
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Transfer failed - place rejected');
|
||||
}
|
||||
|
||||
transferred += transferCount;
|
||||
|
||||
// Create new item in destination slot if needed
|
||||
// (item_stack_response doesn't know item type, so we create it manually)
|
||||
const existingDestItem = destWindow.slots[destSlot];
|
||||
if (!existingDestItem) {
|
||||
const newItem = Object.assign(Object.create(Object.getPrototypeOf(sourceItem)), sourceItem);
|
||||
newItem.count = transferCount;
|
||||
newItem.slot = destSlot;
|
||||
(newItem as any).stackId = destStackId;
|
||||
destWindow.updateSlot(destSlot, newItem);
|
||||
}
|
||||
}
|
||||
|
||||
return transferred;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Convenience Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Deposit items from player inventory to container
|
||||
*/
|
||||
export async function depositToContainer(
|
||||
bot: BedrockBot,
|
||||
containerWindow: Window,
|
||||
itemType: number,
|
||||
metadata: number | null,
|
||||
count: number | null,
|
||||
containerSlots: number,
|
||||
nbt?: object | null
|
||||
): Promise<number> {
|
||||
return transferItems({
|
||||
bot,
|
||||
sourceWindow: bot.inventory,
|
||||
sourceContainerId: ContainerIds.HOTBAR_AND_INVENTORY,
|
||||
sourceStart: 0,
|
||||
sourceEnd: SlotRanges.INVENTORY_END,
|
||||
destWindow: containerWindow,
|
||||
destContainerId: ContainerIds.CONTAINER,
|
||||
destStart: 0,
|
||||
destEnd: containerSlots - 1,
|
||||
itemType,
|
||||
metadata,
|
||||
nbt,
|
||||
count,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw items from container to player inventory
|
||||
*/
|
||||
export async function withdrawFromContainer(
|
||||
bot: BedrockBot,
|
||||
containerWindow: Window,
|
||||
itemType: number,
|
||||
metadata: number | null,
|
||||
count: number | null,
|
||||
containerSlots: number,
|
||||
nbt?: object | null
|
||||
): Promise<number> {
|
||||
return transferItems({
|
||||
bot,
|
||||
sourceWindow: containerWindow,
|
||||
sourceContainerId: ContainerIds.CONTAINER,
|
||||
sourceStart: 0,
|
||||
sourceEnd: containerSlots - 1,
|
||||
destWindow: bot.inventory,
|
||||
destContainerId: ContainerIds.HOTBAR_AND_INVENTORY,
|
||||
destStart: 0,
|
||||
destEnd: SlotRanges.INVENTORY_END,
|
||||
itemType,
|
||||
metadata,
|
||||
nbt,
|
||||
count,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Two-Step Transfer Helper
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Perform a two-step transfer through cursor
|
||||
* Step 1: Take from source to cursor
|
||||
* Step 2: Place from cursor to destination
|
||||
*
|
||||
* This is useful for single-item transfers where you need cursor stack ID tracking.
|
||||
*
|
||||
* @returns The cursor stack ID after the take operation
|
||||
*/
|
||||
export async function twoStepTransfer(bot: BedrockBot, source: SlotLocation, destination: SlotLocation, count: number): Promise<{ success: boolean; cursorStackId: number }> {
|
||||
// Step 1: Take to cursor
|
||||
const takeRequestId = getNextItemStackRequestId();
|
||||
let cursorStackId = source.stackId;
|
||||
|
||||
const takeResponseHandler = (packet: protocolTypes.packet_item_stack_response) => {
|
||||
for (const response of packet.responses) {
|
||||
if (response.request_id === takeRequestId && response.status === 'ok') {
|
||||
for (const container of response.containers || []) {
|
||||
if (container.slot_type?.container_id === 'cursor' && container.slots?.length > 0) {
|
||||
cursorStackId = container.slots[0].item_stack_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
bot._client.on('item_stack_response', takeResponseHandler);
|
||||
|
||||
sendRequest(bot, takeRequestId, actions().takeToCursor(count, source).build());
|
||||
|
||||
let success = await waitForResponse(bot, takeRequestId);
|
||||
bot._client.removeListener('item_stack_response', takeResponseHandler);
|
||||
|
||||
if (!success) {
|
||||
return { success: false, cursorStackId: 0 };
|
||||
}
|
||||
|
||||
// Step 2: Place from cursor
|
||||
const placeRequestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(bot, placeRequestId, actions().placeFromCursor(count, cursorStackId, destination).build());
|
||||
|
||||
success = await waitForResponse(bot, placeRequestId);
|
||||
|
||||
return { success, cursorStackId };
|
||||
}
|
||||
652
bridge/lib/mineflayer/lib/bedrock/crafting-core.mts
Normal file
652
bridge/lib/mineflayer/lib/bedrock/crafting-core.mts
Normal file
|
|
@ -0,0 +1,652 @@
|
|||
/**
|
||||
* Crafting Core - Recipe management and crafting utilities for Bedrock protocol
|
||||
*
|
||||
* Provides:
|
||||
* - Recipe parsing from crafting_data packet
|
||||
* - Recipe lookup by output item
|
||||
* - Ingredient matching (supports item_tag, complex_alias)
|
||||
* - Crafting execution with craft_recipe_auto
|
||||
*
|
||||
* Used by bedrockPlugins/craft.mts
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { BedrockBot } from '../../index.js';
|
||||
import type { protocolTypes } from '../../bedrock-types.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds } from './item-stack-actions.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
type RecipeIngredient = protocolTypes.RecipeIngredient;
|
||||
type ItemLegacy = protocolTypes.ItemLegacy;
|
||||
|
||||
/**
|
||||
* Parsed recipe structure from Bedrock crafting_data packet
|
||||
*/
|
||||
export interface BedrockRecipe {
|
||||
type: 'shaped' | 'shapeless' | 'furnace' | 'furnace_with_metadata' | 'smithing_transform' | 'smithing_trim' | 'multi' | 'shulker_box' | 'shapeless_chemistry' | 'shaped_chemistry';
|
||||
recipeId: string;
|
||||
networkId: number;
|
||||
uuid?: string;
|
||||
block: string;
|
||||
priority?: number;
|
||||
// For shaped recipes
|
||||
width?: number;
|
||||
height?: number;
|
||||
input: RecipeIngredient[] | RecipeIngredient[][];
|
||||
output: ItemLegacy[];
|
||||
// For smithing recipes
|
||||
template?: RecipeIngredient;
|
||||
base?: RecipeIngredient;
|
||||
addition?: RecipeIngredient;
|
||||
result?: ItemLegacy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prismarine-recipe compatible format
|
||||
*/
|
||||
export interface Recipe {
|
||||
result: { id: number; count: number; metadata: number };
|
||||
inShape: { id: number; metadata: number | null }[][] | null;
|
||||
ingredients: { id: number; metadata: number | null }[] | null;
|
||||
requiresTable: boolean;
|
||||
delta: { id: number; metadata: number | null; count: number }[];
|
||||
// Bedrock-specific
|
||||
networkId: number;
|
||||
bedrockRecipe: BedrockRecipe;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Slot Constants
|
||||
// ============================================================================
|
||||
|
||||
export const CraftingSlots = {
|
||||
// 2x2 player inventory crafting grid
|
||||
CRAFTING_2X2_BASE: 28, // crafting_input:28-31 for 2x2
|
||||
|
||||
// 3x3 crafting table grid
|
||||
CRAFTING_3X3_BASE: 32, // crafting_input:32-40 for 3x3
|
||||
|
||||
// Creative output slot
|
||||
CREATIVE_OUTPUT_SLOT: 50,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Recipe Parsing
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parse a recipe from the crafting_data packet
|
||||
*/
|
||||
export function parseRecipe(entry: protocolTypes.Recipes[number]): BedrockRecipe | null {
|
||||
const type = entry.type;
|
||||
const recipe = entry.recipe;
|
||||
|
||||
if (!recipe) return null;
|
||||
|
||||
switch (type) {
|
||||
case 'shaped':
|
||||
return {
|
||||
type,
|
||||
recipeId: recipe.recipe_id,
|
||||
networkId: recipe.network_id,
|
||||
uuid: recipe.uuid,
|
||||
block: recipe.block,
|
||||
priority: recipe.priority,
|
||||
width: recipe.width,
|
||||
height: recipe.height,
|
||||
input: recipe.input,
|
||||
output: recipe.output,
|
||||
};
|
||||
|
||||
case 'shapeless':
|
||||
case 'shulker_box':
|
||||
case 'shapeless_chemistry':
|
||||
return {
|
||||
type,
|
||||
recipeId: recipe.recipe_id,
|
||||
networkId: recipe.network_id,
|
||||
uuid: recipe.uuid,
|
||||
block: recipe.block,
|
||||
priority: recipe.priority,
|
||||
input: recipe.input,
|
||||
output: recipe.output,
|
||||
};
|
||||
|
||||
case 'shaped_chemistry':
|
||||
return {
|
||||
type,
|
||||
recipeId: recipe.recipe_id,
|
||||
networkId: recipe.network_id,
|
||||
uuid: recipe.uuid,
|
||||
block: recipe.block,
|
||||
priority: recipe.priority,
|
||||
width: recipe.width,
|
||||
height: recipe.height,
|
||||
input: recipe.input,
|
||||
output: recipe.output,
|
||||
};
|
||||
|
||||
case 'furnace':
|
||||
case 'furnace_with_metadata':
|
||||
// Furnace recipes have different structure
|
||||
return {
|
||||
type,
|
||||
recipeId: `furnace_${recipe.input_id}_${recipe.input_meta || 0}`,
|
||||
networkId: 0, // Furnace recipes don't have network_id
|
||||
block: recipe.block,
|
||||
input: [
|
||||
{
|
||||
type: 'int_id_meta',
|
||||
network_id: recipe.input_id,
|
||||
metadata: type === 'furnace_with_metadata' ? recipe.input_meta : 32767,
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
output: [recipe.output],
|
||||
};
|
||||
|
||||
case 'smithing_transform':
|
||||
return {
|
||||
type,
|
||||
recipeId: recipe.recipe_id,
|
||||
networkId: recipe.network_id,
|
||||
block: recipe.tag || 'smithing_table',
|
||||
template: recipe.template,
|
||||
base: recipe.base,
|
||||
addition: recipe.addition,
|
||||
result: recipe.result,
|
||||
input: [],
|
||||
output: recipe.result ? [recipe.result] : [],
|
||||
};
|
||||
|
||||
case 'smithing_trim':
|
||||
return {
|
||||
type,
|
||||
recipeId: recipe.recipe_id,
|
||||
networkId: recipe.network_id,
|
||||
block: recipe.block,
|
||||
template: recipe.template,
|
||||
input: [],
|
||||
output: [],
|
||||
};
|
||||
|
||||
case 'multi':
|
||||
// Multi recipes are special (e.g., banner patterns, firework stars)
|
||||
return {
|
||||
type,
|
||||
recipeId: `multi_${recipe.uuid}`,
|
||||
networkId: recipe.network_id,
|
||||
uuid: recipe.uuid,
|
||||
block: 'crafting_table',
|
||||
input: [],
|
||||
output: [],
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Recipe Utilities
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if a recipe can be crafted in 2x2 grid (player inventory)
|
||||
*/
|
||||
export function fitsIn2x2(recipe: BedrockRecipe): boolean {
|
||||
if (recipe.type === 'shaped' || recipe.type === 'shaped_chemistry') {
|
||||
const w = recipe.width || 0;
|
||||
const h = recipe.height || 0;
|
||||
return w <= 2 && h <= 2;
|
||||
}
|
||||
if (recipe.type === 'shapeless' || recipe.type === 'shulker_box' || recipe.type === 'shapeless_chemistry') {
|
||||
const ingredientCount = Array.isArray(recipe.input) ? (recipe.input as RecipeIngredient[]).length : 0;
|
||||
return ingredientCount <= 4;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get flattened ingredients from a recipe
|
||||
*/
|
||||
export function getIngredients(recipe: BedrockRecipe): RecipeIngredient[] {
|
||||
if (recipe.type === 'shaped' || recipe.type === 'shaped_chemistry') {
|
||||
// Flatten 2D array
|
||||
const input = recipe.input as RecipeIngredient[][];
|
||||
return input.flat();
|
||||
}
|
||||
return recipe.input as RecipeIngredient[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an ingredient to a network_id
|
||||
* Handles different ingredient types: int_id_meta, complex_alias, item_tag
|
||||
*/
|
||||
export function resolveIngredientId(ing: RecipeIngredient, registry: any): number {
|
||||
if (ing.type === 'invalid') return -1;
|
||||
|
||||
// Direct network_id
|
||||
if (ing.network_id !== undefined && ing.network_id > 0) {
|
||||
return ing.network_id;
|
||||
}
|
||||
|
||||
// complex_alias: look up by name
|
||||
if (ing.type === 'complex_alias' && (ing as any).name) {
|
||||
const name = (ing as any).name as string;
|
||||
// Remove "minecraft:" prefix and look up in registry
|
||||
const shortName = name.replace('minecraft:', '');
|
||||
const item = registry.itemsByName[shortName];
|
||||
if (item) return item.id;
|
||||
}
|
||||
|
||||
// item_tag: best-effort resolution based on tag name
|
||||
// Common tags: minecraft:coals → coal
|
||||
if (ing.type === 'item_tag' && (ing as any).tag) {
|
||||
const tag = (ing as any).tag as string;
|
||||
// Try to extract item name from tag (e.g., "minecraft:coals" → "coal")
|
||||
const tagName = tag.replace('minecraft:', '').replace(/s$/, ''); // Remove trailing 's'
|
||||
const item = registry.itemsByName[tagName];
|
||||
if (item) return item.id;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an inventory item matches an ingredient
|
||||
* Handles different ingredient types including item_tag
|
||||
*/
|
||||
export function itemMatchesIngredient(item: Item, ing: RecipeIngredient): boolean {
|
||||
if (ing.type === 'invalid') return false;
|
||||
|
||||
// Direct network_id match
|
||||
if (ing.network_id !== undefined && ing.network_id > 0) {
|
||||
if (item.type !== ing.network_id) return false;
|
||||
// Check metadata if specified (32767 = wildcard)
|
||||
if (ing.metadata !== undefined && ing.metadata !== 32767 && item.metadata !== ing.metadata) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// complex_alias: match by name
|
||||
if (ing.type === 'complex_alias' && (ing as any).name) {
|
||||
const name = (ing as any).name as string;
|
||||
const shortName = name.replace('minecraft:', '');
|
||||
return item.name === shortName;
|
||||
}
|
||||
|
||||
// item_tag: check if item has the tag
|
||||
// For now, use simple name matching (e.g., "minecraft:coals" matches "coal", "charcoal")
|
||||
if (ing.type === 'item_tag' && (ing as any).tag) {
|
||||
const tag = (ing as any).tag as string;
|
||||
const tagName = tag.replace('minecraft:', '');
|
||||
// Check common tag patterns
|
||||
if (tagName === 'coals') {
|
||||
return item.name === 'coal' || item.name === 'charcoal';
|
||||
}
|
||||
if (tagName === 'logs' || tagName === 'oak_logs') {
|
||||
return item.name?.endsWith('_log') || item.name?.endsWith('_wood');
|
||||
}
|
||||
if (tagName === 'planks') {
|
||||
return item.name?.endsWith('_planks');
|
||||
}
|
||||
// Generic: try singular form
|
||||
const singular = tagName.replace(/s$/, '');
|
||||
return item.name === singular || item.name === tagName;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count items in inventory that match an ingredient
|
||||
*/
|
||||
export function countMatchingItems(bot: BedrockBot, ing: RecipeIngredient): number {
|
||||
let count = 0;
|
||||
for (const item of bot.inventory.slots) {
|
||||
if (!item) continue;
|
||||
if (itemMatchesIngredient(item, ing)) {
|
||||
count += item.count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find inventory slots containing items that match a recipe ingredient
|
||||
*/
|
||||
export function findIngredientSlots(bot: BedrockBot, ingredient: RecipeIngredient): number[] {
|
||||
const slots: number[] = [];
|
||||
|
||||
for (let i = 0; i < bot.inventory.slots.length; i++) {
|
||||
const item = bot.inventory.slots[i];
|
||||
if (!item) continue;
|
||||
|
||||
if (itemMatchesIngredient(item, ingredient)) {
|
||||
slots.push(i);
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Recipe Conversion
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Convert Bedrock recipe to prismarine-recipe compatible format
|
||||
*/
|
||||
export function convertToRecipe(bedrock: BedrockRecipe, registry: any): Recipe {
|
||||
const output = bedrock.output[0] || { network_id: 0, count: 1, metadata: 0 };
|
||||
|
||||
// Determine if recipe requires crafting table
|
||||
const requiresTable = !fitsIn2x2(bedrock) || (bedrock.block !== 'deprecated' && !fitsIn2x2(bedrock));
|
||||
|
||||
// Build inShape for shaped recipes
|
||||
let inShape: { id: number; metadata: number | null }[][] | null = null;
|
||||
let ingredients: { id: number; metadata: number | null }[] | null = null;
|
||||
|
||||
if (bedrock.type === 'shaped' || bedrock.type === 'shaped_chemistry') {
|
||||
const input = bedrock.input as RecipeIngredient[][];
|
||||
inShape = input.map((row) =>
|
||||
row.map((ing) => ({
|
||||
id: ing.type === 'invalid' ? -1 : ing.network_id || 0,
|
||||
metadata: ing.metadata === 32767 ? null : ing.metadata || null,
|
||||
}))
|
||||
);
|
||||
} else if (bedrock.type === 'shapeless' || bedrock.type === 'shulker_box' || bedrock.type === 'shapeless_chemistry') {
|
||||
const input = bedrock.input as RecipeIngredient[];
|
||||
ingredients = input.map((ing) => ({
|
||||
id: ing.network_id || 0,
|
||||
metadata: ing.metadata === 32767 ? null : ing.metadata || null,
|
||||
}));
|
||||
}
|
||||
|
||||
// Compute delta (inventory change)
|
||||
const delta: { id: number; metadata: number | null; count: number }[] = [];
|
||||
|
||||
// Add consumed ingredients (negative)
|
||||
for (const ing of getIngredients(bedrock)) {
|
||||
if (ing.type === 'invalid') continue;
|
||||
const id = resolveIngredientId(ing, registry);
|
||||
const metadata = ing.metadata === 32767 ? null : ing.metadata || null;
|
||||
const existing = delta.find((d) => d.id === id && d.metadata === metadata);
|
||||
if (existing) {
|
||||
existing.count -= ing.count;
|
||||
} else {
|
||||
delta.push({ id, metadata, count: -ing.count });
|
||||
}
|
||||
}
|
||||
|
||||
// Add produced output (positive)
|
||||
for (const out of bedrock.output) {
|
||||
const id = out.network_id;
|
||||
const metadata = out.metadata || null;
|
||||
const existing = delta.find((d) => d.id === id && d.metadata === metadata);
|
||||
if (existing) {
|
||||
existing.count += out.count;
|
||||
} else {
|
||||
delta.push({ id, metadata, count: out.count });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
result: {
|
||||
id: output.network_id,
|
||||
count: output.count,
|
||||
metadata: output.metadata || 0,
|
||||
},
|
||||
inShape,
|
||||
ingredients,
|
||||
requiresTable,
|
||||
delta,
|
||||
networkId: bedrock.networkId,
|
||||
bedrockRecipe: bedrock,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Recipe Lookup
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Find all recipes that produce a given item
|
||||
*/
|
||||
export function findRecipesByOutput(recipesByOutputId: Map<number, BedrockRecipe[]>, registry: any, itemType: number, metadata: number | null): Recipe[] {
|
||||
const bedrock = recipesByOutputId.get(itemType) || [];
|
||||
return bedrock
|
||||
.filter((r) => {
|
||||
// Filter by metadata if specified
|
||||
if (metadata !== null) {
|
||||
const output = r.output[0];
|
||||
if (output && output.metadata !== metadata && output.metadata !== 32767) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((r) => convertToRecipe(r, registry));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player has enough items for a recipe
|
||||
*/
|
||||
export function hasIngredientsFor(bot: BedrockBot, recipe: Recipe, count: number = 1): boolean {
|
||||
const bedrock = recipe.bedrockRecipe;
|
||||
const ingredients = getIngredients(bedrock);
|
||||
|
||||
// Track how many of each ingredient slot we need
|
||||
const needed = new Map<string, { ing: RecipeIngredient; count: number }>();
|
||||
|
||||
for (const ing of ingredients) {
|
||||
if (ing.type === 'invalid') continue;
|
||||
|
||||
// Create a key for this ingredient type
|
||||
const key = JSON.stringify({ type: ing.type, network_id: ing.network_id, tag: (ing as any).tag, name: (ing as any).name });
|
||||
const existing = needed.get(key);
|
||||
if (existing) {
|
||||
existing.count += ing.count * count;
|
||||
} else {
|
||||
needed.set(key, { ing, count: ing.count * count });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have enough of each ingredient
|
||||
for (const [, { ing, count: neededCount }] of needed) {
|
||||
const available = countMatchingItems(bot, ing);
|
||||
if (available < neededCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Crafting Execution
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Craft using craft_recipe_auto action
|
||||
* This tells the server to automatically source ingredients.
|
||||
*
|
||||
* Based on real client packet captures, the format is:
|
||||
* 1. craft_recipe_auto with recipe_network_id, times_crafted, times_crafted_2, ingredients
|
||||
* 2. results_deprecated with result_items and times_crafted
|
||||
* 3. consume actions from hotbar_and_inventory for each ingredient slot
|
||||
* 4. place action from creative_output:50 to hotbar_and_inventory
|
||||
*/
|
||||
export async function craftWithAuto(
|
||||
bot: BedrockBot,
|
||||
bedrock: BedrockRecipe,
|
||||
Item: any // Item constructor from prismarine-item
|
||||
): Promise<void> {
|
||||
const requestId = getNextItemStackRequestId();
|
||||
const outputCount = bedrock.output[0]?.count ?? 1;
|
||||
const ingredients = getIngredients(bedrock).filter((ing) => ing.type !== 'invalid');
|
||||
|
||||
// Build result_items for results_deprecated
|
||||
const resultItems = bedrock.output.map((output) => ({
|
||||
network_id: output.network_id,
|
||||
count: output.count,
|
||||
metadata: output.metadata ?? 0,
|
||||
block_runtime_id: output.block_runtime_id ?? 0,
|
||||
extra: output.extra ?? { has_nbt: 0, can_place_on: [], can_destroy: [] },
|
||||
}));
|
||||
|
||||
// Find inventory slots with ingredients and build consume actions
|
||||
const consumeActions: any[] = [];
|
||||
const ingredientCounts = new Map<number, { slot: number; stackId: number; count: number }>();
|
||||
const resolvedIngredients: RecipeIngredient[] = [];
|
||||
|
||||
for (const ing of ingredients) {
|
||||
if (ing.type === 'invalid') continue;
|
||||
|
||||
// Find a slot with this ingredient using itemMatchesIngredient
|
||||
for (let slot = 0; slot < bot.inventory.slots.length; slot++) {
|
||||
const item = bot.inventory.slots[slot];
|
||||
if (!item) continue;
|
||||
if (!itemMatchesIngredient(item, ing)) continue;
|
||||
|
||||
const key = slot;
|
||||
const existing = ingredientCounts.get(key);
|
||||
if (existing) {
|
||||
existing.count += ing.count;
|
||||
} else {
|
||||
ingredientCounts.set(key, {
|
||||
slot,
|
||||
stackId: getStackId(item),
|
||||
count: ing.count,
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve item_tag/complex_alias to int_id_meta using the actual item
|
||||
if (ing.type === 'item_tag' || ing.type === 'complex_alias') {
|
||||
resolvedIngredients.push({
|
||||
type: 'int_id_meta',
|
||||
network_id: item.type,
|
||||
metadata: item.metadata ?? 32767,
|
||||
count: ing.count,
|
||||
} as RecipeIngredient);
|
||||
} else {
|
||||
resolvedIngredients.push(ing);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build consume actions
|
||||
let firstConsume = true;
|
||||
for (const [, info] of ingredientCounts) {
|
||||
consumeActions.push({
|
||||
type_id: 'consume',
|
||||
count: info.count,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.HOTBAR_AND_INVENTORY },
|
||||
slot: info.slot,
|
||||
stack_id: firstConsume ? info.stackId : requestId,
|
||||
},
|
||||
});
|
||||
firstConsume = false;
|
||||
}
|
||||
|
||||
// Find an empty slot for the output
|
||||
let outputSlot = -1;
|
||||
for (let slot = 0; slot < bot.inventory.slots.length; slot++) {
|
||||
if (!bot.inventory.slots[slot]) {
|
||||
outputSlot = slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (outputSlot === -1) {
|
||||
throw new Error('No empty inventory slot for crafting output');
|
||||
}
|
||||
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'craft_recipe_auto',
|
||||
recipe_network_id: bedrock.networkId,
|
||||
times_crafted_2: 1,
|
||||
times_crafted: 1,
|
||||
ingredients: resolvedIngredients,
|
||||
},
|
||||
{
|
||||
type_id: 'results_deprecated',
|
||||
result_items: resultItems,
|
||||
times_crafted: 1,
|
||||
},
|
||||
...consumeActions,
|
||||
{
|
||||
type_id: 'place',
|
||||
count: outputCount,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId,
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.HOTBAR_AND_INVENTORY },
|
||||
slot: outputSlot,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot.logger.info(`Sending craft_recipe_auto: id=${requestId}, recipe=${bedrock.networkId}`);
|
||||
bot.logger.debug(` original ingredients: ${JSON.stringify(ingredients)}`);
|
||||
bot.logger.debug(` resolved ingredients: ${JSON.stringify(resolvedIngredients)}`);
|
||||
bot.logger.debug(` actions: ${JSON.stringify(actionList)}`);
|
||||
|
||||
sendRequest(bot, requestId, actionList);
|
||||
|
||||
const success = await waitForResponse(bot, requestId);
|
||||
if (!success) {
|
||||
throw new Error('Crafting failed - server rejected craft_recipe_auto request');
|
||||
}
|
||||
|
||||
// Create the output item in the destination slot
|
||||
const output = bedrock.output[0];
|
||||
if (output) {
|
||||
const newItem = new Item(output.network_id, outputCount, output.metadata ?? 0);
|
||||
(newItem as any).stackId = requestId;
|
||||
bot.inventory.updateSlot(outputSlot, newItem);
|
||||
bot.logger.debug(`Created crafted item in slot ${outputSlot}: ${output.network_id} x${outputCount}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an item in all inventory slots by type or name
|
||||
*/
|
||||
export function findItemInAllSlots(bot: BedrockBot, itemTypeOrName: number | string, metadata: number | null): Item | null {
|
||||
for (let i = 0; i < bot.inventory.slots.length; i++) {
|
||||
const item = bot.inventory.slots[i];
|
||||
if (!item) continue;
|
||||
|
||||
const matches = typeof itemTypeOrName === 'number' ? item.type === itemTypeOrName : item.name === itemTypeOrName;
|
||||
|
||||
if (matches && (metadata === null || item.metadata === metadata)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count items across all inventory slots
|
||||
*/
|
||||
export function countAllItems(bot: BedrockBot, itemType: number, metadata: number | null): number {
|
||||
let sum = 0;
|
||||
for (const item of bot.inventory.slots) {
|
||||
if (item && item.type === itemType && (metadata === null || metadata === 32767 || item.metadata === metadata)) {
|
||||
sum += item.count;
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
157
bridge/lib/mineflayer/lib/bedrock/index.mts
Normal file
157
bridge/lib/mineflayer/lib/bedrock/index.mts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* Bedrock Implementation Core
|
||||
*
|
||||
* This module provides reusable utilities for Bedrock-specific implementations:
|
||||
* - Action builders for item_stack_request protocol
|
||||
* - Slot mapping between Bedrock and prismarine-windows
|
||||
* - Container transfer operations
|
||||
*
|
||||
* These utilities are used by the bedrockPlugins but can also be used
|
||||
* by external plugins that need to interact with Bedrock inventory.
|
||||
*/
|
||||
|
||||
// Item Stack Actions - Core action building utilities
|
||||
export {
|
||||
// Stack ID helpers
|
||||
getStackId,
|
||||
setStackId,
|
||||
|
||||
// Container ID constants
|
||||
ContainerIds,
|
||||
type ContainerId,
|
||||
|
||||
// Slot location types and helpers
|
||||
type SlotLocation,
|
||||
cursor,
|
||||
slot,
|
||||
containerSlot,
|
||||
inventorySlot,
|
||||
fromPlayerSlot,
|
||||
fromItem,
|
||||
|
||||
// Action builder
|
||||
ActionBuilder,
|
||||
actions,
|
||||
|
||||
// Request ID management
|
||||
getNextItemStackRequestId,
|
||||
getNextLegacyRequestId,
|
||||
resetRequestIds,
|
||||
|
||||
// Request execution
|
||||
type ItemStackResult,
|
||||
executeRequest,
|
||||
sendRequest,
|
||||
waitForResponse,
|
||||
captureCursorStackId,
|
||||
|
||||
// Convenience builders
|
||||
buildTakeRequest,
|
||||
buildPlaceRequest,
|
||||
buildSwapRequest,
|
||||
buildDropRequest,
|
||||
} from './item-stack-actions.mts';
|
||||
|
||||
// Slot Mapping - Container ID and slot index mapping
|
||||
export {
|
||||
SlotRanges,
|
||||
getSlotIndex,
|
||||
getWindow,
|
||||
type ContainerLocation,
|
||||
getContainerForCursorOp,
|
||||
getContainerFromSlot,
|
||||
slotToLocation,
|
||||
isHotbarSlot,
|
||||
isInventorySlot,
|
||||
isArmorSlot,
|
||||
isOffhandSlot,
|
||||
getArmorType,
|
||||
getArmorSlot,
|
||||
} from './slot-mapping.mts';
|
||||
|
||||
// Container Operations - Transfer utilities
|
||||
export { type TransferConfig, transferItems, depositToContainer, withdrawFromContainer, twoStepTransfer } from './container.mts';
|
||||
|
||||
// Crafting Core - Recipe management and crafting
|
||||
export {
|
||||
// Types
|
||||
type BedrockRecipe,
|
||||
type Recipe,
|
||||
|
||||
// Slot constants
|
||||
CraftingSlots,
|
||||
|
||||
// Recipe parsing
|
||||
parseRecipe,
|
||||
|
||||
// Recipe utilities
|
||||
fitsIn2x2,
|
||||
getIngredients,
|
||||
resolveIngredientId,
|
||||
itemMatchesIngredient,
|
||||
countMatchingItems,
|
||||
findIngredientSlots,
|
||||
|
||||
// Recipe conversion and lookup
|
||||
convertToRecipe,
|
||||
findRecipesByOutput,
|
||||
hasIngredientsFor,
|
||||
|
||||
// Crafting execution
|
||||
craftWithAuto,
|
||||
|
||||
// Item helpers
|
||||
findItemInAllSlots,
|
||||
countAllItems,
|
||||
} from './crafting-core.mts';
|
||||
|
||||
// Workstations - Specialized container interfaces
|
||||
export {
|
||||
// Furnace
|
||||
openFurnace,
|
||||
FurnaceSlots,
|
||||
type Furnace,
|
||||
|
||||
// Anvil
|
||||
openAnvil,
|
||||
AnvilSlots,
|
||||
type Anvil,
|
||||
|
||||
// Enchanting
|
||||
openEnchantmentTable,
|
||||
EnchantingSlots,
|
||||
type EnchantmentTable,
|
||||
|
||||
// Smithing
|
||||
openSmithingTable,
|
||||
SmithingSlots,
|
||||
type SmithingTable,
|
||||
|
||||
// Stonecutter
|
||||
openStonecutter,
|
||||
StonecutterSlots,
|
||||
type Stonecutter,
|
||||
|
||||
// Grindstone
|
||||
openGrindstone,
|
||||
GrindstoneSlots,
|
||||
type Grindstone,
|
||||
|
||||
// Loom
|
||||
openLoom,
|
||||
LoomSlots,
|
||||
type Loom,
|
||||
|
||||
// Brewing Stand
|
||||
openBrewingStand,
|
||||
BrewingSlots,
|
||||
type BrewingStand,
|
||||
|
||||
// Cartography Table
|
||||
openCartographyTable,
|
||||
CartographySlots,
|
||||
type CartographyTable,
|
||||
} from './workstations/index.mts';
|
||||
|
||||
// Window Types - Bedrock slot mappings are now handled by prismarine-windows patch
|
||||
// See patches/prismarine-windows+2.9.0.patch for the Bedrock window definitions
|
||||
624
bridge/lib/mineflayer/lib/bedrock/item-stack-actions.mts
Normal file
624
bridge/lib/mineflayer/lib/bedrock/item-stack-actions.mts
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
/**
|
||||
* Item Stack Actions - Core action building utilities for Bedrock item_stack_request protocol
|
||||
*
|
||||
* This module provides:
|
||||
* - Stack ID helpers (replaces 50+ occurrences of `(item as any).stackId ?? 0`)
|
||||
* - Container ID constants
|
||||
* - SlotLocation type and helpers
|
||||
* - ActionBuilder class with fluent API
|
||||
* - Request ID management
|
||||
* - Request execution and response waiting
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { BedrockBot } from '../../index.js';
|
||||
import type { protocolTypes } from '../../bedrock-types.js';
|
||||
|
||||
// ============================================================================
|
||||
// Stack ID Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get stack ID from an item safely.
|
||||
* Replaces pattern: `(item as any).stackId ?? 0`
|
||||
*/
|
||||
export function getStackId(item: Item | null | undefined): number {
|
||||
return (item as any)?.stackId ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set stack ID on an item.
|
||||
* Replaces pattern: `(item as any).stackId = value`
|
||||
*/
|
||||
export function setStackId(item: Item, stackId: number): void {
|
||||
(item as any).stackId = stackId;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Container ID Constants
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Container IDs used in Bedrock item_stack_request protocol
|
||||
*/
|
||||
export const ContainerIds = {
|
||||
// Cursor
|
||||
CURSOR: 'cursor',
|
||||
|
||||
// Player inventory sections
|
||||
HOTBAR: 'hotbar',
|
||||
INVENTORY: 'inventory',
|
||||
HOTBAR_AND_INVENTORY: 'hotbar_and_inventory',
|
||||
ARMOR: 'armor',
|
||||
OFFHAND: 'offhand',
|
||||
|
||||
// External containers
|
||||
CONTAINER: 'container',
|
||||
|
||||
// Crafting
|
||||
CRAFTING_INPUT: 'crafting_input',
|
||||
CREATIVE_OUTPUT: 'creative_output',
|
||||
CREATED_OUTPUT: 'created_output',
|
||||
|
||||
// Furnace
|
||||
FURNACE_INGREDIENT: 'furnace_ingredient',
|
||||
FURNACE_FUEL: 'furnace_fuel',
|
||||
FURNACE_OUTPUT: 'furnace_output',
|
||||
|
||||
// Enchanting
|
||||
ENCHANTING_INPUT: 'enchanting_input',
|
||||
ENCHANTING_LAPIS: 'enchanting_lapis',
|
||||
|
||||
// Anvil
|
||||
ANVIL_INPUT: 'anvil_input',
|
||||
ANVIL_MATERIAL: 'anvil_material',
|
||||
|
||||
// Stonecutter
|
||||
STONECUTTER_INPUT: 'stonecutter_input',
|
||||
|
||||
// Smithing Table
|
||||
SMITHING_TABLE_TEMPLATE: 'smithing_table_template',
|
||||
SMITHING_TABLE_INPUT: 'smithing_table_input',
|
||||
SMITHING_TABLE_MATERIAL: 'smithing_table_material',
|
||||
|
||||
// Brewing Stand
|
||||
BREWING_INPUT: 'brewing_input',
|
||||
BREWING_FUEL: 'brewing_fuel',
|
||||
BREWING_RESULT: 'brewing_result',
|
||||
|
||||
// Grindstone
|
||||
GRINDSTONE_INPUT: 'grindstone_input',
|
||||
|
||||
// Loom
|
||||
LOOM_INPUT: 'loom_input',
|
||||
LOOM_DYE: 'loom_dye',
|
||||
|
||||
// Cartography Table
|
||||
CARTOGRAPHY_INPUT: 'cartography_input',
|
||||
CARTOGRAPHY_ADDITIONAL: 'cartography_additional',
|
||||
} as const;
|
||||
|
||||
export type ContainerId = (typeof ContainerIds)[keyof typeof ContainerIds];
|
||||
|
||||
// ============================================================================
|
||||
// Slot Location Type and Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Represents a slot location for item_stack_request actions
|
||||
*/
|
||||
export interface SlotLocation {
|
||||
containerId: string;
|
||||
slot: number;
|
||||
stackId: number;
|
||||
dynamicContainerId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cursor slot location
|
||||
*/
|
||||
export function cursor(stackId: number = 0): SlotLocation {
|
||||
return { containerId: ContainerIds.CURSOR, slot: 0, stackId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a slot location from container ID and slot index
|
||||
*/
|
||||
export function slot(containerId: string, slotIndex: number, stackId: number = 0, dynamicContainerId?: number): SlotLocation {
|
||||
return { containerId, slot: slotIndex, stackId, dynamicContainerId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a container slot location (for chests, etc.)
|
||||
*/
|
||||
export function containerSlot(slotIndex: number, stackId: number = 0): SlotLocation {
|
||||
return { containerId: ContainerIds.CONTAINER, slot: slotIndex, stackId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hotbar_and_inventory slot location
|
||||
*/
|
||||
export function inventorySlot(slotIndex: number, stackId: number = 0): SlotLocation {
|
||||
return { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SlotLocation from a player inventory slot index
|
||||
* Maps to correct container based on slot range:
|
||||
* - 0-8: hotbar
|
||||
* - 9-35: inventory
|
||||
* - 36-39: armor
|
||||
* - 45: offhand
|
||||
*/
|
||||
export function fromPlayerSlot(slotIndex: number, item?: Item | null): SlotLocation {
|
||||
const stackId = getStackId(item);
|
||||
|
||||
if (slotIndex >= 0 && slotIndex <= 8) {
|
||||
return { containerId: ContainerIds.HOTBAR, slot: slotIndex, stackId };
|
||||
} else if (slotIndex >= 9 && slotIndex <= 35) {
|
||||
return { containerId: ContainerIds.INVENTORY, slot: slotIndex, stackId };
|
||||
} else if (slotIndex >= 36 && slotIndex <= 39) {
|
||||
return { containerId: ContainerIds.ARMOR, slot: slotIndex - 36, stackId };
|
||||
} else if (slotIndex === 45) {
|
||||
return { containerId: ContainerIds.OFFHAND, slot: 1, stackId };
|
||||
} else {
|
||||
throw new Error(`Invalid player inventory slot index: ${slotIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SlotLocation from an item (uses item.slot)
|
||||
*/
|
||||
export function fromItem(containerId: string, item: Item): SlotLocation {
|
||||
return {
|
||||
containerId,
|
||||
slot: item.slot,
|
||||
stackId: getStackId(item),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Action Types
|
||||
// ============================================================================
|
||||
|
||||
type ItemStackAction = protocolTypes.ItemStackRequest['actions'][number];
|
||||
type StackRequestSlotInfo = protocolTypes.StackRequestSlotInfo;
|
||||
type RecipeIngredient = protocolTypes.RecipeIngredient;
|
||||
type ItemLegacy = protocolTypes.ItemLegacy;
|
||||
|
||||
/**
|
||||
* Convert SlotLocation to StackRequestSlotInfo format
|
||||
*/
|
||||
function toSlotInfo(loc: SlotLocation): StackRequestSlotInfo {
|
||||
const slotType: { container_id: string; dynamic_container_id?: number } = {
|
||||
container_id: loc.containerId,
|
||||
};
|
||||
if (loc.dynamicContainerId !== undefined) {
|
||||
slotType.dynamic_container_id = loc.dynamicContainerId;
|
||||
}
|
||||
return {
|
||||
slot_type: slotType as any,
|
||||
slot: loc.slot,
|
||||
stack_id: loc.stackId,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Action Builder Class
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fluent builder for item_stack_request actions
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const actionList = actions()
|
||||
* .takeToCursor(count, source)
|
||||
* .placeFromCursor(count, cursorStackId, dest)
|
||||
* .build();
|
||||
* ```
|
||||
*/
|
||||
export class ActionBuilder {
|
||||
private actionList: ItemStackAction[] = [];
|
||||
|
||||
/**
|
||||
* Take items from source to destination
|
||||
*/
|
||||
take(count: number, source: SlotLocation, destination: SlotLocation): this {
|
||||
this.actionList.push({
|
||||
type_id: 'take',
|
||||
count,
|
||||
source: toSlotInfo(source),
|
||||
destination: toSlotInfo(destination),
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take items from source to cursor
|
||||
*/
|
||||
takeToCursor(count: number, source: SlotLocation, cursorStackId: number = 0): this {
|
||||
return this.take(count, source, cursor(cursorStackId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Place items from source to destination
|
||||
*/
|
||||
place(count: number, source: SlotLocation, destination: SlotLocation): this {
|
||||
this.actionList.push({
|
||||
type_id: 'place',
|
||||
count,
|
||||
source: toSlotInfo(source),
|
||||
destination: toSlotInfo(destination),
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Place items from cursor to destination
|
||||
*/
|
||||
placeFromCursor(count: number, cursorStackId: number, destination: SlotLocation): this {
|
||||
return this.place(count, cursor(cursorStackId), destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap items between source and destination
|
||||
*/
|
||||
swap(source: SlotLocation, destination: SlotLocation): this {
|
||||
this.actionList.push({
|
||||
type_id: 'swap',
|
||||
source: toSlotInfo(source),
|
||||
destination: toSlotInfo(destination),
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop items from source
|
||||
*/
|
||||
drop(count: number, source: SlotLocation, randomly: boolean = false): this {
|
||||
this.actionList.push({
|
||||
type_id: 'drop',
|
||||
count,
|
||||
source: toSlotInfo(source),
|
||||
randomly,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume items from source (used in crafting)
|
||||
*/
|
||||
consume(count: number, source: SlotLocation): this {
|
||||
this.actionList.push({
|
||||
type_id: 'consume',
|
||||
count,
|
||||
source: toSlotInfo(source),
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy items from source (creative mode)
|
||||
*/
|
||||
destroy(count: number, source: SlotLocation): this {
|
||||
this.actionList.push({
|
||||
type_id: 'destroy',
|
||||
count,
|
||||
source: toSlotInfo(source),
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create item (creative mode)
|
||||
*/
|
||||
create(resultSlotId: number = 0): this {
|
||||
this.actionList.push({
|
||||
type_id: 'create',
|
||||
result_slot_id: resultSlotId,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Craft creative action (pick item from creative inventory)
|
||||
* Used with results_deprecated and take actions
|
||||
* @param itemId - The entry_id from creative_content packet
|
||||
* @param timesCrafted - How many times to craft (default 1)
|
||||
*/
|
||||
craftCreative(itemId: number, timesCrafted: number = 1): this {
|
||||
this.actionList.push({
|
||||
type_id: 'craft_creative',
|
||||
item_id: itemId,
|
||||
times_crafted: timesCrafted,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Craft recipe action
|
||||
*/
|
||||
craftRecipe(recipeNetworkId: number, timesCrafted: number = 1): this {
|
||||
this.actionList.push({
|
||||
type_id: 'craft_recipe',
|
||||
recipe_network_id: recipeNetworkId,
|
||||
times_crafted: timesCrafted,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Craft recipe auto action (shift-click crafting / auto ingredient sourcing)
|
||||
*/
|
||||
craftRecipeAuto(recipeNetworkId: number, timesCrafted: number = 1, ingredients?: RecipeIngredient[]): this {
|
||||
this.actionList.push({
|
||||
type_id: 'craft_recipe_auto',
|
||||
recipe_network_id: recipeNetworkId,
|
||||
times_crafted: timesCrafted,
|
||||
times_crafted_2: timesCrafted,
|
||||
ingredients,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Results deprecated action (required for crafting to declare expected outputs)
|
||||
*/
|
||||
resultsDeprecated(resultItems: ItemLegacy[], timesCrafted: number = 1): this {
|
||||
this.actionList.push({
|
||||
type_id: 'results_deprecated',
|
||||
result_items: resultItems,
|
||||
times_crafted: timesCrafted,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional action (anvil renaming, cartography)
|
||||
*/
|
||||
optional(filteredStringIndex: number = 0): this {
|
||||
this.actionList.push({
|
||||
type_id: 'optional',
|
||||
filtered_string_index: filteredStringIndex,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Craft loom request action (banner patterns)
|
||||
* Note: Loom does NOT use recipeNetworkId
|
||||
*/
|
||||
craftLoomRequest(timesCrafted: number = 1): this {
|
||||
this.actionList.push({
|
||||
type_id: 'craft_loom_request',
|
||||
times_crafted: timesCrafted,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Craft grindstone request action (disenchanting)
|
||||
*/
|
||||
craftGrindstoneRequest(recipeNetworkId: number, timesCrafted: number = 1): this {
|
||||
this.actionList.push({
|
||||
type_id: 'craft_grindstone_request',
|
||||
recipe_network_id: recipeNetworkId,
|
||||
times_crafted: timesCrafted,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mine block action (tool durability)
|
||||
*/
|
||||
mineBlock(hotbarSlot: number, predictedDurability: number, networkId: number): this {
|
||||
this.actionList.push({
|
||||
type_id: 'mine_block',
|
||||
hotbar_slot: hotbarSlot,
|
||||
predicted_durability: predictedDurability,
|
||||
network_id: networkId,
|
||||
} as any);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the built actions array
|
||||
*/
|
||||
build(): ItemStackAction[] {
|
||||
return this.actionList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the builder for reuse
|
||||
*/
|
||||
reset(): this {
|
||||
this.actionList = [];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current action count
|
||||
*/
|
||||
get length(): number {
|
||||
return this.actionList.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new action builder
|
||||
*/
|
||||
export function actions(): ActionBuilder {
|
||||
return new ActionBuilder();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Request ID Management
|
||||
// ============================================================================
|
||||
|
||||
let nextRequestId = -861;
|
||||
let nextLegacyRequestId = -1;
|
||||
|
||||
/**
|
||||
* Get next item_stack_request ID (shared across all plugins)
|
||||
* Uses negative IDs decremented by 2
|
||||
*/
|
||||
export function getNextItemStackRequestId(): number {
|
||||
nextRequestId -= 2;
|
||||
return nextRequestId + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next legacy_request_id for inventory_transaction packets
|
||||
*/
|
||||
export function getNextLegacyRequestId(): number {
|
||||
return nextLegacyRequestId--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset request IDs (useful for testing)
|
||||
*/
|
||||
export function resetRequestIds(): void {
|
||||
nextRequestId = -861;
|
||||
nextLegacyRequestId = -1;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Request Execution
|
||||
// ============================================================================
|
||||
|
||||
export interface ItemStackResult {
|
||||
success: boolean;
|
||||
requestId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an item_stack_request and await response
|
||||
*/
|
||||
export async function executeRequest(bot: BedrockBot, actionList: ItemStackAction[], customNames: string[] = [], cause: number = -1, timeout: number = 5000): Promise<ItemStackResult> {
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
bot._client.write('item_stack_request', {
|
||||
requests: [
|
||||
{
|
||||
request_id: requestId,
|
||||
actions: actionList,
|
||||
custom_names: customNames,
|
||||
cause,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const success = await waitForResponse(bot, requestId, timeout);
|
||||
return { success, requestId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an item_stack_request with a specific request ID
|
||||
*/
|
||||
export function sendRequest(bot: BedrockBot, requestId: number, actionList: ItemStackAction[], customNames: string[] = [], cause: number = -1): void {
|
||||
bot._client.write('item_stack_request', {
|
||||
requests: [
|
||||
{
|
||||
request_id: requestId,
|
||||
actions: actionList,
|
||||
custom_names: customNames,
|
||||
cause,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for item_stack_response with given request ID
|
||||
*/
|
||||
export function waitForResponse(bot: BedrockBot, requestId: number, timeout: number = 5000): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
bot.removeListener(`itemStackResponse:${requestId}`, handler);
|
||||
resolve(false);
|
||||
}, timeout);
|
||||
|
||||
const handler = async (success: boolean) => {
|
||||
clearTimeout(timer);
|
||||
// Small delay to allow inventory updates to process
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
resolve(success);
|
||||
};
|
||||
|
||||
bot.once(`itemStackResponse:${requestId}`, handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture cursor stack ID from item_stack_response
|
||||
* Used for chained requests where cursor stack ID changes
|
||||
*/
|
||||
export function captureCursorStackId(bot: BedrockBot, requestId: number, callback: (stackId: number) => void): () => void {
|
||||
const handler = (packet: protocolTypes.packet_item_stack_response) => {
|
||||
for (const response of packet.responses) {
|
||||
if (response.request_id === requestId && response.status === 'ok') {
|
||||
for (const container of response.containers || []) {
|
||||
if (container.slot_type?.container_id === 'cursor' && container.slots?.length > 0) {
|
||||
callback(container.slots[0].item_stack_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bot._client.on('item_stack_response', handler);
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
bot._client.removeListener('item_stack_response', handler);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Convenience Functions for Common Patterns
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Build a complete take-to-cursor request
|
||||
*/
|
||||
export function buildTakeRequest(source: SlotLocation, count: number, cursorStackId: number = 0): { actions: ItemStackAction[]; customNames: string[]; cause: number } {
|
||||
return {
|
||||
actions: actions().takeToCursor(count, source, cursorStackId).build(),
|
||||
customNames: [],
|
||||
cause: -1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a complete place-from-cursor request
|
||||
*/
|
||||
export function buildPlaceRequest(destination: SlotLocation, count: number, cursorStackId: number): { actions: ItemStackAction[]; customNames: string[]; cause: number } {
|
||||
return {
|
||||
actions: actions().placeFromCursor(count, cursorStackId, destination).build(),
|
||||
customNames: [],
|
||||
cause: -1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a swap request
|
||||
*/
|
||||
export function buildSwapRequest(source: SlotLocation, destination: SlotLocation): { actions: ItemStackAction[]; customNames: string[]; cause: number } {
|
||||
return {
|
||||
actions: actions().swap(source, destination).build(),
|
||||
customNames: [],
|
||||
cause: -1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a drop request
|
||||
*/
|
||||
export function buildDropRequest(source: SlotLocation, count: number, randomly: boolean = false): { actions: ItemStackAction[]; customNames: string[]; cause: number } {
|
||||
return {
|
||||
actions: actions().drop(count, source, randomly).build(),
|
||||
customNames: [],
|
||||
cause: -1,
|
||||
};
|
||||
}
|
||||
235
bridge/lib/mineflayer/lib/bedrock/slot-mapping.mts
Normal file
235
bridge/lib/mineflayer/lib/bedrock/slot-mapping.mts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* Slot Mapping - Container ID and slot index mapping utilities for Bedrock protocol
|
||||
*
|
||||
* Handles the translation between:
|
||||
* - Bedrock window_id strings ("inventory", "armor", "hotbar", etc.)
|
||||
* - Prismarine-windows slot indices (0-45 for player inventory)
|
||||
* - Bedrock container IDs for item_stack_request actions
|
||||
*/
|
||||
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../index.js';
|
||||
import { ContainerIds, type SlotLocation } from './item-stack-actions.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Slot Index Constants
|
||||
// ============================================================================
|
||||
|
||||
export const SlotRanges = {
|
||||
// Bedrock slot layout: 0-8 hotbar, 9-35 inventory, 36-39 armor, 45 offhand
|
||||
HOTBAR_START: 0,
|
||||
HOTBAR_END: 8,
|
||||
INVENTORY_START: 9,
|
||||
INVENTORY_END: 35,
|
||||
ARMOR_START: 36,
|
||||
ARMOR_END: 39,
|
||||
OFFHAND: 45,
|
||||
|
||||
// Hotbar count
|
||||
HOTBAR_COUNT: 9,
|
||||
|
||||
// Total slots in player inventory
|
||||
TOTAL_SLOTS: 46,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Window ID to Slot Index Mapping
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Map Bedrock window_id and slot to prismarine-windows slot index
|
||||
*
|
||||
* @param windowId - Bedrock window ID ("inventory", "armor", "hotbar", etc.)
|
||||
* @param slot - Slot index within that window
|
||||
* @returns Prismarine-windows slot index
|
||||
*/
|
||||
export function getSlotIndex(windowId: protocolTypes.WindowID, slot: number): number {
|
||||
switch (windowId) {
|
||||
case 'inventory':
|
||||
return slot;
|
||||
case 'armor':
|
||||
return SlotRanges.ARMOR_START + slot; // armor slots 36-39 (head, torso, legs, feet)
|
||||
case 'offhand':
|
||||
return SlotRanges.OFFHAND + slot; // offhand at slot 45 (Java compatibility)
|
||||
case 'hotbar':
|
||||
return slot;
|
||||
default:
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Window ID to Window Object Mapping
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Map Bedrock window_id to Window object
|
||||
*
|
||||
* @param bot - The bot instance
|
||||
* @param windowId - Bedrock window ID
|
||||
* @returns Window object or null for UI windows
|
||||
*/
|
||||
export function getWindow(bot: BedrockBot, windowId: protocolTypes.WindowID): Window | null {
|
||||
if (windowId === 'inventory' || windowId === 'armor' || windowId === 'offhand' || windowId === 'hotbar' || windowId === 'fixed_inventory') {
|
||||
return bot.inventory;
|
||||
} else if (windowId === 'ui') {
|
||||
return null;
|
||||
} else {
|
||||
// For container windows (chest, furnace, etc.), use currentWindow
|
||||
// Returns null if no container is currently open
|
||||
return bot.currentWindow;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Slot Index to Container ID Mapping
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Container location with ID and slot
|
||||
*/
|
||||
export interface ContainerLocation {
|
||||
containerId: string;
|
||||
slot: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container ID for cursor-based operations (take/place through cursor).
|
||||
* Based on packet captures:
|
||||
* - Hotbar slots (0-8) use "hotbar" container
|
||||
* - Main inventory slots (9-35) use "inventory" container
|
||||
* - Armor slots (36-39) use "armor" container
|
||||
* - Offhand (45) uses "offhand" container
|
||||
*
|
||||
* @param slotIndex - Prismarine-windows slot index
|
||||
* @returns Container location with ID and slot
|
||||
*/
|
||||
export function getContainerForCursorOp(slotIndex: number): ContainerLocation {
|
||||
if (slotIndex >= SlotRanges.HOTBAR_START && slotIndex <= SlotRanges.HOTBAR_END) {
|
||||
return { containerId: ContainerIds.HOTBAR, slot: slotIndex };
|
||||
} else if (slotIndex >= SlotRanges.INVENTORY_START && slotIndex <= SlotRanges.INVENTORY_END) {
|
||||
return { containerId: ContainerIds.INVENTORY, slot: slotIndex };
|
||||
} else if (slotIndex >= SlotRanges.ARMOR_START && slotIndex <= SlotRanges.ARMOR_END) {
|
||||
return { containerId: ContainerIds.ARMOR, slot: slotIndex - SlotRanges.ARMOR_START };
|
||||
} else if (slotIndex === SlotRanges.OFFHAND) {
|
||||
return { containerId: ContainerIds.OFFHAND, slot: 1 };
|
||||
} else {
|
||||
throw new Error(`Invalid slot index for cursor op: ${slotIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container ID from slot index, considering open container windows.
|
||||
*
|
||||
* @param slotIndex - Prismarine-windows slot index
|
||||
* @param window - Optional window object (for container slot detection)
|
||||
* @returns Container location with ID and slot
|
||||
*/
|
||||
export function getContainerFromSlot(slotIndex: number, window?: Window): ContainerLocation {
|
||||
// If we have a container window open, check if slot is in container section
|
||||
// Only apply container logic if inventoryStart > 9 (player inventory has inventoryStart=9)
|
||||
// Container windows like chests have inventoryStart >= 27
|
||||
if (window && (window as any).inventoryStart !== undefined) {
|
||||
const inventoryStart = (window as any).inventoryStart as number;
|
||||
|
||||
// Player inventory has inventoryStart=9 (separating hotbar from main inventory)
|
||||
// Container windows have inventoryStart >= 27 (chest) or more
|
||||
// Only treat as container window if inventoryStart is large enough to indicate a container
|
||||
if (inventoryStart > SlotRanges.INVENTORY_END) {
|
||||
if (slotIndex < inventoryStart) {
|
||||
// Container slot (e.g., chest slots 0-26)
|
||||
return { containerId: ContainerIds.CONTAINER, slot: slotIndex };
|
||||
}
|
||||
|
||||
// Adjust slot index for player inventory section within container window
|
||||
// Window slots 27-62 map to player inventory
|
||||
const playerSlot = slotIndex - inventoryStart;
|
||||
if (playerSlot >= SlotRanges.HOTBAR_START && playerSlot <= SlotRanges.HOTBAR_END) {
|
||||
return { containerId: ContainerIds.HOTBAR, slot: playerSlot };
|
||||
} else if (playerSlot >= SlotRanges.INVENTORY_START && playerSlot <= SlotRanges.INVENTORY_END) {
|
||||
return { containerId: ContainerIds.INVENTORY, slot: playerSlot };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Player inventory layout (Java compatible):
|
||||
// 0-8: hotbar (hotbar slots 0-8)
|
||||
// 9-35: main inventory (inventory slots 9-35)
|
||||
// 36-39: armor (armor slots 0-3: head, torso, legs, feet)
|
||||
// 45: offhand (offhand slot 0)
|
||||
|
||||
if (slotIndex >= SlotRanges.HOTBAR_START && slotIndex <= SlotRanges.HOTBAR_END) {
|
||||
return { containerId: ContainerIds.HOTBAR, slot: slotIndex };
|
||||
} else if (slotIndex >= SlotRanges.INVENTORY_START && slotIndex <= SlotRanges.INVENTORY_END) {
|
||||
return { containerId: ContainerIds.INVENTORY, slot: slotIndex };
|
||||
} else if (slotIndex >= SlotRanges.ARMOR_START && slotIndex <= SlotRanges.ARMOR_END) {
|
||||
return { containerId: ContainerIds.ARMOR, slot: slotIndex - SlotRanges.ARMOR_START };
|
||||
} else if (slotIndex === SlotRanges.OFFHAND) {
|
||||
// Offhand uses slot 1 in item_stack_request, not 0
|
||||
return { containerId: ContainerIds.OFFHAND, slot: 1 };
|
||||
} else {
|
||||
throw new Error(`Invalid slot index: ${slotIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert slot index to SlotLocation with stack ID from item
|
||||
*/
|
||||
export function slotToLocation(slotIndex: number, stackId: number, window?: Window): SlotLocation {
|
||||
const container = getContainerFromSlot(slotIndex, window);
|
||||
return {
|
||||
containerId: container.containerId,
|
||||
slot: container.slot,
|
||||
stackId,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Inventory Section Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if slot is in hotbar
|
||||
*/
|
||||
export function isHotbarSlot(slotIndex: number): boolean {
|
||||
return slotIndex >= SlotRanges.HOTBAR_START && slotIndex <= SlotRanges.HOTBAR_END;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if slot is in main inventory
|
||||
*/
|
||||
export function isInventorySlot(slotIndex: number): boolean {
|
||||
return slotIndex >= SlotRanges.INVENTORY_START && slotIndex <= SlotRanges.INVENTORY_END;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if slot is armor
|
||||
*/
|
||||
export function isArmorSlot(slotIndex: number): boolean {
|
||||
return slotIndex >= SlotRanges.ARMOR_START && slotIndex <= SlotRanges.ARMOR_END;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if slot is offhand
|
||||
*/
|
||||
export function isOffhandSlot(slotIndex: number): boolean {
|
||||
return slotIndex === SlotRanges.OFFHAND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get armor slot type (head, torso, legs, feet)
|
||||
*/
|
||||
export function getArmorType(slotIndex: number): 'head' | 'torso' | 'legs' | 'feet' | null {
|
||||
if (!isArmorSlot(slotIndex)) return null;
|
||||
const armorSlot = slotIndex - SlotRanges.ARMOR_START;
|
||||
const types: ('head' | 'torso' | 'legs' | 'feet')[] = ['head', 'torso', 'legs', 'feet'];
|
||||
return types[armorSlot];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slot index for armor type
|
||||
*/
|
||||
export function getArmorSlot(type: 'head' | 'torso' | 'legs' | 'feet'): number {
|
||||
const offsets = { head: 0, torso: 1, legs: 2, feet: 3 };
|
||||
return SlotRanges.ARMOR_START + offsets[type];
|
||||
}
|
||||
315
bridge/lib/mineflayer/lib/bedrock/workstations/anvil.mts
Normal file
315
bridge/lib/mineflayer/lib/bedrock/workstations/anvil.mts
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* Anvil Workstation - Repair and rename operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - anvil_input: slot 1
|
||||
* - anvil_material: slot 2
|
||||
*
|
||||
* Uses 'optional' action type for rename operations
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds, cursor } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots, CraftingSlots } from '../crafting-core.mts';
|
||||
import { twoStepTransfer } from '../container.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const AnvilSlots = {
|
||||
INPUT: 1,
|
||||
MATERIAL: 2,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Anvil Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface Anvil {
|
||||
window: Window;
|
||||
/** Rename item */
|
||||
rename: (newName: string) => Promise<void>;
|
||||
/** Combine items (repair) */
|
||||
combine: () => Promise<void>;
|
||||
/** Put item in first slot */
|
||||
putTarget: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Put item in material slot */
|
||||
putMaterial: (itemType: number | string, metadata: number | null, count: number) => Promise<void>;
|
||||
/** Take result */
|
||||
takeResult: () => Promise<Item | null>;
|
||||
/** Close the anvil */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open an anvil and return interface for repair/rename
|
||||
*
|
||||
* Note: Unlike other workstations, anvil in Bedrock doesn't require waiting for
|
||||
* container_open. The client opens the anvil UI locally and directly sends
|
||||
* item_stack_requests to anvil_input/anvil_material containers.
|
||||
*/
|
||||
export async function openAnvil(bot: BedrockBot, anvilBlock: Block): Promise<Anvil> {
|
||||
// Send the interaction packet but don't wait for container_open
|
||||
// Anvil is a "screen" that opens client-side without server confirmation
|
||||
await bot.lookAt(anvilBlock.position.offset(0.5, 0.5, 0.5), true);
|
||||
|
||||
// Send player_action: start_item_use_on to signal intent
|
||||
const entityId = bot.entity.id;
|
||||
const face = 1; // Click on top of anvil
|
||||
bot._client.write('player_action', {
|
||||
runtime_entity_id: entityId,
|
||||
action: 'start_item_use_on',
|
||||
position: {
|
||||
x: anvilBlock.position.x,
|
||||
y: anvilBlock.position.y,
|
||||
z: anvilBlock.position.z,
|
||||
},
|
||||
result_position: {
|
||||
x: anvilBlock.position.x,
|
||||
y: anvilBlock.position.y + 1,
|
||||
z: anvilBlock.position.z,
|
||||
},
|
||||
face: face,
|
||||
});
|
||||
|
||||
// Send inventory_transaction to click on the anvil
|
||||
bot._client.write('inventory_transaction', {
|
||||
transaction: {
|
||||
legacy: { legacy_request_id: 0 },
|
||||
transaction_type: 'item_use',
|
||||
actions: [],
|
||||
transaction_data: {
|
||||
action_type: 'click_block',
|
||||
trigger_type: 'player_input',
|
||||
block_position: anvilBlock.position,
|
||||
face: face,
|
||||
hotbar_slot: bot.quickBarSlot ?? 0,
|
||||
held_item: { network_id: 0 },
|
||||
player_pos: {
|
||||
x: bot.entity.position.x,
|
||||
y: bot.entity.position.y + 1.62,
|
||||
z: bot.entity.position.z,
|
||||
},
|
||||
click_pos: { x: 0.5, y: 0.5, z: 0.5 },
|
||||
block_runtime_id: (anvilBlock as any).stateId >>> 0,
|
||||
client_prediction: 'success',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Wait a moment for the server to acknowledge
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
// Create a minimal window object for compatibility
|
||||
const windowLoader = await import('prismarine-windows');
|
||||
const windows = (windowLoader.default as any)(bot.registry);
|
||||
const window = windows.createWindow(255, 'anvil', 'Anvil', 3); // 3 slots for anvil
|
||||
|
||||
bot.logger.debug(`Opened anvil (no container_open needed)`);
|
||||
|
||||
// Track stack IDs for items placed in anvil
|
||||
let inputStackId = 0;
|
||||
let materialStackId = 0;
|
||||
|
||||
return {
|
||||
window,
|
||||
|
||||
async rename(newName: string): Promise<void> {
|
||||
// Use the tracked inputStackId from putTarget, or try to get it from window slots
|
||||
const currentInputStackId = inputStackId || (window.slots[AnvilSlots.INPUT] ? getStackId(window.slots[AnvilSlots.INPUT]!) : 0);
|
||||
|
||||
// Find an empty slot for output
|
||||
const emptySlot = bot.inventory.slots.findIndex(
|
||||
(s, i) => i >= bot.inventory.inventoryStart && i <= bot.inventory.inventoryEnd && !s
|
||||
);
|
||||
const destSlot = emptySlot !== -1 ? emptySlot : 0;
|
||||
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Anvil rename uses 'optional' action + consume + place pattern
|
||||
// Based on packet capture: {"type":"optional"},{"type":"consume","src":"anvil_input:1",...},{"type":"place","src":"creative_output:50",...,"dst":"hotbar_and_inventory:9",...}
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'optional',
|
||||
filtered_string_index: 0,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.ANVIL_INPUT },
|
||||
slot: AnvilSlots.INPUT,
|
||||
stack_id: currentInputStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'place',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId,
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.HOTBAR_AND_INVENTORY },
|
||||
slot: destSlot,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot._client.write('item_stack_request', {
|
||||
requests: [
|
||||
{
|
||||
request_id: requestId,
|
||||
actions: actionList,
|
||||
custom_names: [newName],
|
||||
cause: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Anvil rename failed');
|
||||
}
|
||||
},
|
||||
|
||||
async combine(): Promise<void> {
|
||||
// Get stack IDs from items in anvil slots
|
||||
const inputItem = window.slots[AnvilSlots.INPUT];
|
||||
const materialItem = window.slots[AnvilSlots.MATERIAL];
|
||||
const inputStackId = inputItem ? getStackId(inputItem) : 0;
|
||||
const materialStackId = materialItem ? getStackId(materialItem) : 0;
|
||||
|
||||
// Find an empty slot for output
|
||||
const emptySlot = bot.inventory.slots.findIndex(
|
||||
(s, i) => i >= bot.inventory.inventoryStart && i <= bot.inventory.inventoryEnd && !s
|
||||
);
|
||||
const destSlot = emptySlot !== -1 ? emptySlot : 0;
|
||||
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Based on packet capture: optional + consume material + consume input + place
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'optional',
|
||||
filtered_string_index: 0,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: materialItem?.count || 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.ANVIL_MATERIAL },
|
||||
slot: AnvilSlots.MATERIAL,
|
||||
stack_id: materialStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.ANVIL_INPUT },
|
||||
slot: AnvilSlots.INPUT,
|
||||
stack_id: inputStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'place',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId,
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.HOTBAR_AND_INVENTORY },
|
||||
slot: destSlot,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot._client.write('item_stack_request', {
|
||||
requests: [
|
||||
{
|
||||
request_id: requestId,
|
||||
actions: actionList,
|
||||
custom_names: [''],
|
||||
cause: -1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Anvil combine failed');
|
||||
}
|
||||
},
|
||||
|
||||
async putTarget(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Item ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
|
||||
// Use two-step cursor transfer for anvil (like stonecutter)
|
||||
const result = await twoStepTransfer(
|
||||
bot,
|
||||
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId },
|
||||
{ containerId: ContainerIds.ANVIL_INPUT, slot: AnvilSlots.INPUT, stackId: 0 },
|
||||
1
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to put item in anvil');
|
||||
}
|
||||
|
||||
// Track the stack ID for later use in rename
|
||||
inputStackId = result.cursorStackId;
|
||||
},
|
||||
|
||||
async putMaterial(itemType: number | string, metadata: number | null, count: number): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Material ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
|
||||
// Use two-step cursor transfer for anvil
|
||||
const result = await twoStepTransfer(
|
||||
bot,
|
||||
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId },
|
||||
{ containerId: ContainerIds.ANVIL_MATERIAL, slot: AnvilSlots.MATERIAL, stackId: 0 },
|
||||
count
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to put material in anvil');
|
||||
}
|
||||
|
||||
// Track the stack ID for later use
|
||||
materialStackId = result.cursorStackId;
|
||||
},
|
||||
|
||||
async takeResult(): Promise<Item | null> {
|
||||
// Result is taken via combine/rename actions
|
||||
return null;
|
||||
},
|
||||
|
||||
close() {
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
}
|
||||
211
bridge/lib/mineflayer/lib/bedrock/workstations/brewing.mts
Normal file
211
bridge/lib/mineflayer/lib/bedrock/workstations/brewing.mts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* Brewing Stand Workstation - Potion brewing operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - brewing_input: slot 0 (ingredient like nether wart, sugar)
|
||||
* - brewing_result: slots 1, 2, 3 (three potion bottle slots)
|
||||
* - brewing_fuel: slot 4 (blaze powder)
|
||||
*
|
||||
* Pattern: Place items → server auto-brews (no craft action needed)
|
||||
* Similar to furnace - just place items and wait.
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots } from '../crafting-core.mts';
|
||||
import { twoStepTransfer } from '../container.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const BrewingSlots = {
|
||||
INGREDIENT: 0,
|
||||
RESULT_1: 1,
|
||||
RESULT_2: 2,
|
||||
RESULT_3: 3,
|
||||
FUEL: 4,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Brewing Stand Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface BrewingStand {
|
||||
window: Window;
|
||||
/** Put blaze powder as fuel */
|
||||
putFuel: (count: number) => Promise<void>;
|
||||
/** Put ingredient (nether wart, sugar, etc.) */
|
||||
putIngredient: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Put potion bottle in result slot (1-3) */
|
||||
putBottle: (slot: 1 | 2 | 3, itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Take bottle from result slot (1-3) */
|
||||
takeBottle: (slot: 1 | 2 | 3) => Promise<Item | null>;
|
||||
/** Get fuel item */
|
||||
fuelItem: () => Item | null;
|
||||
/** Get ingredient item */
|
||||
ingredientItem: () => Item | null;
|
||||
/** Get bottle in slot (1-3) */
|
||||
bottleItem: (slot: 1 | 2 | 3) => Item | null;
|
||||
/** Fuel remaining (0-1) */
|
||||
fuel: number;
|
||||
/** Brewing progress (0-1) */
|
||||
progress: number;
|
||||
/** Close the brewing stand */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a brewing stand block and return interface for brewing
|
||||
*/
|
||||
export async function openBrewingStand(bot: BedrockBot, brewingBlock: Block): Promise<BrewingStand> {
|
||||
const window = await bot.openBlock(brewingBlock);
|
||||
bot.logger.debug(`Opened brewing stand window: ${window?.id}`);
|
||||
|
||||
// Track fuel and progress from container_set_data packet
|
||||
let fuelProgress = 0;
|
||||
let brewProgress = 0;
|
||||
|
||||
// Listen for container data updates (brewing progress)
|
||||
const dataHandler = (packet: any) => {
|
||||
if (packet.window_id !== window.id) return;
|
||||
|
||||
// Property 0 = brewing time remaining
|
||||
// Property 1 = fuel amount
|
||||
if (packet.property === 0) {
|
||||
// Brewing progress - 400 ticks = full brew
|
||||
brewProgress = Math.min(1, 1 - (packet.value / 400));
|
||||
} else if (packet.property === 1) {
|
||||
// Fuel - 20 is full
|
||||
fuelProgress = packet.value / 20;
|
||||
}
|
||||
};
|
||||
bot._client.on('container_set_data', dataHandler);
|
||||
|
||||
const brewing: BrewingStand = {
|
||||
window,
|
||||
|
||||
get fuel() { return fuelProgress; },
|
||||
get progress() { return brewProgress; },
|
||||
|
||||
async putFuel(count: number): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, 'blaze_powder', null);
|
||||
if (!foundItem) {
|
||||
throw new Error('Blaze powder not found in inventory');
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.place(count, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.BREWING_FUEL, slot: BrewingSlots.FUEL, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to put fuel in brewing stand');
|
||||
}
|
||||
},
|
||||
|
||||
async putIngredient(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Ingredient ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.place(1, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.BREWING_INPUT, slot: BrewingSlots.INGREDIENT, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to put ingredient in brewing stand');
|
||||
}
|
||||
},
|
||||
|
||||
async putBottle(slot: 1 | 2 | 3, itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Potion/bottle ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const targetSlot = slot; // slots 1, 2, 3 map directly
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.place(1, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.BREWING_RESULT, slot: targetSlot, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error(`Failed to put bottle in brewing stand slot ${slot}`);
|
||||
}
|
||||
},
|
||||
|
||||
async takeBottle(slot: 1 | 2 | 3): Promise<Item | null> {
|
||||
const targetSlot = slot; // slots 1, 2, 3 map directly
|
||||
const item = window.slots[targetSlot];
|
||||
if (!item) return null;
|
||||
|
||||
const stackId = getStackId(item);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.takeToCursor(1, { containerId: ContainerIds.BREWING_RESULT, slot: targetSlot, stackId })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error(`Failed to take bottle from brewing stand slot ${slot}`);
|
||||
}
|
||||
|
||||
await bot.putAway(0);
|
||||
return item;
|
||||
},
|
||||
|
||||
fuelItem(): Item | null {
|
||||
return window.slots[BrewingSlots.FUEL] || null;
|
||||
},
|
||||
|
||||
ingredientItem(): Item | null {
|
||||
return window.slots[BrewingSlots.INGREDIENT] || null;
|
||||
},
|
||||
|
||||
bottleItem(slot: 1 | 2 | 3): Item | null {
|
||||
return window.slots[slot] || null;
|
||||
},
|
||||
|
||||
close() {
|
||||
bot._client.removeListener('container_set_data', dataHandler);
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
|
||||
return brewing;
|
||||
}
|
||||
206
bridge/lib/mineflayer/lib/bedrock/workstations/cartography.mts
Normal file
206
bridge/lib/mineflayer/lib/bedrock/workstations/cartography.mts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
* Cartography Table Workstation - Map operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - cartography_input: slot 12 (main map)
|
||||
* - cartography_additional: slot 13 (paper/empty map/glass pane)
|
||||
*
|
||||
* Pattern from captures:
|
||||
* optional + consume(cartography_input:12) + consume(cartography_additional:13) +
|
||||
* take(creative_output → cursor)
|
||||
*
|
||||
* Operations:
|
||||
* - Clone map: map + empty_map → 2 identical maps
|
||||
* - Extend map: map + paper → larger map
|
||||
* - Lock map: map + glass_pane → locked (non-updating) map
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds, cursor } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots, CraftingSlots } from '../crafting-core.mts';
|
||||
import { twoStepTransfer } from '../container.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const CartographySlots = {
|
||||
INPUT: 12,
|
||||
ADDITIONAL: 13,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Cartography Table Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface CartographyTable {
|
||||
window: Window;
|
||||
/** Put map in input slot */
|
||||
putMap: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Put paper in additional slot (for extending) */
|
||||
putPaper: () => Promise<void>;
|
||||
/** Put empty map in additional slot (for cloning) */
|
||||
putEmptyMap: () => Promise<void>;
|
||||
/** Put glass pane in additional slot (for locking) */
|
||||
putGlassPane: () => Promise<void>;
|
||||
/** Execute the cartography operation (clone/extend/lock) */
|
||||
craft: () => Promise<void>;
|
||||
/** Take result from cartography table */
|
||||
takeResult: () => Promise<Item | null>;
|
||||
/** Get current map item */
|
||||
mapItem: () => Item | null;
|
||||
/** Get current additional item */
|
||||
additionalItem: () => Item | null;
|
||||
/** Close the cartography table */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a cartography table block and return interface for map operations
|
||||
*/
|
||||
export async function openCartographyTable(bot: BedrockBot, cartographyBlock: Block): Promise<CartographyTable> {
|
||||
const window = await bot.openBlock(cartographyBlock);
|
||||
bot.logger.debug(`Opened cartography table window: ${window?.id}`);
|
||||
|
||||
// Track stack IDs of placed items
|
||||
let mapStackId = 0;
|
||||
let additionalStackId = 0;
|
||||
|
||||
async function putItem(containerId: string, slot: number, itemType: number | string, metadata: number | null): Promise<number> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Item ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
|
||||
const result = await twoStepTransfer(
|
||||
bot,
|
||||
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId },
|
||||
{ containerId, slot, stackId: 0 },
|
||||
1
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Failed to place ${itemType} in cartography table`);
|
||||
}
|
||||
|
||||
return result.cursorStackId;
|
||||
}
|
||||
|
||||
return {
|
||||
window,
|
||||
|
||||
async putMap(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
mapStackId = await putItem(ContainerIds.CARTOGRAPHY_INPUT, CartographySlots.INPUT, itemType, metadata);
|
||||
},
|
||||
|
||||
async putPaper(): Promise<void> {
|
||||
additionalStackId = await putItem(ContainerIds.CARTOGRAPHY_ADDITIONAL, CartographySlots.ADDITIONAL, 'paper', null);
|
||||
},
|
||||
|
||||
async putEmptyMap(): Promise<void> {
|
||||
additionalStackId = await putItem(ContainerIds.CARTOGRAPHY_ADDITIONAL, CartographySlots.ADDITIONAL, 'empty_map', null);
|
||||
},
|
||||
|
||||
async putGlassPane(): Promise<void> {
|
||||
additionalStackId = await putItem(ContainerIds.CARTOGRAPHY_ADDITIONAL, CartographySlots.ADDITIONAL, 'glass_pane', null);
|
||||
},
|
||||
|
||||
async craft(): Promise<void> {
|
||||
// Get current stack IDs from window slots
|
||||
const mapItem = window.slots[CartographySlots.INPUT];
|
||||
const addItem = window.slots[CartographySlots.ADDITIONAL];
|
||||
|
||||
if (!mapItem || !addItem) {
|
||||
throw new Error('Map and additional item required in cartography table');
|
||||
}
|
||||
|
||||
const currentMapStackId = getStackId(mapItem) || mapStackId;
|
||||
const currentAdditionalStackId = getStackId(addItem) || additionalStackId;
|
||||
|
||||
// Need to wait a bit for server to register the placed items
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Pattern from packet captures:
|
||||
// optional + consume(input) + consume(additional) + take(output)
|
||||
// Note: Cartography uses 'optional' instead of craft_recipe
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'optional',
|
||||
filtered_string_index: 0,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CARTOGRAPHY_INPUT },
|
||||
slot: CartographySlots.INPUT,
|
||||
stack_id: currentMapStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CARTOGRAPHY_ADDITIONAL },
|
||||
slot: CartographySlots.ADDITIONAL,
|
||||
stack_id: currentAdditionalStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'take',
|
||||
count: 2, // Clone produces 2 maps
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId,
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.CURSOR },
|
||||
slot: 0,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot.logger.debug(`Cartography craft: mapStackId=${currentMapStackId}, additionalStackId=${currentAdditionalStackId}`);
|
||||
sendRequest(bot, requestId, actionList);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Cartography table craft failed');
|
||||
}
|
||||
|
||||
// Put result away from cursor
|
||||
await bot.putAway(0);
|
||||
},
|
||||
|
||||
async takeResult(): Promise<Item | null> {
|
||||
// Result is already in cursor after craft, just put away
|
||||
await bot.putAway(0);
|
||||
return null;
|
||||
},
|
||||
|
||||
mapItem(): Item | null {
|
||||
return window.slots[CartographySlots.INPUT] || null;
|
||||
},
|
||||
|
||||
additionalItem(): Item | null {
|
||||
return window.slots[CartographySlots.ADDITIONAL] || null;
|
||||
},
|
||||
|
||||
close() {
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
}
|
||||
368
bridge/lib/mineflayer/lib/bedrock/workstations/enchanting.mts
Normal file
368
bridge/lib/mineflayer/lib/bedrock/workstations/enchanting.mts
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
/**
|
||||
* Enchanting Table Workstation - Enchanting operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - enchanting_input: slot 14
|
||||
* - enchanting_lapis: slot 15
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots, CraftingSlots } from '../crafting-core.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const EnchantingSlots = {
|
||||
INPUT: 14,
|
||||
LAPIS: 15,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Enchantment Table Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface EnchantmentTable {
|
||||
window: Window;
|
||||
/** Enchant item. enchantSlot is 0-2 for the three enchant options */
|
||||
enchant: (enchantSlot: number) => Promise<void>;
|
||||
/** Put item to enchant */
|
||||
putItem: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Put lapis lazuli */
|
||||
putLapis: (count: number) => Promise<void>;
|
||||
/** Take enchanted item back */
|
||||
takeItem: () => Promise<Item | null>;
|
||||
/** Close the enchanting table */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open an enchanting table and return interface for enchanting
|
||||
*/
|
||||
export async function openEnchantmentTable(bot: BedrockBot, enchantTableBlock: Block): Promise<EnchantmentTable> {
|
||||
const window = await bot.openBlock(enchantTableBlock);
|
||||
bot.logger.debug(`Opened enchanting table window: ${window?.id}`);
|
||||
|
||||
// Track enchant options from server
|
||||
let enchantOptions: any[] = [];
|
||||
let inputStackId = 0;
|
||||
let lapisStackId = 0;
|
||||
let inputItemInfo: { network_id: number; count: number; metadata: number; block_runtime_id: number; extra: any } | null = null;
|
||||
|
||||
// Listen for enchant options from server
|
||||
const optionsHandler = (packet: any) => {
|
||||
enchantOptions = packet.options || [];
|
||||
bot.logger.debug(`Received ${enchantOptions.length} enchant options`);
|
||||
};
|
||||
bot._client.on('player_enchant_options', optionsHandler);
|
||||
|
||||
return {
|
||||
window,
|
||||
|
||||
/**
|
||||
* Get available enchantment options
|
||||
*/
|
||||
getOptions(): any[] {
|
||||
return enchantOptions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enchant item using the option at the given index (0-2)
|
||||
* The enchantSlot must be an option_id from player_enchant_options packet
|
||||
*
|
||||
* Pattern from packet captures (verified working):
|
||||
* 1. craft_recipe(recipeNetworkId) + results_deprecated
|
||||
* 2. consume(enchanting_input:14)
|
||||
* 3. place(creative_output → enchanting_input:14) - result goes BACK to input slot
|
||||
* 4. consume(enchanting_lapis:15) - lapis consumed AFTER placing result
|
||||
*/
|
||||
async enchant(enchantSlot: number): Promise<void> {
|
||||
// If we have options, use the option_id from the options list
|
||||
// The option_id is received as zigzag32, but recipe_network_id needs the unsigned encoding
|
||||
// zigzag encode: (n << 1) ^ (n >> 31) for 32-bit
|
||||
let optionId = enchantSlot;
|
||||
if (enchantOptions.length > 0) {
|
||||
if (enchantSlot >= 0 && enchantSlot < enchantOptions.length) {
|
||||
const rawOptionId = enchantOptions[enchantSlot].option_id;
|
||||
// Convert zigzag-decoded value back to unsigned varint: zigzag encode
|
||||
optionId = (rawOptionId << 1) ^ (rawOptionId >> 31);
|
||||
bot.logger.debug(`Using enchant option ${enchantSlot}: rawOptionId=${rawOptionId}, encoded=${optionId}`);
|
||||
} else {
|
||||
throw new Error(`Invalid enchant slot ${enchantSlot}, only ${enchantOptions.length} options available`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the lapis cost from enchant options (defaults to 1 for level 1)
|
||||
const lapisCost = enchantOptions.length > 0 ? (enchantOptions[enchantSlot].cost || 1) : 1;
|
||||
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Pattern from packet captures:
|
||||
// craft_recipe + results_deprecated + consume(input) + place(output→input) + consume(lapis)
|
||||
// Include the input item as result (required to prevent server crash)
|
||||
const resultItems = inputItemInfo ? [{
|
||||
network_id: inputItemInfo.network_id,
|
||||
count: inputItemInfo.count,
|
||||
metadata: inputItemInfo.metadata,
|
||||
block_runtime_id: inputItemInfo.block_runtime_id,
|
||||
extra: inputItemInfo.extra,
|
||||
}] : [];
|
||||
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'craft_recipe',
|
||||
recipe_network_id: optionId, // Use option_id from enchant options
|
||||
times_crafted: 1,
|
||||
},
|
||||
{
|
||||
type_id: 'results_deprecated',
|
||||
result_items: resultItems,
|
||||
times_crafted: 1,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.ENCHANTING_INPUT },
|
||||
slot: EnchantingSlots.INPUT,
|
||||
stack_id: inputStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Result placed BACK to enchanting_input slot (key pattern!)
|
||||
type_id: 'place',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId, // Use -requestId as source stack
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.ENCHANTING_INPUT },
|
||||
slot: EnchantingSlots.INPUT,
|
||||
stack_id: requestId, // Use -requestId as dest stack (same as source)
|
||||
},
|
||||
},
|
||||
{
|
||||
// Consume lapis AFTER placing result back
|
||||
type_id: 'consume',
|
||||
count: lapisCost,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.ENCHANTING_LAPIS },
|
||||
slot: EnchantingSlots.LAPIS,
|
||||
stack_id: lapisStackId,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot.logger.debug(`Enchanting: optionId=${optionId}, inputStackId=${inputStackId}, lapisStackId=${lapisStackId}, lapisCost=${lapisCost}`);
|
||||
|
||||
// Capture the new stackId from the response
|
||||
const captureHandler = (packet: any) => {
|
||||
for (const resp of packet.responses || []) {
|
||||
if (resp.request_id === requestId && resp.status === 'ok') {
|
||||
for (const container of resp.containers || []) {
|
||||
if (container.slot_type?.container_id === 'enchanting_input') {
|
||||
for (const slot of container.slots || []) {
|
||||
if (slot.slot === EnchantingSlots.INPUT && slot.item_stack_id > 0) {
|
||||
inputStackId = slot.item_stack_id;
|
||||
bot.logger.debug(`Captured enchanted item stackId: ${inputStackId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
bot._client.once('item_stack_response', captureHandler);
|
||||
|
||||
sendRequest(bot, requestId, actionList);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
bot._client.removeListener('item_stack_response', captureHandler);
|
||||
throw new Error('Enchanting failed');
|
||||
}
|
||||
|
||||
// Clear options after enchanting
|
||||
enchantOptions = [];
|
||||
},
|
||||
|
||||
async putItem(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Item ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
// Store item info for results_deprecated - ItemLegacy format
|
||||
// network_id of 0 means empty slot (void), so we only need network_id for non-empty
|
||||
inputItemInfo = {
|
||||
network_id: (foundItem as any).nid ?? foundItem.type,
|
||||
count: 1,
|
||||
metadata: foundItem.metadata ?? 0,
|
||||
block_runtime_id: 0,
|
||||
extra: { has_nbt: 0, can_place_on: [], can_destroy: [] },
|
||||
};
|
||||
bot.logger.debug(`Stored input item info: network_id=${inputItemInfo.network_id}`);
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Use take + place through cursor like real client
|
||||
// Step 1: Take item to cursor
|
||||
const takeRequestId = requestId;
|
||||
sendRequest(
|
||||
bot,
|
||||
takeRequestId,
|
||||
actions()
|
||||
.takeToCursor(1, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, takeRequestId))) {
|
||||
throw new Error('Failed to take item to cursor');
|
||||
}
|
||||
|
||||
// Step 2: Place from cursor to enchanting_input
|
||||
const placeRequestId = getNextItemStackRequestId();
|
||||
let capturedStackId = stackId;
|
||||
const captureHandler = (packet: any) => {
|
||||
for (const resp of packet.responses || []) {
|
||||
if (resp.request_id === placeRequestId && resp.status === 'ok') {
|
||||
for (const container of resp.containers || []) {
|
||||
if (container.slot_type?.container_id === 'enchanting_input') {
|
||||
for (const slot of container.slots || []) {
|
||||
if (slot.slot === EnchantingSlots.INPUT && slot.item_stack_id > 0) {
|
||||
capturedStackId = slot.item_stack_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
bot._client.once('item_stack_response', captureHandler);
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
placeRequestId,
|
||||
actions()
|
||||
.place(1, { containerId: ContainerIds.CURSOR, slot: 0, stackId }, { containerId: ContainerIds.ENCHANTING_INPUT, slot: EnchantingSlots.INPUT, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, placeRequestId))) {
|
||||
bot._client.removeListener('item_stack_response', captureHandler);
|
||||
throw new Error('Failed to put item in enchanting table');
|
||||
}
|
||||
|
||||
inputStackId = capturedStackId;
|
||||
bot.logger.debug(`Enchanting input stackId: ${inputStackId}`);
|
||||
|
||||
// Wait a bit for server to send enchant options
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
},
|
||||
|
||||
async putLapis(count: number): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, 'lapis_lazuli', null);
|
||||
if (!foundItem) {
|
||||
throw new Error('Lapis lazuli not found in inventory');
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const totalCount = foundItem.count;
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Use take + place through cursor like real client
|
||||
// Step 1: Take lapis to cursor (take all)
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.takeToCursor(totalCount, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to take lapis to cursor');
|
||||
}
|
||||
|
||||
// Step 2: Place count lapis from cursor to enchanting_lapis
|
||||
const placeRequestId = getNextItemStackRequestId();
|
||||
let capturedStackId = stackId;
|
||||
const captureHandler = (packet: any) => {
|
||||
for (const resp of packet.responses || []) {
|
||||
if (resp.request_id === placeRequestId && resp.status === 'ok') {
|
||||
for (const container of resp.containers || []) {
|
||||
if (container.slot_type?.container_id === 'enchanting_lapis') {
|
||||
for (const slot of container.slots || []) {
|
||||
if (slot.slot === EnchantingSlots.LAPIS && slot.item_stack_id > 0) {
|
||||
capturedStackId = slot.item_stack_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
bot._client.once('item_stack_response', captureHandler);
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
placeRequestId,
|
||||
actions()
|
||||
.place(count, { containerId: ContainerIds.CURSOR, slot: 0, stackId }, { containerId: ContainerIds.ENCHANTING_LAPIS, slot: EnchantingSlots.LAPIS, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, placeRequestId))) {
|
||||
bot._client.removeListener('item_stack_response', captureHandler);
|
||||
throw new Error('Failed to put lapis in enchanting table');
|
||||
}
|
||||
|
||||
lapisStackId = capturedStackId;
|
||||
bot.logger.debug(`Enchanting lapis stackId: ${lapisStackId}`);
|
||||
|
||||
// If we had more lapis than needed, put the rest back
|
||||
if (totalCount > count) {
|
||||
const putBackRequestId = getNextItemStackRequestId();
|
||||
sendRequest(
|
||||
bot,
|
||||
putBackRequestId,
|
||||
actions()
|
||||
.place(totalCount - count, { containerId: ContainerIds.CURSOR, slot: 0, stackId }, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
await waitForResponse(bot, putBackRequestId);
|
||||
}
|
||||
},
|
||||
|
||||
async takeItem(): Promise<Item | null> {
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Use the captured stackId from enchanting response
|
||||
sendRequest(bot, requestId, actions().takeToCursor(1, { containerId: ContainerIds.ENCHANTING_INPUT, slot: EnchantingSlots.INPUT, stackId: inputStackId }).build());
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await bot.putAway(0);
|
||||
return null; // Would need to track the item
|
||||
},
|
||||
|
||||
close() {
|
||||
bot._client.removeListener('player_enchant_options', optionsHandler);
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
}
|
||||
216
bridge/lib/mineflayer/lib/bedrock/workstations/furnace.mts
Normal file
216
bridge/lib/mineflayer/lib/bedrock/workstations/furnace.mts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/**
|
||||
* Furnace Workstation - Smelting operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - furnace_ingredient: slot 0
|
||||
* - furnace_fuel: slot 1
|
||||
* - furnace_output: slot 2
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots } from '../crafting-core.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const FurnaceSlots = {
|
||||
INGREDIENT: 0,
|
||||
FUEL: 1,
|
||||
OUTPUT: 2,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Furnace Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface Furnace {
|
||||
window: Window;
|
||||
/** Put item in ingredient slot */
|
||||
putIngredient: (itemType: number | string, metadata: number | null, count: number) => Promise<void>;
|
||||
/** Put item in fuel slot */
|
||||
putFuel: (itemType: number | string, metadata: number | null, count: number) => Promise<void>;
|
||||
/** Take item from ingredient slot */
|
||||
takeInput: () => Promise<Item | null>;
|
||||
/** Take item from fuel slot */
|
||||
takeFuel: () => Promise<Item | null>;
|
||||
/** Take item from output slot */
|
||||
takeOutput: () => Promise<Item | null>;
|
||||
/** Get current ingredient item */
|
||||
inputItem: () => Item | null;
|
||||
/** Get current fuel item */
|
||||
fuelItem: () => Item | null;
|
||||
/** Get current output item */
|
||||
outputItem: () => Item | null;
|
||||
/** Fuel progress (0-1) - percentage of fuel remaining */
|
||||
fuel: number;
|
||||
/** Smelting progress (0-1) - percentage of current item smelted */
|
||||
progress: number;
|
||||
/** Close the furnace */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a furnace block and return interface for smelting
|
||||
*/
|
||||
export async function openFurnace(bot: BedrockBot, furnaceBlock: Block): Promise<Furnace> {
|
||||
const window = await bot.openBlock(furnaceBlock);
|
||||
bot.logger.debug(`Opened furnace window: ${window?.id}`);
|
||||
|
||||
// Track fuel and progress from container_set_data packet
|
||||
let fuelProgress = 0;
|
||||
let smeltProgress = 0;
|
||||
|
||||
// Listen for container data updates (furnace progress bars)
|
||||
// Bedrock sends container_set_data with property IDs:
|
||||
// 0 = furnace tick count (smelting progress)
|
||||
// 1 = furnace lit time (fuel remaining)
|
||||
// 2 = furnace lit duration (max fuel time)
|
||||
// 3 = furnace tick count total (max smelting time)
|
||||
const dataHandler = (packet: any) => {
|
||||
if (packet.window_id !== window.id) return;
|
||||
|
||||
// Property 0/3 = smelting progress
|
||||
// Property 1/2 = fuel progress
|
||||
// Values are raw tick counts, convert to 0-1 range
|
||||
if (packet.property === 0 || packet.property === 3) {
|
||||
// Smelting progress - 200 ticks = full smelt
|
||||
smeltProgress = Math.min(1, packet.value / 200);
|
||||
} else if (packet.property === 1 || packet.property === 2) {
|
||||
// Fuel progress - depends on fuel type
|
||||
if (packet.property === 1 && packet.value > 0) {
|
||||
fuelProgress = packet.value / 1600; // Coal = 1600 ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
bot._client.on('container_set_data', dataHandler);
|
||||
|
||||
const furnace: Furnace = {
|
||||
window,
|
||||
|
||||
get fuel() { return fuelProgress; },
|
||||
get progress() { return smeltProgress; },
|
||||
|
||||
async putIngredient(itemType: number | string, metadata: number | null, count: number): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Item ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.place(count, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.FURNACE_INGREDIENT, slot: FurnaceSlots.INGREDIENT, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to put ingredient in furnace');
|
||||
}
|
||||
},
|
||||
|
||||
async putFuel(itemType: number | string, metadata: number | null, count: number): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Fuel ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions().place(count, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.FURNACE_FUEL, slot: FurnaceSlots.FUEL, stackId: 0 }).build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to put fuel in furnace');
|
||||
}
|
||||
},
|
||||
|
||||
async takeInput(): Promise<Item | null> {
|
||||
const inputItem = window.slots[FurnaceSlots.INGREDIENT];
|
||||
if (!inputItem) return null;
|
||||
|
||||
const stackId = getStackId(inputItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(bot, requestId, actions().takeToCursor(inputItem.count, { containerId: ContainerIds.FURNACE_INGREDIENT, slot: FurnaceSlots.INGREDIENT, stackId }).build());
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to take input from furnace');
|
||||
}
|
||||
|
||||
await bot.putAway(0);
|
||||
return inputItem;
|
||||
},
|
||||
|
||||
async takeFuel(): Promise<Item | null> {
|
||||
const fuelItem = window.slots[FurnaceSlots.FUEL];
|
||||
if (!fuelItem) return null;
|
||||
|
||||
const stackId = getStackId(fuelItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(bot, requestId, actions().takeToCursor(fuelItem.count, { containerId: ContainerIds.FURNACE_FUEL, slot: FurnaceSlots.FUEL, stackId }).build());
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to take fuel from furnace');
|
||||
}
|
||||
|
||||
await bot.putAway(0);
|
||||
return fuelItem;
|
||||
},
|
||||
|
||||
async takeOutput(): Promise<Item | null> {
|
||||
const outputItem = window.slots[FurnaceSlots.OUTPUT];
|
||||
if (!outputItem) return null;
|
||||
|
||||
const stackId = getStackId(outputItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(bot, requestId, actions().takeToCursor(outputItem.count, { containerId: ContainerIds.FURNACE_OUTPUT, slot: FurnaceSlots.OUTPUT, stackId }).build());
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to take output from furnace');
|
||||
}
|
||||
|
||||
await bot.putAway(0);
|
||||
return outputItem;
|
||||
},
|
||||
|
||||
inputItem(): Item | null {
|
||||
return window.slots[FurnaceSlots.INGREDIENT] || null;
|
||||
},
|
||||
|
||||
fuelItem(): Item | null {
|
||||
return window.slots[FurnaceSlots.FUEL] || null;
|
||||
},
|
||||
|
||||
outputItem(): Item | null {
|
||||
return window.slots[FurnaceSlots.OUTPUT] || null;
|
||||
},
|
||||
|
||||
close() {
|
||||
bot._client.removeListener('container_set_data', dataHandler);
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
|
||||
return furnace;
|
||||
}
|
||||
167
bridge/lib/mineflayer/lib/bedrock/workstations/grindstone.mts
Normal file
167
bridge/lib/mineflayer/lib/bedrock/workstations/grindstone.mts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* Grindstone Workstation - Disenchanting operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - grindstone_input: slot 16
|
||||
*
|
||||
* Pattern from captures:
|
||||
* craft_grindstone_request(recipeNetworkId) + results_deprecated +
|
||||
* consume(grindstone_input:16) + take(creative_output → cursor)
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds, cursor } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots, CraftingSlots } from '../crafting-core.mts';
|
||||
import { twoStepTransfer } from '../container.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const GrindstoneSlots = {
|
||||
INPUT: 16,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Grindstone Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface Grindstone {
|
||||
window: Window;
|
||||
/** Put enchanted item in input slot */
|
||||
putItem: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Disenchant the item (removes enchantments, returns XP) */
|
||||
disenchant: () => Promise<void>;
|
||||
/** Take result from grindstone */
|
||||
takeResult: () => Promise<Item | null>;
|
||||
/** Get current input item */
|
||||
inputItem: () => Item | null;
|
||||
/** Close the grindstone */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a grindstone block and return interface for disenchanting
|
||||
*/
|
||||
export async function openGrindstone(bot: BedrockBot, grindstoneBlock: Block): Promise<Grindstone> {
|
||||
const window = await bot.openBlock(grindstoneBlock);
|
||||
bot.logger.debug(`Opened grindstone window: ${window?.id}`);
|
||||
|
||||
// Track stack ID of placed item
|
||||
let inputStackId = 0;
|
||||
|
||||
return {
|
||||
window,
|
||||
|
||||
async putItem(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Item ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
|
||||
// Use two-step transfer (via cursor) like stonecutter
|
||||
const result = await twoStepTransfer(
|
||||
bot,
|
||||
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId },
|
||||
{ containerId: ContainerIds.GRINDSTONE_INPUT, slot: GrindstoneSlots.INPUT, stackId: 0 },
|
||||
1
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to place item in grindstone');
|
||||
}
|
||||
|
||||
// Track the stack ID for later disenchant
|
||||
inputStackId = result.cursorStackId;
|
||||
},
|
||||
|
||||
async disenchant(): Promise<void> {
|
||||
// Get the current stack ID from window slot
|
||||
const inputItem = window.slots.find((s) => s != null);
|
||||
const currentStackId = inputItem ? getStackId(inputItem) : inputStackId;
|
||||
|
||||
if (!inputItem) {
|
||||
throw new Error('No item in grindstone to disenchant');
|
||||
}
|
||||
|
||||
// Need to wait a bit for server to register the placed item
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Pattern from packet captures:
|
||||
// craft_grindstone_request + results_deprecated + consume + take
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'craft_grindstone_request',
|
||||
// Grindstone uses a dynamic recipe ID based on the item
|
||||
// The server calculates it, we can use a placeholder
|
||||
recipe_network_id: 6117, // This is item-specific, may need adjustment
|
||||
times_crafted: 1,
|
||||
},
|
||||
{
|
||||
type_id: 'results_deprecated',
|
||||
result_items: [],
|
||||
times_crafted: 1,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.GRINDSTONE_INPUT },
|
||||
slot: GrindstoneSlots.INPUT,
|
||||
stack_id: currentStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'take',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId,
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.CURSOR },
|
||||
slot: 0,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot.logger.debug(`Grindstone disenchant: stackId=${currentStackId}`);
|
||||
sendRequest(bot, requestId, actionList);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Grindstone disenchant failed');
|
||||
}
|
||||
|
||||
// Put result away from cursor
|
||||
await bot.putAway(0);
|
||||
},
|
||||
|
||||
async takeResult(): Promise<Item | null> {
|
||||
// Result is already in cursor after disenchant, just put away
|
||||
await bot.putAway(0);
|
||||
return null;
|
||||
},
|
||||
|
||||
inputItem(): Item | null {
|
||||
return window.slots[GrindstoneSlots.INPUT] || null;
|
||||
},
|
||||
|
||||
close() {
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
}
|
||||
13
bridge/lib/mineflayer/lib/bedrock/workstations/index.mts
Normal file
13
bridge/lib/mineflayer/lib/bedrock/workstations/index.mts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Workstations Index - Re-exports all workstation modules
|
||||
*/
|
||||
|
||||
export { openFurnace, FurnaceSlots, type Furnace } from './furnace.mts';
|
||||
export { openAnvil, AnvilSlots, type Anvil } from './anvil.mts';
|
||||
export { openEnchantmentTable, EnchantingSlots, type EnchantmentTable } from './enchanting.mts';
|
||||
export { openSmithingTable, SmithingSlots, type SmithingTable } from './smithing.mts';
|
||||
export { openStonecutter, StonecutterSlots, type Stonecutter } from './stonecutter.mts';
|
||||
export { openGrindstone, GrindstoneSlots, type Grindstone } from './grindstone.mts';
|
||||
export { openLoom, LoomSlots, type Loom } from './loom.mts';
|
||||
export { openBrewingStand, BrewingSlots, type BrewingStand } from './brewing.mts';
|
||||
export { openCartographyTable, CartographySlots, type CartographyTable } from './cartography.mts';
|
||||
209
bridge/lib/mineflayer/lib/bedrock/workstations/loom.mts
Normal file
209
bridge/lib/mineflayer/lib/bedrock/workstations/loom.mts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* Loom Workstation - Banner pattern operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - loom_input: slot 9 (banner)
|
||||
* - loom_dye: slot 10 (dye)
|
||||
*
|
||||
* Pattern from captures (NO recipeNetworkId!):
|
||||
* craft_loom_request(timesCrafted) + results_deprecated +
|
||||
* consume(loom_input:9) + consume(loom_dye:10) + take(creative_output → cursor)
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds, cursor } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots, CraftingSlots } from '../crafting-core.mts';
|
||||
import { twoStepTransfer } from '../container.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const LoomSlots = {
|
||||
BANNER: 9,
|
||||
DYE: 10,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Loom Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface Loom {
|
||||
window: Window;
|
||||
/** Put banner in input slot */
|
||||
putBanner: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Put dye in dye slot */
|
||||
putDye: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Apply pattern to banner (pattern selection is done via UI, this just confirms) */
|
||||
applyPattern: () => Promise<void>;
|
||||
/** Take result from loom */
|
||||
takeResult: () => Promise<Item | null>;
|
||||
/** Get current banner item */
|
||||
bannerItem: () => Item | null;
|
||||
/** Get current dye item */
|
||||
dyeItem: () => Item | null;
|
||||
/** Close the loom */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a loom block and return interface for banner patterns
|
||||
*/
|
||||
export async function openLoom(bot: BedrockBot, loomBlock: Block): Promise<Loom> {
|
||||
const window = await bot.openBlock(loomBlock);
|
||||
bot.logger.debug(`Opened loom window: ${window?.id}`);
|
||||
|
||||
// Track stack IDs of placed items
|
||||
let bannerStackId = 0;
|
||||
let dyeStackId = 0;
|
||||
|
||||
return {
|
||||
window,
|
||||
|
||||
async putBanner(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Banner ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
|
||||
const result = await twoStepTransfer(
|
||||
bot,
|
||||
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId },
|
||||
{ containerId: ContainerIds.LOOM_INPUT, slot: LoomSlots.BANNER, stackId: 0 },
|
||||
1
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to place banner in loom');
|
||||
}
|
||||
|
||||
bannerStackId = result.cursorStackId;
|
||||
},
|
||||
|
||||
async putDye(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Dye ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
|
||||
const result = await twoStepTransfer(
|
||||
bot,
|
||||
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId },
|
||||
{ containerId: ContainerIds.LOOM_DYE, slot: LoomSlots.DYE, stackId: 0 },
|
||||
1
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to place dye in loom');
|
||||
}
|
||||
|
||||
dyeStackId = result.cursorStackId;
|
||||
},
|
||||
|
||||
async applyPattern(): Promise<void> {
|
||||
// Get current stack IDs from window slots
|
||||
const bannerItem = window.slots[LoomSlots.BANNER];
|
||||
const dyeItem = window.slots[LoomSlots.DYE];
|
||||
|
||||
if (!bannerItem || !dyeItem) {
|
||||
throw new Error('Banner and dye required in loom');
|
||||
}
|
||||
|
||||
const currentBannerStackId = getStackId(bannerItem) || bannerStackId;
|
||||
const currentDyeStackId = getStackId(dyeItem) || dyeStackId;
|
||||
|
||||
// Need to wait a bit for server to register the placed items
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Pattern from packet captures:
|
||||
// craft_loom_request (NO recipe_network_id!) + results_deprecated + 2x consume + take
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'craft_loom_request',
|
||||
times_crafted: 1,
|
||||
// Note: Loom does NOT have recipe_network_id, pattern is selected via UI
|
||||
},
|
||||
{
|
||||
type_id: 'results_deprecated',
|
||||
result_items: [],
|
||||
times_crafted: 1,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.LOOM_INPUT },
|
||||
slot: LoomSlots.BANNER,
|
||||
stack_id: currentBannerStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.LOOM_DYE },
|
||||
slot: LoomSlots.DYE,
|
||||
stack_id: currentDyeStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'take',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId,
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.CURSOR },
|
||||
slot: 0,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot.logger.debug(`Loom apply pattern: bannerStackId=${currentBannerStackId}, dyeStackId=${currentDyeStackId}`);
|
||||
sendRequest(bot, requestId, actionList);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Loom pattern application failed');
|
||||
}
|
||||
|
||||
// Put result away from cursor
|
||||
await bot.putAway(0);
|
||||
},
|
||||
|
||||
async takeResult(): Promise<Item | null> {
|
||||
// Result is already in cursor after applyPattern, just put away
|
||||
await bot.putAway(0);
|
||||
return null;
|
||||
},
|
||||
|
||||
bannerItem(): Item | null {
|
||||
return window.slots[LoomSlots.BANNER] || null;
|
||||
},
|
||||
|
||||
dyeItem(): Item | null {
|
||||
return window.slots[LoomSlots.DYE] || null;
|
||||
},
|
||||
|
||||
close() {
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
}
|
||||
253
bridge/lib/mineflayer/lib/bedrock/workstations/smithing.mts
Normal file
253
bridge/lib/mineflayer/lib/bedrock/workstations/smithing.mts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
/**
|
||||
* Smithing Table Workstation - Upgrade operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - smithing_table_template: slot 53
|
||||
* - smithing_table_input: slot 51
|
||||
* - smithing_table_material: slot 52
|
||||
*/
|
||||
|
||||
import type { Item as ItemType } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds } from '../item-stack-actions.mts';
|
||||
import { findItemInAllSlots, CraftingSlots } from '../crafting-core.mts';
|
||||
import PItem from 'prismarine-item';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const SmithingSlots = {
|
||||
TEMPLATE: 53,
|
||||
INPUT: 51,
|
||||
MATERIAL: 52,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Smithing Table Interface
|
||||
// ============================================================================
|
||||
|
||||
/** Result item info for smithing upgrade */
|
||||
export interface SmithingResult {
|
||||
network_id: number;
|
||||
count?: number;
|
||||
metadata?: number;
|
||||
block_runtime_id?: number;
|
||||
extra?: { has_nbt: number; can_place_on: any[]; can_destroy: any[] };
|
||||
}
|
||||
|
||||
export interface SmithingTable {
|
||||
window: Window;
|
||||
/** Upgrade item (e.g., diamond to netherite) - requires recipe network ID and result item info */
|
||||
upgrade: (recipeNetworkId: number, result: SmithingResult) => Promise<void>;
|
||||
/** Put template in template slot */
|
||||
putTemplate: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Put item to upgrade */
|
||||
putInput: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Put upgrade material */
|
||||
putMaterial: (itemType: number | string, metadata: number | null) => Promise<void>;
|
||||
/** Close the smithing table */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a smithing table and return interface for upgrades
|
||||
*/
|
||||
export async function openSmithingTable(bot: BedrockBot, smithingBlock: Block): Promise<SmithingTable> {
|
||||
const window = await bot.openBlock(smithingBlock);
|
||||
bot.logger.debug(`Opened smithing table window: ${window?.id}`);
|
||||
|
||||
// Track stack IDs of items placed in smithing slots
|
||||
let templateStackId = 0;
|
||||
let inputStackId = 0;
|
||||
let materialStackId = 0;
|
||||
|
||||
return {
|
||||
window,
|
||||
|
||||
async upgrade(recipeNetworkId: number, result: SmithingResult): Promise<void> {
|
||||
// Get stack IDs from window slots (updated after putTemplate/putInput/putMaterial)
|
||||
const templateItem = window.slots[SmithingSlots.TEMPLATE];
|
||||
const inputItem = window.slots[SmithingSlots.INPUT];
|
||||
const materialItem = window.slots[SmithingSlots.MATERIAL];
|
||||
|
||||
const tStackId = templateItem ? getStackId(templateItem) : templateStackId;
|
||||
const iStackId = inputItem ? getStackId(inputItem) : inputStackId;
|
||||
const mStackId = materialItem ? getStackId(materialItem) : materialStackId;
|
||||
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
// Build result_items for results_deprecated (required by protocol)
|
||||
const resultItems = [{
|
||||
network_id: result.network_id,
|
||||
count: result.count ?? 1,
|
||||
metadata: result.metadata ?? 0,
|
||||
block_runtime_id: result.block_runtime_id ?? 0,
|
||||
extra: result.extra ?? { has_nbt: 0, can_place_on: [], can_destroy: [] },
|
||||
}];
|
||||
|
||||
// Find an empty slot for output
|
||||
const emptySlot = bot.inventory.slots.findIndex(
|
||||
(s, i) => i >= bot.inventory.inventoryStart && i <= bot.inventory.inventoryEnd && !s
|
||||
);
|
||||
const destSlot = emptySlot !== -1 ? emptySlot : 0;
|
||||
|
||||
// Based on packet capture: craft_recipe + results_deprecated + consume (all 3) + place
|
||||
// Use 'place' directly to inventory like stonecutter (not 'take' to cursor)
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'craft_recipe',
|
||||
recipe_network_id: recipeNetworkId,
|
||||
times_crafted: 1,
|
||||
},
|
||||
{
|
||||
type_id: 'results_deprecated',
|
||||
result_items: resultItems,
|
||||
times_crafted: 1,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.SMITHING_TABLE_TEMPLATE },
|
||||
slot: SmithingSlots.TEMPLATE,
|
||||
stack_id: tStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.SMITHING_TABLE_INPUT },
|
||||
slot: SmithingSlots.INPUT,
|
||||
stack_id: iStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.SMITHING_TABLE_MATERIAL },
|
||||
slot: SmithingSlots.MATERIAL,
|
||||
stack_id: mStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'place',
|
||||
count: 1,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: requestId,
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.HOTBAR_AND_INVENTORY },
|
||||
slot: destSlot,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
sendRequest(bot, requestId, actionList);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Smithing upgrade failed');
|
||||
}
|
||||
|
||||
// item_stack_response only updates counts, not creates new items
|
||||
// Manually create the output item in the destination slot
|
||||
const Item = PItem(bot.registry);
|
||||
const newItem = new Item(result.network_id, result.count ?? 1, result.metadata ?? 0);
|
||||
(newItem as any).stackId = requestId; // Use request_id as stack_id
|
||||
bot.inventory.updateSlot(destSlot, newItem);
|
||||
},
|
||||
|
||||
async putTemplate(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Template ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.place(1, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.SMITHING_TABLE_TEMPLATE, slot: SmithingSlots.TEMPLATE, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to put template in smithing table');
|
||||
}
|
||||
|
||||
// Track the stack ID for later use in upgrade
|
||||
templateStackId = stackId;
|
||||
},
|
||||
|
||||
async putInput(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Input item ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.place(1, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.SMITHING_TABLE_INPUT, slot: SmithingSlots.INPUT, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to put input in smithing table');
|
||||
}
|
||||
|
||||
// Track the stack ID for later use in upgrade
|
||||
inputStackId = stackId;
|
||||
},
|
||||
|
||||
async putMaterial(itemType: number | string, metadata: number | null): Promise<void> {
|
||||
const foundItem = findItemInAllSlots(bot, itemType, metadata);
|
||||
if (!foundItem) {
|
||||
throw new Error(`Material ${itemType} not found in inventory`);
|
||||
}
|
||||
|
||||
const slotIndex = foundItem.slot;
|
||||
const stackId = getStackId(foundItem);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
requestId,
|
||||
actions()
|
||||
.place(1, { containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: slotIndex, stackId }, { containerId: ContainerIds.SMITHING_TABLE_MATERIAL, slot: SmithingSlots.MATERIAL, stackId: 0 })
|
||||
.build()
|
||||
);
|
||||
|
||||
if (!(await waitForResponse(bot, requestId))) {
|
||||
throw new Error('Failed to put material in smithing table');
|
||||
}
|
||||
|
||||
// Track the stack ID for later use in upgrade
|
||||
materialStackId = stackId;
|
||||
},
|
||||
|
||||
close() {
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
}
|
||||
142
bridge/lib/mineflayer/lib/bedrock/workstations/stonecutter.mts
Normal file
142
bridge/lib/mineflayer/lib/bedrock/workstations/stonecutter.mts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Stonecutter Workstation - Stone crafting operations for Bedrock protocol
|
||||
*
|
||||
* Container IDs from packet captures:
|
||||
* - stonecutter_input: slot 3
|
||||
*/
|
||||
|
||||
import type { Item } from 'prismarine-item';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../../index.js';
|
||||
import { actions, getNextItemStackRequestId, getStackId, sendRequest, waitForResponse, ContainerIds } from '../item-stack-actions.mts';
|
||||
import { CraftingSlots } from '../crafting-core.mts';
|
||||
import { twoStepTransfer } from '../container.mts';
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const StonecutterSlots = {
|
||||
INPUT: 3,
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// Stonecutter Interface
|
||||
// ============================================================================
|
||||
|
||||
export interface Stonecutter {
|
||||
window: Window;
|
||||
/** Craft using stonecutter. recipeNetworkId is the recipe's network_id */
|
||||
craft: (recipeNetworkId: number, count?: number) => Promise<void>;
|
||||
/** Close the stonecutter */
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Implementation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Open a stonecutter block and return interface for crafting
|
||||
*/
|
||||
export async function openStonecutter(bot: BedrockBot, stonecutterBlock: Block): Promise<Stonecutter> {
|
||||
const window = await bot.openBlock(stonecutterBlock);
|
||||
bot.logger.debug(`Opened stonecutter window: ${window?.id}`);
|
||||
|
||||
// Track the stack ID of items placed in stonecutter input
|
||||
let inputStackId = 0;
|
||||
|
||||
return {
|
||||
window,
|
||||
|
||||
async craft(recipeNetworkId: number, count: number = 1): Promise<void> {
|
||||
// Find stone-type item in inventory for initial placement
|
||||
const inputSlot = bot.inventory.slots.findIndex((s) => s && s.name?.includes('stone'));
|
||||
if (inputSlot === -1) {
|
||||
throw new Error('No stone item found in inventory for stonecutter');
|
||||
}
|
||||
|
||||
const inputItem = bot.inventory.slots[inputSlot]!;
|
||||
const sourceStackId = getStackId(inputItem);
|
||||
|
||||
// Transfer item to stonecutter using two-step cursor transfer
|
||||
const result = await twoStepTransfer(
|
||||
bot,
|
||||
{ containerId: ContainerIds.HOTBAR_AND_INVENTORY, slot: inputSlot, stackId: sourceStackId },
|
||||
{ containerId: ContainerIds.STONECUTTER_INPUT, slot: StonecutterSlots.INPUT, stackId: 0 },
|
||||
count
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error('Failed to place item in stonecutter');
|
||||
}
|
||||
|
||||
// The stackId of items in stonecutter is tracked from the cursor transfer
|
||||
inputStackId = result.cursorStackId;
|
||||
|
||||
// Find an empty slot for output
|
||||
const emptySlot = bot.inventory.slots.findIndex((s, i) =>
|
||||
i >= bot.inventory.inventoryStart && i <= bot.inventory.inventoryEnd && !s
|
||||
);
|
||||
const destSlot = emptySlot !== -1 ? emptySlot : 0;
|
||||
|
||||
// Need to wait a bit for server to register the placed item
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
// Craft the item - use craft_recipe + consume + place pattern from packet captures
|
||||
const craftRequestId = getNextItemStackRequestId();
|
||||
|
||||
// Debug: log window slots
|
||||
bot.logger.info(`Stonecutter window slots: ${JSON.stringify(window.slots.map((s) => s ? { name: s.name, count: s.count, stackId: getStackId(s) } : null))}`);
|
||||
|
||||
// Get the updated stackId from the window slot (it may have been updated by the server)
|
||||
const stonecutterInputItem = window.slots.find((s) => s != null);
|
||||
const currentInputStackId = stonecutterInputItem ? getStackId(stonecutterInputItem) : inputStackId;
|
||||
|
||||
bot.logger.info(`Stonecutter craft: recipeId=${recipeNetworkId}, inputStackId=${currentInputStackId} (tracked: ${inputStackId}), destSlot=${destSlot}`);
|
||||
|
||||
const actionList: any[] = [
|
||||
{
|
||||
type_id: 'craft_recipe',
|
||||
recipe_network_id: recipeNetworkId,
|
||||
times_crafted: count,
|
||||
},
|
||||
{
|
||||
type_id: 'consume',
|
||||
count: count,
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.STONECUTTER_INPUT },
|
||||
slot: StonecutterSlots.INPUT,
|
||||
stack_id: currentInputStackId,
|
||||
},
|
||||
},
|
||||
{
|
||||
type_id: 'place',
|
||||
count: count, // Stonecutter typically outputs 1:1 or 2:1
|
||||
source: {
|
||||
slot_type: { container_id: ContainerIds.CREATIVE_OUTPUT },
|
||||
slot: CraftingSlots.CREATIVE_OUTPUT_SLOT,
|
||||
stack_id: craftRequestId, // Use negative request ID as stack ID for crafted items
|
||||
},
|
||||
destination: {
|
||||
slot_type: { container_id: ContainerIds.HOTBAR_AND_INVENTORY },
|
||||
slot: destSlot,
|
||||
stack_id: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
bot.logger.debug(`Stonecutter actions: ${JSON.stringify(actionList)}`);
|
||||
sendRequest(bot, craftRequestId, actionList);
|
||||
|
||||
if (!(await waitForResponse(bot, craftRequestId))) {
|
||||
throw new Error('Stonecutter craft failed');
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
bot.closeWindow(window);
|
||||
},
|
||||
};
|
||||
}
|
||||
30
bridge/lib/mineflayer/lib/bedrockPlugins/attribute-patch.js
Normal file
30
bridge/lib/mineflayer/lib/bedrockPlugins/attribute-patch.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Patch prismarine-physics to handle missing bedrock attributes
|
||||
const origPhysics = require('prismarine-physics');
|
||||
const { Physics: OrigPhysics, PlayerState } = origPhysics;
|
||||
|
||||
function PatchedPhysics(mcData, world) {
|
||||
// If attributesByName is missing, provide defaults
|
||||
if (!mcData.attributesByName || !mcData.attributesByName.movementSpeed) {
|
||||
const attrs = [
|
||||
{ name: 'movementSpeed', resource: 'minecraft:movement', min: 0, max: 3.4028235E38, default: 0.1 },
|
||||
{ name: 'followRange', resource: 'minecraft:follow_range', min: 0, max: 2048, default: 16 },
|
||||
{ name: 'knockbackResistance', resource: 'minecraft:knockback_resistance', min: 0, max: 1, default: 0 },
|
||||
{ name: 'attackDamage', resource: 'minecraft:attack_damage', min: 0, max: 3.4028235E38, default: 1 },
|
||||
{ name: 'armor', resource: 'minecraft:armor', min: 0, max: 30, default: 0 },
|
||||
{ name: 'armorToughness', resource: 'minecraft:armor_toughness', min: 0, max: 20, default: 0 },
|
||||
{ name: 'attackSpeed', resource: 'minecraft:attack_speed', min: 0, max: 1024, default: 4 },
|
||||
{ name: 'luck', resource: 'minecraft:luck', min: -1024, max: 1024, default: 0 },
|
||||
{ name: 'maxHealth', resource: 'minecraft:health', min: 0, max: 1024, default: 20 },
|
||||
];
|
||||
mcData.attributesArray = attrs;
|
||||
mcData.attributes = {};
|
||||
mcData.attributesByName = {};
|
||||
for (const attr of attrs) {
|
||||
mcData.attributes[attr.resource] = attr;
|
||||
mcData.attributesByName[attr.name] = attr;
|
||||
}
|
||||
}
|
||||
return OrigPhysics(mcData, world);
|
||||
}
|
||||
|
||||
module.exports = { Physics: PatchedPhysics, PlayerState };
|
||||
229
bridge/lib/mineflayer/lib/bedrockPlugins/bed.mts
Normal file
229
bridge/lib/mineflayer/lib/bedrockPlugins/bed.mts
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import type { Bot } from '../..'
|
||||
import type { Block } from 'prismarine-block'
|
||||
import { Vec3 } from 'vec3'
|
||||
|
||||
export default function inject(bot: Bot) {
|
||||
bot.isSleeping = false
|
||||
|
||||
// All bed block names in Bedrock Edition
|
||||
const beds = new Set([
|
||||
'white_bed', 'orange_bed', 'magenta_bed', 'light_blue_bed',
|
||||
'yellow_bed', 'lime_bed', 'pink_bed', 'gray_bed',
|
||||
'light_gray_bed', 'cyan_bed', 'purple_bed', 'blue_bed',
|
||||
'brown_bed', 'green_bed', 'red_bed', 'black_bed', 'bed'
|
||||
])
|
||||
|
||||
// Bedrock direction: 0=south, 1=west, 2=north, 3=east
|
||||
// Maps to offset from foot to head
|
||||
const DIRECTION_OFFSETS: Record<number, Vec3> = {
|
||||
0: new Vec3(0, 0, 1), // south (+Z)
|
||||
1: new Vec3(-1, 0, 0), // west (-X)
|
||||
2: new Vec3(0, 0, -1), // north (-Z)
|
||||
3: new Vec3(1, 0, 0), // east (+X)
|
||||
}
|
||||
|
||||
function isABed(block: Block): boolean {
|
||||
return beds.has(block.name)
|
||||
}
|
||||
|
||||
interface BedMetadata {
|
||||
part: boolean // true: head, false: foot
|
||||
occupied: boolean
|
||||
facing: number // 0: south, 1: west, 2: north, 3: east
|
||||
headOffset: Vec3 // offset from foot to head
|
||||
}
|
||||
|
||||
function parseBedMetadata(bedBlock: Block): BedMetadata {
|
||||
const metadata: BedMetadata = {
|
||||
part: false,
|
||||
occupied: false,
|
||||
facing: 0,
|
||||
headOffset: new Vec3(0, 0, 1)
|
||||
}
|
||||
|
||||
// Bedrock uses _properties for block state
|
||||
const props = (bedBlock as any)._properties
|
||||
if (props) {
|
||||
// direction: 0=south, 1=west, 2=north, 3=east
|
||||
metadata.facing = props.direction ?? props['minecraft:direction'] ?? 0
|
||||
// head_piece_bit: true for head, false for foot
|
||||
metadata.part = props.head_piece_bit === true || props.head_piece_bit === 1
|
||||
// occupied_bit: true if someone is in the bed
|
||||
metadata.occupied = props.occupied_bit === true || props.occupied_bit === 1
|
||||
// Calculate head offset based on direction
|
||||
metadata.headOffset = DIRECTION_OFFSETS[metadata.facing] || new Vec3(0, 0, 1)
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
async function wake(): Promise<void> {
|
||||
if (!bot.isSleeping) {
|
||||
throw new Error('already awake')
|
||||
}
|
||||
|
||||
// Bedrock uses player_action packet with stop_sleeping action
|
||||
bot._client.write('player_action', {
|
||||
runtime_entity_id: bot.entity.id,
|
||||
action: 'stop_sleeping',
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
result_position: { x: 0, y: 0, z: 0 },
|
||||
face: 0
|
||||
})
|
||||
}
|
||||
|
||||
async function sleep(bedBlock: Block): Promise<void> {
|
||||
const thunderstorm = bot.isRaining && (bot.thunderState > 0)
|
||||
if (!thunderstorm && !(bot.time.timeOfDay >= 12541 && bot.time.timeOfDay <= 23458)) {
|
||||
throw new Error("it's not night and it's not a thunderstorm")
|
||||
}
|
||||
if (bot.isSleeping) {
|
||||
throw new Error('already sleeping')
|
||||
}
|
||||
if (!isABed(bedBlock)) {
|
||||
throw new Error('wrong block : not a bed block')
|
||||
}
|
||||
|
||||
const botPos = bot.entity.position.floored()
|
||||
const metadata = parseBedMetadata(bedBlock)
|
||||
let headPoint = bedBlock.position
|
||||
|
||||
if (metadata.occupied) {
|
||||
throw new Error('the bed is occupied')
|
||||
}
|
||||
|
||||
if (!metadata.part) {
|
||||
// This is the foot part, find the head
|
||||
const upperBlock = bot.blockAt(bedBlock.position.plus(metadata.headOffset))
|
||||
|
||||
if (upperBlock && isABed(upperBlock)) {
|
||||
headPoint = upperBlock.position
|
||||
} else {
|
||||
// Try the opposite direction
|
||||
const lowerBlock = bot.blockAt(bedBlock.position.plus(metadata.headOffset.scaled(-1)))
|
||||
|
||||
if (lowerBlock && isABed(lowerBlock)) {
|
||||
// If there are 2 foot parts, minecraft only lets you sleep if you click on the lower one
|
||||
headPoint = bedBlock.position
|
||||
bedBlock = lowerBlock
|
||||
} else {
|
||||
throw new Error("there's only half bed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bot.canDigBlock(bedBlock)) {
|
||||
throw new Error('cant click the bed')
|
||||
}
|
||||
|
||||
// Check distance constraints
|
||||
const clickRange = [2, -3, -3, 2] // [south, west, north, east]
|
||||
const monsterRange = [7, -8, -8, 7]
|
||||
const oppositeCardinal = (metadata.facing + 2) % 4
|
||||
|
||||
if (clickRange[oppositeCardinal] < 0) {
|
||||
clickRange[oppositeCardinal]--
|
||||
} else {
|
||||
clickRange[oppositeCardinal]++
|
||||
}
|
||||
|
||||
const nwClickCorner = headPoint.offset(clickRange[1], -2, clickRange[2])
|
||||
const seClickCorner = headPoint.offset(clickRange[3], 2, clickRange[0])
|
||||
if (
|
||||
botPos.x > seClickCorner.x || botPos.x < nwClickCorner.x ||
|
||||
botPos.y > seClickCorner.y || botPos.y < nwClickCorner.y ||
|
||||
botPos.z > seClickCorner.z || botPos.z < nwClickCorner.z
|
||||
) {
|
||||
throw new Error('the bed is too far')
|
||||
}
|
||||
|
||||
// Check for monsters nearby (unless creative mode)
|
||||
if (bot.game.gameMode !== 'creative') {
|
||||
const nwMonsterCorner = headPoint.offset(monsterRange[1], -6, monsterRange[2])
|
||||
const seMonsterCorner = headPoint.offset(monsterRange[3], 4, monsterRange[0])
|
||||
|
||||
for (const key of Object.keys(bot.entities)) {
|
||||
const entity = bot.entities[key]
|
||||
if (entity.kind === 'Hostile mobs') {
|
||||
const entityPos = entity.position.floored()
|
||||
if (
|
||||
entityPos.x <= seMonsterCorner.x && entityPos.x >= nwMonsterCorner.x &&
|
||||
entityPos.y <= seMonsterCorner.y && entityPos.y >= nwMonsterCorner.y &&
|
||||
entityPos.z <= seMonsterCorner.z && entityPos.z >= nwMonsterCorner.z
|
||||
) {
|
||||
throw new Error('there are monsters nearby')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register listener before activating to avoid race conditions
|
||||
const waitingPromise = waitUntilSleep()
|
||||
await bot.activateBlock(bedBlock)
|
||||
await waitingPromise
|
||||
}
|
||||
|
||||
async function waitUntilSleep(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutForSleep = setTimeout(() => {
|
||||
reject(new Error('bot is not sleeping'))
|
||||
}, 3000)
|
||||
|
||||
bot.once('sleep', () => {
|
||||
clearTimeout(timeoutForSleep)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Handle animate packet for wake_up action
|
||||
bot._client.on('animate', (packet: { action: string; runtime_entity_id?: any; entity_id?: any }) => {
|
||||
if (packet.action === 'wake_up') {
|
||||
const entityId = packet.runtime_entity_id ?? packet.entity_id
|
||||
if (entityId === bot.entity.id) {
|
||||
bot.isSleeping = false
|
||||
bot.emit('wake')
|
||||
} else {
|
||||
// Another entity woke up
|
||||
const entity = bot.entities[entityId]
|
||||
if (entity) {
|
||||
bot.emit('entityWake', entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle player_action for sleep detection
|
||||
// Note: The client sends start_sleeping after server confirms bed interaction
|
||||
// We track this via the server's response (occupied bit change or move to bed position)
|
||||
bot._client.on('player_action', (packet: { action: string; runtime_entity_id?: any }) => {
|
||||
if (packet.action === 'start_sleeping' && packet.runtime_entity_id === bot.entity.id) {
|
||||
bot.isSleeping = true
|
||||
bot.emit('sleep')
|
||||
}
|
||||
})
|
||||
|
||||
// Handle set_spawn_position for spawnReset event
|
||||
// This is emitted when player can't spawn at their bed (bed destroyed, obstructed)
|
||||
// In Bedrock, we track this via set_spawn_position with specific values
|
||||
bot._client.on('set_spawn_position', (packet: {
|
||||
spawn_type: string
|
||||
player_position?: { x: number; y: number; z: number }
|
||||
world_position?: { x: number; y: number; z: number }
|
||||
}) => {
|
||||
// When spawn type is 'player' and position is invalid (very large negative values),
|
||||
// it indicates spawn reset
|
||||
if (packet.spawn_type === 'player') {
|
||||
const pos = packet.player_position
|
||||
if (pos && (pos.x === -2147483648 || pos.y === -2147483648 || pos.z === -2147483648)) {
|
||||
bot.emit('spawnReset')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Expose API
|
||||
bot.parseBedMetadata = parseBedMetadata
|
||||
bot.wake = wake
|
||||
bot.sleep = sleep
|
||||
bot.isABed = isABed
|
||||
}
|
||||
128
bridge/lib/mineflayer/lib/bedrockPlugins/block_actions.mts
Normal file
128
bridge/lib/mineflayer/lib/bedrockPlugins/block_actions.mts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
|
||||
const CARDINALS = {
|
||||
north: new Vec3(0, 0, -1),
|
||||
south: new Vec3(0, 0, 1),
|
||||
west: new Vec3(-1, 0, 0),
|
||||
east: new Vec3(1, 0, 0),
|
||||
};
|
||||
|
||||
const FACING_MAP: Record<string, Record<string, string>> = {
|
||||
north: { west: 'right', east: 'left' },
|
||||
south: { west: 'left', east: 'right' },
|
||||
west: { north: 'left', south: 'right' },
|
||||
east: { north: 'right', south: 'left' },
|
||||
};
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const { instruments, blocks } = bot.registry;
|
||||
|
||||
// Stores how many players have currently open a container at a certain position
|
||||
const openCountByPos: Record<string, number> = {};
|
||||
|
||||
function parseChestMetadata(chestBlock: any) {
|
||||
// Bedrock uses _properties with minecraft:cardinal_direction
|
||||
// Java uses metadata with encoded facing/type/waterlogged
|
||||
if (chestBlock._properties) {
|
||||
// Bedrock Edition - get facing from block properties
|
||||
const facing = chestBlock._properties['minecraft:cardinal_direction'] as string;
|
||||
return { facing };
|
||||
} else if (bot.registry.blockStates && chestBlock.stateId !== undefined) {
|
||||
// Alternative: use registry blockStates
|
||||
const states = bot.registry.blockStates[chestBlock.stateId]?.states;
|
||||
const facing = states?.['minecraft:cardinal_direction']?.value as string;
|
||||
return { facing };
|
||||
} else {
|
||||
// Fallback for Java Edition
|
||||
const chestTypes = ['single', 'right', 'left'];
|
||||
return bot.supportFeature('doesntHaveChestType')
|
||||
? { facing: Object.keys(CARDINALS)[chestBlock.metadata - 2] }
|
||||
: {
|
||||
waterlogged: !(chestBlock.metadata & 1),
|
||||
type: chestTypes[(chestBlock.metadata >> 1) % 3],
|
||||
facing: Object.keys(CARDINALS)[Math.floor(chestBlock.metadata / 6)],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getChestType(chestBlock: any) {
|
||||
// Returns 'single', 'right' or 'left'
|
||||
// if (bot.supportFeature('doesntHaveChestType')) {
|
||||
const facing = parseChestMetadata(chestBlock).facing;
|
||||
|
||||
if (!facing) return 'single';
|
||||
|
||||
// We have to check if the adjacent blocks in the perpendicular cardinals are the same type
|
||||
const perpendicularCardinals = Object.keys(FACING_MAP[facing]);
|
||||
for (const cardinal of perpendicularCardinals) {
|
||||
const cardinalOffset = CARDINALS[cardinal as keyof typeof CARDINALS];
|
||||
if (bot.blockAt(chestBlock.position.plus(cardinalOffset))?.type === chestBlock.type) {
|
||||
return FACING_MAP[cardinal][facing];
|
||||
}
|
||||
}
|
||||
|
||||
return 'single';
|
||||
// } else {
|
||||
// return parseChestMetadata(chestBlock).type
|
||||
// }
|
||||
}
|
||||
|
||||
bot._client.on('block_event', (packet) => {
|
||||
const pt = new Vec3(packet.position.x, packet.position.y, packet.position.z);
|
||||
const block = bot.blockAt(pt);
|
||||
|
||||
// Ignore on non-vanilla blocks
|
||||
if (block === null) {
|
||||
return;
|
||||
} // !blocks[block.type] non vanilla <---
|
||||
|
||||
const blockName = block.name;
|
||||
|
||||
if (blockName === 'noteblock') {
|
||||
// Pre 1.13
|
||||
bot.emit('noteHeard', block, instruments[packet.data], packet.data);
|
||||
} else if (blockName === 'note_block') {
|
||||
// 1.13 onward
|
||||
bot.emit('noteHeard', block, instruments[Math.floor(block.metadata / 50)], Math.floor((block.metadata % 50) / 2));
|
||||
} else if (blockName === 'sticky_piston' || blockName === 'piston') {
|
||||
bot.emit('pistonMove', block, packet.data, packet.data); // find java values!!!
|
||||
} else {
|
||||
let block2 = null;
|
||||
|
||||
if (blockName === 'chest' || blockName === 'trapped_chest') {
|
||||
const chestType = getChestType(block);
|
||||
if (chestType === 'right') {
|
||||
const index = Object.values(FACING_MAP[parseChestMetadata(block).facing!]).indexOf('left');
|
||||
const cardinalBlock2 = Object.keys(FACING_MAP[parseChestMetadata(block).facing!])[index];
|
||||
const block2Position = block.position.plus(CARDINALS[cardinalBlock2 as keyof typeof CARDINALS]);
|
||||
block2 = bot.blockAt(block2Position);
|
||||
} else if (chestType === 'left') return; // Omit left part of the chest so 'chestLidMove' doesn't emit twice when it's a double chest
|
||||
}
|
||||
|
||||
// Emit 'chestLidMove' only if the number of players with the lid open changes
|
||||
if (openCountByPos[block.position.toString()] !== packet.data) {
|
||||
bot.emit('chestLidMove', block, packet.data === 1, block2);
|
||||
if (packet.data > 0) {
|
||||
openCountByPos[block.position.toString()] = packet.data;
|
||||
} else {
|
||||
delete openCountByPos[block.position.toString()];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('level_event', (packet) => {
|
||||
if (packet.event === 'block_start_break' || packet.event === 'block_stop_break') {
|
||||
const destroyStage = 0; //packet.destroyStage unavalible for bedrock, calculates client-side
|
||||
const pt = new Vec3(packet.position.x, packet.position.y, packet.position.z);
|
||||
const block = bot.blockAt(pt);
|
||||
const entity = null; //bot.entities[packet.entityId]
|
||||
if (packet.event === 'block_stop_break') {
|
||||
bot.emit('blockBreakProgressEnd', block, entity);
|
||||
} else {
|
||||
bot.emit('blockBreakProgressObserved', block, destroyStage, entity);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
715
bridge/lib/mineflayer/lib/bedrockPlugins/blocks.mts
Normal file
715
bridge/lib/mineflayer/lib/bedrockPlugins/blocks.mts
Normal file
|
|
@ -0,0 +1,715 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
import assert from 'assert';
|
||||
import { onceWithCleanup } from '../promise_utils.js';
|
||||
import fs from 'fs';
|
||||
import { createRequire } from 'module';
|
||||
import type { BedrockChunk } from 'prismarine-chunk';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const { BlobEntry, BlobType } = require('prismarine-chunk');
|
||||
const BlobStore = require('../BlobStore');
|
||||
|
||||
const { OctahedronIterator } = require('prismarine-world/src/iterators');
|
||||
|
||||
const serialize = (obj: any) => JSON.stringify(obj, (k, v) => (typeof v?.valueOf?.() === 'bigint' ? v.toString() : v));
|
||||
|
||||
const dimensionNames: Record<string, string> = {
|
||||
'-1': 'minecraft:nether',
|
||||
0: 'minecraft:overworld',
|
||||
1: 'minecraft:end',
|
||||
};
|
||||
|
||||
interface BlocksOptions {
|
||||
version?: string;
|
||||
storageBuilder?: any;
|
||||
hideErrors?: boolean;
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot, { version, storageBuilder, hideErrors }: BlocksOptions = {}) {
|
||||
// const registry = bot._client.host !== 'mco.cubecraft.net' ? bot.registry : require('prismarine-registry')('bedrock_1.18.30')
|
||||
const Block = require('prismarine-block')(bot.registry);
|
||||
const Chunk = require('prismarine-chunk')(bot.registry); // bot.registry ChunkColumn bot.registry
|
||||
const World = require('prismarine-world')(bot.registry);
|
||||
const blobStore = new BlobStore();
|
||||
|
||||
function delColumn(chunkX: number, chunkZ: number) {
|
||||
bot.world.unloadColumn(chunkX, chunkZ);
|
||||
}
|
||||
// load chunk into a column
|
||||
function addColumn(args: any) {
|
||||
try {
|
||||
bot.world.setColumn(args.x, args.z, args.column);
|
||||
} catch (e) {
|
||||
bot.emit('error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForChunksToLoad() {
|
||||
const dist = 4;
|
||||
// This makes sure that the bot's real position has been already sent
|
||||
if (!bot.entity.height) await onceWithCleanup(bot, 'chunkColumnLoad');
|
||||
const pos = bot.entity.position;
|
||||
const center = new Vec3((pos.x >> 4) << 4, 0, (pos.z >> 4) << 4);
|
||||
// get corner coords of 5x5 chunks around us
|
||||
const chunkPosToCheck = new Set<string>();
|
||||
for (let x = -dist; x <= dist; x++) {
|
||||
for (let y = -dist; y <= dist; y++) {
|
||||
// ignore any chunks which are already loaded
|
||||
const pos = center.plus(new Vec3(x, 0, y).scaled(16));
|
||||
if (!bot.world.getColumnAt(pos)) chunkPosToCheck.add(pos.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (chunkPosToCheck.size) {
|
||||
return new Promise<void>((resolve) => {
|
||||
function waitForLoadEvents(columnCorner: Vec3) {
|
||||
chunkPosToCheck.delete(columnCorner.toString());
|
||||
if (chunkPosToCheck.size === 0) {
|
||||
// no chunks left to find
|
||||
bot.world.off('chunkColumnLoad', waitForLoadEvents); // remove this listener instance
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
// begin listening for remaining chunks to load
|
||||
bot.world.on('chunkColumnLoad', waitForLoadEvents);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.on('join', () => {
|
||||
bot._client.queue('client_cache_status', { enabled: cachingEnabled });
|
||||
});
|
||||
|
||||
// this would go in pworld
|
||||
let subChunkMissHashes: any[] = [];
|
||||
let sentMiss = false;
|
||||
let gotMiss = false;
|
||||
let lostSubChunks = 0,
|
||||
foundSubChunks = 0;
|
||||
|
||||
const cachingEnabled = false;
|
||||
|
||||
//let points = []
|
||||
async function processLevelChunk(packet: any) {
|
||||
const cc = new Chunk({ x: packet.x, z: packet.z });
|
||||
if (!cachingEnabled) {
|
||||
await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count);
|
||||
} else if (cachingEnabled) {
|
||||
const misses = await cc.networkDecode(packet.blobs.hashes, blobStore, packet.payload);
|
||||
if (!packet.blobs.hashes.length) return; // no blobs
|
||||
|
||||
bot._client.queue('client_cache_blob_status', {
|
||||
misses: misses.length,
|
||||
haves: 0,
|
||||
have: [],
|
||||
missing: misses,
|
||||
});
|
||||
|
||||
if (packet.sub_chunk_count < 0) {
|
||||
// 1.18+
|
||||
for (const miss of misses) blobStore.addPending(miss, new BlobEntry({ type: BlobType.Biomes, x: packet.x, z: packet.z }));
|
||||
} else {
|
||||
// 1.17-
|
||||
const lastBlob = packet.blobs.hashes[packet.blobs.hashes.length - 1];
|
||||
for (const miss of misses) {
|
||||
blobStore.addPending(
|
||||
miss,
|
||||
new BlobEntry({
|
||||
type: miss === lastBlob ? BlobType.Biomes : BlobType.ChunkSection,
|
||||
x: packet.x,
|
||||
z: packet.z,
|
||||
})
|
||||
);
|
||||
}
|
||||
sentMiss = true;
|
||||
}
|
||||
|
||||
blobStore.once(misses, async () => {
|
||||
// The things we were missing have now arrived
|
||||
const now = await cc.networkDecode(packet.blobs.hashes, blobStore, packet.payload);
|
||||
fs.writeFileSync(
|
||||
`fixtures/${version}/level_chunk CacheMissResponse ${packet.x},${packet.z}.json`,
|
||||
serialize({
|
||||
blobs: Object.fromEntries(packet.blobs.hashes.map((h: any) => [h.toString(), blobStore.get(h).buffer])),
|
||||
})
|
||||
);
|
||||
assert.strictEqual(now.length, 0);
|
||||
|
||||
bot._client.queue('client_cache_blob_status', {
|
||||
misses: 0,
|
||||
haves: packet.blobs.hashes.length,
|
||||
have: packet.blobs.hashes,
|
||||
missing: [],
|
||||
});
|
||||
|
||||
gotMiss = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (packet.sub_chunk_count < 0) {
|
||||
// 1.18.0+
|
||||
// 1.18+ handling, we need to send a SubChunk request
|
||||
const maxSubChunkCount = packet.highest_subchunk_count || 5; // field is set if sub_chunk_count=-2 (1.18.10+) meaning all air
|
||||
|
||||
function getChunkCoordinates(pos: Vec3) {
|
||||
let chunkX = Math.floor(pos.x / 16);
|
||||
let chunkZ = Math.floor(pos.z / 16);
|
||||
let subchunkY = Math.floor(pos.y / 16);
|
||||
return { chunkX: chunkX, chunkZ: chunkZ, subchunkY: subchunkY };
|
||||
}
|
||||
|
||||
if (bot.registry.version['>=']('1.18.11')) {
|
||||
// We can send the request in one big load!
|
||||
// let origin = getChunkCoordinates(bot.entity.position)
|
||||
// let x = packet.x <= 0 ? 255 + packet.x : packet.x
|
||||
// let z = packet.z <= 0 ? 255 + packet.z : packet.z
|
||||
|
||||
let requests: any[] = [];
|
||||
|
||||
let offset = cc.minCY;
|
||||
// load all height of the chunk
|
||||
for (let i = offset; i <= offset + maxSubChunkCount; i++) {
|
||||
requests.push({ dx: 0, dz: 0, dy: i });
|
||||
}
|
||||
if (requests.length > 0) {
|
||||
bot._client.queue('subchunk_request', {
|
||||
origin: { x: packet.x, z: packet.z, y: 0 },
|
||||
requests,
|
||||
dimension: 0,
|
||||
});
|
||||
}
|
||||
} else if (bot.registry.version['>=']('1.18')) {
|
||||
for (let i = 1; i < maxSubChunkCount; i++) {
|
||||
// Math.min(maxSubChunkCount, 5)
|
||||
bot._client.queue('subchunk_request', {
|
||||
x: packet.x,
|
||||
z: packet.z,
|
||||
y: 0,
|
||||
dimension: 0,
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addColumn({
|
||||
x: packet.x,
|
||||
z: packet.z,
|
||||
column: cc,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCached(cc: any, x: number, y: number, z: number, blobId: any, extraData: any) {
|
||||
const misses = await cc.networkDecodeSubChunk([blobId], blobStore, extraData);
|
||||
subChunkMissHashes.push(...misses);
|
||||
|
||||
for (const miss of misses) {
|
||||
blobStore.addPending(miss, new BlobEntry({ type: BlobType.ChunkSection, x, z, y }));
|
||||
}
|
||||
|
||||
if (subChunkMissHashes.length >= 10) {
|
||||
sentMiss = true;
|
||||
const r = {
|
||||
misses: subChunkMissHashes.length,
|
||||
haves: 0,
|
||||
have: [],
|
||||
missing: subChunkMissHashes,
|
||||
};
|
||||
|
||||
bot._client.queue('client_cache_blob_status', r);
|
||||
subChunkMissHashes = [];
|
||||
}
|
||||
|
||||
if (misses.length) {
|
||||
const [missed] = misses;
|
||||
// Once we get this blob, try again
|
||||
|
||||
blobStore.once([missed], async () => {
|
||||
gotMiss = true;
|
||||
fs.writeFileSync(
|
||||
`fixtures/${version}/subchunk CacheMissResponse ${x},${z},${y}.json`,
|
||||
serialize({
|
||||
blobs: Object.fromEntries([[missed.toString(), blobStore.get(missed).buffer]]),
|
||||
})
|
||||
);
|
||||
// Call this again, ignore the payload since that's already been decoded
|
||||
const misses = await cc.networkDecodeSubChunk([missed], blobStore);
|
||||
assert(!misses.length, 'Should not have missed anything');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function processSubChunk(packet: protocolTypes.packet_subchunk) {
|
||||
const pkt = packet as any;
|
||||
if (pkt.entries) {
|
||||
// 1.18.10+ handling
|
||||
for (const entry of pkt.entries) {
|
||||
const x = pkt.origin.x + entry.dx;
|
||||
const y = pkt.origin.y + Buffer.from([entry.dy]).readInt8(0);
|
||||
const z = pkt.origin.z + entry.dz;
|
||||
|
||||
const cc = bot.world.getColumn(x, z) as BedrockChunk;
|
||||
|
||||
if (entry.result === 'success') {
|
||||
foundSubChunks++;
|
||||
if (pkt.cache_enabled) {
|
||||
await loadCached(cc, x, y, z, entry.blob_id, entry.payload);
|
||||
} else {
|
||||
try {
|
||||
await cc.networkDecodeSubChunkNoCache(y, entry.payload);
|
||||
bot.world.emit('chunkColumnLoad', new Vec3(x, y, z));
|
||||
} catch (e) {
|
||||
bot.logger.error(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lostSubChunks++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pkt.request_result !== 'success') {
|
||||
lostSubChunks++;
|
||||
return;
|
||||
}
|
||||
foundSubChunks++;
|
||||
const cc = bot.world.getColumn(pkt.x, pkt.z) as BedrockChunk;
|
||||
if (pkt.cache_enabled) {
|
||||
await loadCached(cc, pkt.x, pkt.y, pkt.z, pkt.blob_id, pkt.data);
|
||||
} else {
|
||||
await cc.networkDecodeSubChunkNoCache(pkt.y, pkt.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processCacheMiss(packet: any) {
|
||||
const acks: any[] = [];
|
||||
for (const { hash, payload } of packet.blobs) {
|
||||
const name = hash.toString();
|
||||
blobStore.updatePending(name, { buffer: payload });
|
||||
acks.push(hash);
|
||||
}
|
||||
|
||||
// Send back an ACK
|
||||
bot._client.queue('client_cache_blob_status', {
|
||||
misses: 0,
|
||||
haves: acks.length,
|
||||
have: [],
|
||||
missing: acks,
|
||||
});
|
||||
}
|
||||
|
||||
bot._client.on('level_chunk', processLevelChunk);
|
||||
bot._client.on('subchunk', (sc) => processSubChunk(sc).catch(console.error));
|
||||
bot._client.on('client_cache_miss_response', processCacheMiss);
|
||||
|
||||
// fs.mkdirSync(`fixtures/${version}/pchunk`, { recursive: true })
|
||||
// bot._client.on('packet', ({ data: { name, params }, fullBuffer }) => {
|
||||
// if (name === 'level_chunk') {
|
||||
// fs.writeFileSync(`fixtures/${version}/level_chunk ${cachingEnabled ? 'cached' : ''} ${params.x},${params.z}.json`, serialize(params))
|
||||
// } else if (name === 'subchunk') {
|
||||
// if (params.origin) {
|
||||
// fs.writeFileSync(`fixtures/${version}/subchunk ${cachingEnabled ? 'cached' : ''} ${params.origin.x},${params.origin.z},${params.origin.y}.json`, serialize(params))
|
||||
// } else {
|
||||
// fs.writeFileSync(`fixtures/${version}/subchunk ${cachingEnabled ? 'cached' : ''} ${params.x},${params.z},${params.y}.json`, serialize(params))
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
function getMatchingFunction(matching: any) {
|
||||
if (typeof matching !== 'function') {
|
||||
if (!Array.isArray(matching)) {
|
||||
matching = [matching];
|
||||
}
|
||||
return isMatchingType;
|
||||
}
|
||||
return matching;
|
||||
|
||||
function isMatchingType(block: any) {
|
||||
return block === null ? false : matching.indexOf(block.type) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
function isBlockInSection(section: any, matcher: any) {
|
||||
if (!section) return false; // section is empty, skip it (yay!)
|
||||
// If the chunk use a palette we can speed up the search by first
|
||||
// checking the palette which usually contains less than 20 ids
|
||||
// vs checking the 4096 block of the section. If we don't have a
|
||||
// match in the palette, we can skip this section.
|
||||
if (section.palette) {
|
||||
for (const stateId of section.palette[0]) {
|
||||
if (matcher(Block.fromStateId(stateId.stateId, 0))) {
|
||||
return true; // the block is in the palette
|
||||
}
|
||||
}
|
||||
return false; // skip
|
||||
}
|
||||
return true; // global palette, the block might be in there
|
||||
}
|
||||
|
||||
function getFullMatchingFunction(matcher: any, useExtraInfo: any) {
|
||||
if (typeof useExtraInfo === 'boolean') {
|
||||
return fullSearchMatcher;
|
||||
}
|
||||
|
||||
return nonFullSearchMatcher;
|
||||
|
||||
function nonFullSearchMatcher(point: Vec3) {
|
||||
const block = blockAt(point, true);
|
||||
return matcher(block) && useExtraInfo(block);
|
||||
}
|
||||
|
||||
function fullSearchMatcher(point: Vec3) {
|
||||
return matcher(bot.blockAt(point, useExtraInfo));
|
||||
}
|
||||
}
|
||||
|
||||
bot.findBlocks = (options: any) => {
|
||||
const matcher = getMatchingFunction(options.matching);
|
||||
const point = (options.point || bot.entity.position).floored();
|
||||
const maxDistance = options.maxDistance || 16;
|
||||
const count = options.count || 1;
|
||||
const useExtraInfo = options.useExtraInfo || false;
|
||||
const fullMatcher = getFullMatchingFunction(matcher, useExtraInfo);
|
||||
const start = new Vec3(Math.floor(point.x / 16), Math.floor(point.y / 16), Math.floor(point.z / 16));
|
||||
const it = new OctahedronIterator(start, Math.ceil((maxDistance + 8) / 16));
|
||||
// the octahedron iterator can sometime go through the same section again
|
||||
// we use a set to keep track of visited sections
|
||||
const visitedSections = new Set<string>();
|
||||
|
||||
let blocks: Vec3[] = [];
|
||||
let startedLayer = 0;
|
||||
let next = start;
|
||||
while (next) {
|
||||
const column = bot.world.getColumn(next.x, next.z) as any;
|
||||
const sectionY = next.y + Math.abs(bot.game.minY >> 4);
|
||||
const totalSections = bot.game.height >> 4;
|
||||
if (sectionY >= 0 && sectionY < totalSections && column && !visitedSections.has(next.toString())) {
|
||||
const section = column.sections[sectionY];
|
||||
if (useExtraInfo === true || isBlockInSection(section, matcher)) {
|
||||
const begin = new Vec3(next.x * 16, sectionY * 16 + bot.game.minY, next.z * 16);
|
||||
const cursor = begin.clone();
|
||||
const end = cursor.offset(16, 16, 16);
|
||||
for (cursor.x = begin.x; cursor.x < end.x; cursor.x++) {
|
||||
for (cursor.y = begin.y; cursor.y < end.y; cursor.y++) {
|
||||
for (cursor.z = begin.z; cursor.z < end.z; cursor.z++) {
|
||||
if (fullMatcher(cursor) && cursor.distanceTo(point) <= maxDistance) blocks.push(cursor.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visitedSections.add(next.toString());
|
||||
}
|
||||
// If we started a layer, we have to finish it otherwise we might miss closer blocks
|
||||
if (startedLayer !== it.apothem && blocks.length >= count) {
|
||||
break;
|
||||
}
|
||||
startedLayer = it.apothem;
|
||||
next = it.next();
|
||||
}
|
||||
blocks.sort((a, b) => {
|
||||
return a.distanceTo(point) - b.distanceTo(point);
|
||||
});
|
||||
// We found more blocks than needed, shorten the array to not confuse people
|
||||
if (blocks.length > count) {
|
||||
blocks = blocks.slice(0, count);
|
||||
}
|
||||
return blocks;
|
||||
};
|
||||
|
||||
function findBlock(options: any) {
|
||||
const blocks = bot.findBlocks(options);
|
||||
if (blocks.length === 0) return null;
|
||||
return bot.blockAt(blocks[0]);
|
||||
}
|
||||
|
||||
function blockAt(absolutePoint: Vec3, extraInfos = true) {
|
||||
const block = bot.world.getBlock(absolutePoint);
|
||||
// null block means chunk not loaded
|
||||
if (!block) return null;
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
// if passed in block is within line of sight to the bot, returns true
|
||||
// also works on anything with a position value
|
||||
function canSeeBlock(block: any) {
|
||||
const headPos = bot.entity.position.offset(0, bot.entity.height, 0);
|
||||
const range = headPos.distanceTo(block.position);
|
||||
const dir = block.position.offset(0.5, 0.5, 0.5).minus(headPos);
|
||||
const match = (inputBlock: any, iter: any) => {
|
||||
const intersect = iter.intersect(inputBlock.shapes, inputBlock.position);
|
||||
if (intersect) {
|
||||
return true;
|
||||
}
|
||||
return block.position.equals(inputBlock.position);
|
||||
};
|
||||
const blockAtCursor = bot.world.raycast(headPos, dir.normalize(), range, match as any);
|
||||
return blockAtCursor && blockAtCursor.position.equals(block.position);
|
||||
}
|
||||
|
||||
function updateBlockEntityData(point: Vec3, nbt: any) {
|
||||
const column = bot.world.getColumnAt(point) as any;
|
||||
if (!column) return;
|
||||
|
||||
if (nbt) {
|
||||
column.setBlockEntity(posInChunk(point), nbt);
|
||||
} else {
|
||||
const debug = bot.world.getBlock(point);
|
||||
if (debug.entity) bot.logger.debug('Removed Entity Data');
|
||||
|
||||
column.removeBlockEntity(posInChunk(point));
|
||||
}
|
||||
|
||||
const block = bot.world.getBlock(point);
|
||||
bot.world.emit('blockUpdate', block, block);
|
||||
}
|
||||
|
||||
function posInChunk(pos: Vec3) {
|
||||
return new Vec3(Math.floor(pos.x) & 15, Math.floor(pos.y), Math.floor(pos.z) & 15);
|
||||
}
|
||||
|
||||
function updateBlockState(point: Vec3, block_runtime_id: number) {
|
||||
const registry = bot.registry as any;
|
||||
if (!registry.blocksByRuntimeId) {
|
||||
bot.logger.warn('registry.blocksByRuntimeId is not available');
|
||||
return;
|
||||
}
|
||||
let registryBlock = registry.blocksByRuntimeId[block_runtime_id];
|
||||
if (!registryBlock) registryBlock = bot.registry.blocksByName['stone'];
|
||||
|
||||
// Create a new block object with stateId set to the runtime ID
|
||||
// This is needed for openBlock() to send the correct block_runtime_id
|
||||
const block = {
|
||||
...registryBlock,
|
||||
stateId: block_runtime_id,
|
||||
};
|
||||
|
||||
const oldBlock = bot.world.getBlock(point);
|
||||
|
||||
if (oldBlock && oldBlock.stateId === block_runtime_id) {
|
||||
bot.world.emit('blockUpdate', oldBlock, oldBlock);
|
||||
return;
|
||||
}
|
||||
|
||||
//Rather use bot.registry.blocksByStateId[stateId]?
|
||||
if (oldBlock)
|
||||
if (oldBlock.type !== block.type) {
|
||||
updateBlockEntityData(point, null);
|
||||
}
|
||||
|
||||
bot.world.setBlock(point, block);
|
||||
|
||||
// Emit position-specific blockUpdate event for digging plugin
|
||||
const newBlock = blockAt(point);
|
||||
if (newBlock !== null) {
|
||||
bot.world.emit(`blockUpdate:${point}`, oldBlock, newBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// bot._client.on('map_chunk', (packet) => {
|
||||
// addColumn({
|
||||
// x: packet.x,
|
||||
// z: packet.z,
|
||||
// bitMap: packet.bitMap,
|
||||
// heightmaps: packet.heightmaps,
|
||||
// biomes: packet.biomes,
|
||||
// skyLightSent: bot.game.dimension === 'minecraft:overworld',
|
||||
// groundUp: packet.groundUp,
|
||||
// data: packet.chunkData,
|
||||
// trustEdges: packet.trustEdges,
|
||||
// skyLightMask: packet.skyLightMask,
|
||||
// blockLightMask: packet.blockLightMask,
|
||||
// emptySkyLightMask: packet.emptySkyLightMask,
|
||||
// emptyBlockLightMask: packet.emptyBlockLightMask,
|
||||
// skyLight: packet.skyLight,
|
||||
// blockLight: packet.blockLight
|
||||
// })
|
||||
//
|
||||
// if (typeof packet.blockEntities !== 'undefined') {
|
||||
// const column = bot.world.getColumn(packet.x, packet.z)
|
||||
// if (!column) {
|
||||
// if (!hideErrors) console.warn('Ignoring block entities as chunk failed to load at', packet.x, packet.z)
|
||||
// return
|
||||
// }
|
||||
// for (const blockEntity of packet.blockEntities) {
|
||||
// if (blockEntity.x !== undefined) { // 1.17+
|
||||
// column.setBlockEntity(blockEntity, blockEntity.nbtData)
|
||||
// } else {
|
||||
// const pos = new Vec3(blockEntity.value.x.value & 0xf, blockEntity.value.y.value, blockEntity.value.z.value & 0xf)
|
||||
// column.setBlockEntity(pos, blockEntity)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
// bot._client.on('map_chunk_bulk', (packet) => {
|
||||
// let offset = 0
|
||||
// let meta
|
||||
// let i
|
||||
// let size
|
||||
// for (i = 0; i < packet.meta.length; ++i) {
|
||||
// meta = packet.meta[i]
|
||||
// size = (8192 + (packet.skyLightSent ? 2048 : 0)) *
|
||||
// onesInShort(meta.bitMap) + // block ids
|
||||
// 2048 * onesInShort(meta.bitMap) + // (two bytes per block id)
|
||||
// 256 // biomes
|
||||
// addColumn({
|
||||
// x: meta.x,
|
||||
// z: meta.z,
|
||||
// bitMap: meta.bitMap,
|
||||
// heightmaps: packet.heightmaps,
|
||||
// skyLightSent: packet.skyLightSent,
|
||||
// groundUp: true,
|
||||
// data: packet.data.slice(offset, offset + size)
|
||||
// })
|
||||
// offset += size
|
||||
// }
|
||||
//
|
||||
// assert.strictEqual(offset, packet.data.length)
|
||||
// })
|
||||
|
||||
bot._client.on('update_subchunk_blocks', (packet) => {
|
||||
// Packet Update Subchunk Blocks
|
||||
// multi block change
|
||||
// EXTRA NOT IMPLEMENTED (WATERLOGGED)
|
||||
for (let i = 0; i < packet.blocks.length; i++) {
|
||||
const record = packet.blocks[i];
|
||||
const pt = new Vec3(record.position.x, record.position.y, record.position.z);
|
||||
updateBlockState(pt, record.runtime_id);
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('update_block', (packet) => {
|
||||
const pt = new Vec3(packet.position.x, packet.position.y, packet.position.z) as any;
|
||||
pt.l = packet.layer;
|
||||
updateBlockState(pt, packet.block_runtime_id);
|
||||
});
|
||||
|
||||
bot._client.on('block_entity_data', (packet) => {
|
||||
const pt = new Vec3(packet.position.x, packet.position.y, packet.position.z);
|
||||
updateBlockEntityData(pt, packet.nbt);
|
||||
});
|
||||
|
||||
// bot._client.on('explosion', (packet) => {
|
||||
// // explosion
|
||||
// const p = new Vec3(packet.x, packet.y, packet.z)
|
||||
// packet.affectedBlockOffsets.forEach((offset) => {
|
||||
// const pt = p.offset(offset.x, offset.y, offset.z)
|
||||
// updateBlockState(pt, 0)
|
||||
// })
|
||||
// }) // NO EXP PACKET ON BEDROCK
|
||||
|
||||
// if we get a respawn packet and the dimension is changed,
|
||||
// unload all chunks from memory.
|
||||
let dimension: any;
|
||||
let worldName: any;
|
||||
function dimensionToFolderName(dimension: any) {
|
||||
if (bot.supportFeature('dimensionIsAnInt')) {
|
||||
return dimensionNames[dimension];
|
||||
} else if (bot.supportFeature('dimensionIsAString') || bot.supportFeature('dimensionIsAWorld')) {
|
||||
return dimension;
|
||||
}
|
||||
}
|
||||
|
||||
async function switchWorld() {
|
||||
if (bot.world) {
|
||||
if (storageBuilder) {
|
||||
await bot.world.async.waitSaving();
|
||||
}
|
||||
|
||||
for (const [name, listener] of Object.entries((bot as any)._events) as [string, any][]) {
|
||||
if (name.startsWith('blockUpdate:')) {
|
||||
bot.emit(name as any, null, null);
|
||||
bot.off(name as any, listener as any);
|
||||
}
|
||||
}
|
||||
|
||||
const worldAsync = bot.world.async as any;
|
||||
for (const [x, z] of Object.keys(worldAsync.columns).map((key) => key.split(',').map((x) => parseInt(x, 10)))) {
|
||||
bot.world.unloadColumn(x, z);
|
||||
}
|
||||
|
||||
if (storageBuilder) {
|
||||
worldAsync.storageProvider = storageBuilder({
|
||||
version: bot.version,
|
||||
worldName: dimensionToFolderName(dimension),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
bot.world = new World(null, storageBuilder ? storageBuilder({ version: bot.version, worldName: dimensionToFolderName(dimension) }) : null).sync;
|
||||
startListenerProxy();
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.on('start_game', (packet) => {
|
||||
if (bot.supportFeature('dimensionIsAnInt')) {
|
||||
dimension = packet.dimension;
|
||||
} else {
|
||||
dimension = packet.dimension;
|
||||
worldName = packet.world_name;
|
||||
}
|
||||
switchWorld();
|
||||
});
|
||||
|
||||
bot._client.on('respawn', (packet) => {
|
||||
const pkt = packet as any;
|
||||
if (bot.supportFeature('dimensionIsAnInt')) {
|
||||
// <=1.15.2
|
||||
if (dimension === pkt.dimension) return;
|
||||
dimension = pkt.dimension;
|
||||
} else {
|
||||
// >= 1.15.2
|
||||
if (dimension === pkt.dimension) return;
|
||||
if (worldName === pkt.world_name && pkt.copyMetadata === true) return; // don't unload chunks if in same world and metaData is true
|
||||
// Metadata is true when switching dimensions however, then the world name is different packet.copyMetadata unavaliable for bedrock!!!
|
||||
dimension = pkt.dimension;
|
||||
worldName = pkt.world_name;
|
||||
}
|
||||
switchWorld();
|
||||
});
|
||||
|
||||
let listener: any;
|
||||
let listenerRemove: any;
|
||||
function startListenerProxy() {
|
||||
if (listener) {
|
||||
// custom forwarder for custom events
|
||||
bot.off('newListener', listener);
|
||||
bot.off('removeListener', listenerRemove);
|
||||
}
|
||||
// standardized forwarding
|
||||
const forwardedEvents = ['blockUpdate', 'chunkColumnLoad', 'chunkColumnUnload'];
|
||||
|
||||
for (const event of forwardedEvents) {
|
||||
bot.world.on(event, (...args: any[]) => (bot.emit as any)(event, ...args));
|
||||
}
|
||||
const blockUpdateRegex = /blockUpdate:\(-?\d+, -?\d+, -?\d+\)/;
|
||||
listener = (event: string, listener: any) => {
|
||||
if (blockUpdateRegex.test(event)) {
|
||||
bot.world.on(event, listener);
|
||||
}
|
||||
};
|
||||
listenerRemove = (event: string, listener: any) => {
|
||||
if (blockUpdateRegex.test(event)) {
|
||||
bot.world.off(event, listener);
|
||||
}
|
||||
};
|
||||
bot.on('newListener', listener);
|
||||
bot.on('removeListener', listenerRemove);
|
||||
}
|
||||
|
||||
bot.findBlock = findBlock;
|
||||
bot.canSeeBlock = canSeeBlock;
|
||||
bot.blockAt = blockAt;
|
||||
bot._updateBlockState = updateBlockState;
|
||||
bot.waitForChunksToLoad = waitForChunksToLoad;
|
||||
}
|
||||
|
||||
// function onesInShort (n) {
|
||||
// n = n & 0xffff
|
||||
// let count = 0
|
||||
// for (let i = 0; i < 16; ++i) {
|
||||
// count = ((1 << i) & n) ? count + 1 : count
|
||||
// }
|
||||
// return count
|
||||
// }
|
||||
184
bridge/lib/mineflayer/lib/bedrockPlugins/book.mts
Normal file
184
bridge/lib/mineflayer/lib/bedrockPlugins/book.mts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* Bedrock Edition book plugin
|
||||
*
|
||||
* Provides book writing and signing functionality for Bedrock.
|
||||
*
|
||||
* Protocol differences from Java:
|
||||
* - Java: Uses `edit_book` packet with NBT data
|
||||
* - Bedrock: Uses `book_edit` packet with action-based editing
|
||||
* - action: "replace_page" - Update page content
|
||||
* - action: "add_page" - Add new page (not commonly used, replace_page auto-creates)
|
||||
* - action: "delete_page" - Remove a page
|
||||
* - action: "swap_pages" - Swap two pages
|
||||
* - action: "sign" - Sign and finalize book (becomes written_book)
|
||||
*
|
||||
* Book editing flow:
|
||||
* 1. Hold writable_book (book and quill)
|
||||
* 2. Right-click to open (inventory_transaction item_use click_air)
|
||||
* 3. C→S: book_edit {action: "replace_page", slot, page, text}
|
||||
* 4. C→S: book_edit {action: "sign", slot, title, author, xuid}
|
||||
* 5. Book becomes written_book (no server confirmation packet)
|
||||
*
|
||||
* API (matches Java):
|
||||
* - bot.writeBook(slot, pages) - Write pages to book and quill
|
||||
* - bot.signBook(slot, pages, author, title) - Write and sign book
|
||||
*/
|
||||
|
||||
import type { Bot } from '../..'
|
||||
import * as assert from 'assert'
|
||||
|
||||
export default function inject(bot: Bot) {
|
||||
/**
|
||||
* Send a book_edit packet to write page content
|
||||
*/
|
||||
function sendBookEdit(
|
||||
slot: number,
|
||||
action: string,
|
||||
options: {
|
||||
page?: number;
|
||||
text?: string;
|
||||
secondaryPage?: number;
|
||||
title?: string;
|
||||
author?: string;
|
||||
} = {},
|
||||
) {
|
||||
const packet: Record<string, unknown> = {
|
||||
type: action, // Bedrock uses 'type' field for action
|
||||
slot,
|
||||
page_number: options.page ?? 0,
|
||||
secondary_page_number: options.secondaryPage ?? 0,
|
||||
text: options.text ?? '',
|
||||
photo_name: '',
|
||||
title: options.title ?? '',
|
||||
author: options.author ?? '',
|
||||
xuid: '', // XUID is handled by server based on client
|
||||
};
|
||||
|
||||
bot._client.write('book_edit', packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to a book and quill without signing
|
||||
*
|
||||
* @param slot - Inventory slot containing the book (0-44)
|
||||
* @param pages - Array of page contents (strings)
|
||||
*/
|
||||
async function writeBook(slot: number, pages: string[]): Promise<void> {
|
||||
assert.ok(slot >= 0 && slot <= 44, 'slot out of inventory range');
|
||||
|
||||
const book = bot.inventory.slots[slot];
|
||||
const writableBookId = bot.registry.itemsByName.writable_book?.id;
|
||||
|
||||
assert.ok(book && writableBookId && book.type === writableBookId, `no book found in slot ${slot}`);
|
||||
|
||||
const quickBarSlot = bot.quickBarSlot;
|
||||
const moveToQuickBar = slot < 36;
|
||||
|
||||
// Move book to hotbar if needed
|
||||
if (moveToQuickBar) {
|
||||
await bot.moveSlotItem(slot, 36);
|
||||
}
|
||||
|
||||
// Select the book slot
|
||||
bot.setQuickBarSlot(moveToQuickBar ? 0 : slot - 36);
|
||||
|
||||
// Write each page
|
||||
const hotbarSlot = moveToQuickBar ? 0 : slot - 36;
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
sendBookEdit(hotbarSlot, 'replace_page', {
|
||||
page: i,
|
||||
text: pages[i],
|
||||
});
|
||||
}
|
||||
|
||||
// Small delay to let server process
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Restore original quickbar slot
|
||||
bot.setQuickBarSlot(quickBarSlot);
|
||||
|
||||
// Move book back if we moved it
|
||||
if (moveToQuickBar) {
|
||||
await bot.moveSlotItem(36, slot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to a book and sign it
|
||||
*
|
||||
* @param slot - Inventory slot containing the book (0-44)
|
||||
* @param pages - Array of page contents (strings)
|
||||
* @param author - Author name for the signed book
|
||||
* @param title - Title for the signed book
|
||||
*/
|
||||
async function signBook(slot: number, pages: string[], author: string, title: string): Promise<void> {
|
||||
assert.ok(slot >= 0 && slot <= 44, 'slot out of inventory range');
|
||||
|
||||
const book = bot.inventory.slots[slot];
|
||||
const writableBookId = bot.registry.itemsByName.writable_book?.id;
|
||||
|
||||
assert.ok(book && writableBookId && book.type === writableBookId, `no book found in slot ${slot}`);
|
||||
|
||||
const quickBarSlot = bot.quickBarSlot;
|
||||
const moveToQuickBar = slot < 36;
|
||||
|
||||
// Move book to hotbar if needed
|
||||
if (moveToQuickBar) {
|
||||
await bot.moveSlotItem(slot, 36);
|
||||
}
|
||||
|
||||
// Select the book slot
|
||||
bot.setQuickBarSlot(moveToQuickBar ? 0 : slot - 36);
|
||||
|
||||
// Write each page first
|
||||
const hotbarSlot = moveToQuickBar ? 0 : slot - 36;
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
sendBookEdit(hotbarSlot, 'replace_page', {
|
||||
page: i,
|
||||
text: pages[i],
|
||||
});
|
||||
}
|
||||
|
||||
// Small delay before signing
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Sign the book
|
||||
sendBookEdit(hotbarSlot, 'sign', {
|
||||
title,
|
||||
author,
|
||||
});
|
||||
|
||||
// Wait for inventory update (book becomes written_book)
|
||||
const inventorySlot = moveToQuickBar ? 36 : slot;
|
||||
await new Promise<void>((resolve) => {
|
||||
const handler = (oldItem: unknown, newItem: unknown) => {
|
||||
// Book was signed when it changes type
|
||||
const newItemTyped = newItem as { type?: number } | null;
|
||||
const writtenBookId = bot.registry.itemsByName.written_book?.id;
|
||||
if (newItemTyped && writtenBookId && newItemTyped.type === writtenBookId) {
|
||||
bot.inventory.off(`updateSlot:${inventorySlot}`, handler);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
bot.inventory.on(`updateSlot:${inventorySlot}`, handler);
|
||||
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
bot.inventory.off(`updateSlot:${inventorySlot}`, handler);
|
||||
resolve(); // Resolve anyway, server might not send update
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Restore original quickbar slot
|
||||
bot.setQuickBarSlot(quickBarSlot);
|
||||
|
||||
// Move book back if we moved it
|
||||
if (moveToQuickBar) {
|
||||
await bot.moveSlotItem(36, slot);
|
||||
}
|
||||
}
|
||||
|
||||
// Expose API on bot object
|
||||
bot.writeBook = writeBook;
|
||||
bot.signBook = signBook;
|
||||
}
|
||||
61
bridge/lib/mineflayer/lib/bedrockPlugins/bossbar.mts
Normal file
61
bridge/lib/mineflayer/lib/bedrockPlugins/bossbar.mts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const BossBar = require('../bossbar')(bot.registry);
|
||||
const bars: Record<string, any> = {};
|
||||
|
||||
bot.on('entityGone', (entity) => {
|
||||
if (!entity) return;
|
||||
if (!(entity.unique_id in bars)) return;
|
||||
bot.emit('bossBarDeleted', bars[entity.unique_id]);
|
||||
delete bars[entity.unique_id];
|
||||
});
|
||||
|
||||
bot._client.on('boss_event', (packet) => {
|
||||
if (packet.type === 'show_bar') {
|
||||
let flags = 0;
|
||||
|
||||
if (packet.screen_darkening === 1) {
|
||||
flags |= 0x1;
|
||||
}
|
||||
|
||||
bars[packet.boss_entity_id] = new BossBar(packet.boss_entity_id, packet.title, getBossHealth(packet.boss_entity_id), 4, packet.color, flags);
|
||||
|
||||
bot._client.write('boss_event', {
|
||||
boss_entity_id: packet.boss_entity_id,
|
||||
type: 'register_player',
|
||||
player_id: bot.entity.unique_id,
|
||||
});
|
||||
|
||||
bot.emit('bossBarCreated', bars[packet.boss_entity_id]);
|
||||
} else if (packet.type === 'set_bar_progress') {
|
||||
if (!(packet.boss_entity_id in bars)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bars[packet.boss_entity_id].health = getBossHealth(packet.boss_entity_id);
|
||||
|
||||
bot.emit('bossBarUpdated', bars[packet.boss_entity_id]);
|
||||
}
|
||||
});
|
||||
|
||||
function getBossHealth(boss_id: string | number) {
|
||||
let boss_entity = (bot as any).fetchEntity(boss_id);
|
||||
|
||||
let boss_health = 0;
|
||||
|
||||
if ('minecraft:health' in boss_entity.attributes) {
|
||||
boss_health = boss_entity.attributes['minecraft:health'].value;
|
||||
}
|
||||
|
||||
return boss_health;
|
||||
}
|
||||
|
||||
Object.defineProperty(bot, 'bossBars', {
|
||||
get() {
|
||||
return Object.values(bars);
|
||||
},
|
||||
});
|
||||
}
|
||||
17
bridge/lib/mineflayer/lib/bedrockPlugins/breath.mts
Normal file
17
bridge/lib/mineflayer/lib/bedrockPlugins/breath.mts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot._client.on('set_entity_data', (packet) => {
|
||||
if (!bot?.entity?.id === packet?.runtime_entity_id) return;
|
||||
if (packet?.metadata[1]?.key === 'air') {
|
||||
if (!packet?.metadata[1]?.value) return;
|
||||
bot.oxygenLevel = Math.round(packet.metadata[1].value / 15);
|
||||
bot.emit('breath');
|
||||
}
|
||||
if (packet?.metadata[0]?.key === 'air') {
|
||||
if (!packet?.metadata[0]?.value) return;
|
||||
bot.oxygenLevel = Math.round(packet.metadata[0].value / 15);
|
||||
bot.emit('breath');
|
||||
}
|
||||
});
|
||||
}
|
||||
299
bridge/lib/mineflayer/lib/bedrockPlugins/chat.mts
Normal file
299
bridge/lib/mineflayer/lib/bedrockPlugins/chat.mts
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const USERNAME_REGEX = '(?:\\(.{1,15}\\)|\\[.{1,15}\\]|.){0,5}?(\\w+)';
|
||||
const LEGACY_VANILLA_CHAT_REGEX = new RegExp(`^${USERNAME_REGEX}\\s?[>:\\-»\\]\\)~]+\\s(.*)$`);
|
||||
|
||||
interface ChatPattern {
|
||||
name: string;
|
||||
patterns: RegExp[];
|
||||
position: number;
|
||||
matches: string[];
|
||||
messages: any[];
|
||||
deprecated?: boolean;
|
||||
repeat: boolean;
|
||||
parse: boolean;
|
||||
}
|
||||
|
||||
interface ChatOptions {
|
||||
chatLengthLimit?: number;
|
||||
defaultChatPatterns?: boolean;
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot, options: ChatOptions = {}) {
|
||||
const CHAT_LENGTH_LIMIT = options.chatLengthLimit ?? (bot.supportFeature('lessCharsInChat') ? 100 : 256);
|
||||
const defaultChatPatterns = options.defaultChatPatterns ?? true;
|
||||
|
||||
const ChatMessage = require('prismarine-chat')(bot.registry);
|
||||
|
||||
const _patterns: Record<number, ChatPattern | undefined> = {};
|
||||
let _length = 0;
|
||||
|
||||
// deprecated
|
||||
bot.chatAddPattern = (patternValue: RegExp, typeValue: string) => {
|
||||
return bot.addChatPattern(typeValue, patternValue, { deprecated: true });
|
||||
};
|
||||
|
||||
bot.addChatPatternSet = (name: string, patterns: RegExp[], opts: { repeat?: boolean; parse?: boolean } = {}) => {
|
||||
if (!patterns.every((p) => p instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp');
|
||||
const { repeat = true, parse = false } = opts;
|
||||
_patterns[_length++] = {
|
||||
name,
|
||||
patterns,
|
||||
position: 0,
|
||||
matches: [],
|
||||
messages: [],
|
||||
repeat,
|
||||
parse,
|
||||
};
|
||||
return _length;
|
||||
};
|
||||
|
||||
bot.addChatPattern = (name: string, pattern: RegExp, opts: { repeat?: boolean; deprecated?: boolean; parse?: boolean } = {}) => {
|
||||
if (!(pattern instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp');
|
||||
const { repeat = true, deprecated = false, parse = false } = opts;
|
||||
_patterns[_length] = {
|
||||
name,
|
||||
patterns: [pattern],
|
||||
position: 0,
|
||||
matches: [],
|
||||
messages: [],
|
||||
deprecated,
|
||||
repeat,
|
||||
parse,
|
||||
};
|
||||
return _length++;
|
||||
};
|
||||
|
||||
bot.removeChatPattern = (name: string | number) => {
|
||||
if (typeof name === 'number') {
|
||||
_patterns[name] = undefined;
|
||||
} else {
|
||||
const matchingPatterns = Object.entries(_patterns).filter((pattern) => pattern[1]?.name === name);
|
||||
matchingPatterns.forEach(([indexString]) => {
|
||||
_patterns[+indexString] = undefined;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function findMatchingPatterns(msg: string): number[] {
|
||||
const found: number[] = [];
|
||||
for (const [indexString, pattern] of Object.entries(_patterns)) {
|
||||
if (!pattern) continue;
|
||||
const { position, patterns } = pattern;
|
||||
if (patterns[position].test(msg)) {
|
||||
found.push(+indexString);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
bot.on('messagestr', (msg: string, _: any, originalMsg: any) => {
|
||||
const foundPatterns = findMatchingPatterns(msg);
|
||||
|
||||
for (const ix of foundPatterns) {
|
||||
const pattern = _patterns[ix];
|
||||
if (!pattern) continue;
|
||||
|
||||
pattern.matches.push(msg);
|
||||
pattern.messages.push(originalMsg);
|
||||
pattern.position++;
|
||||
|
||||
if (pattern.deprecated) {
|
||||
const matchResult = pattern.matches[0].match(pattern.patterns[0]);
|
||||
if (matchResult) {
|
||||
const [, ...matches] = matchResult;
|
||||
(bot.emit as any)(pattern.name, ...matches, pattern.messages[0]?.translate, ...pattern.messages);
|
||||
}
|
||||
pattern.messages = [];
|
||||
} else {
|
||||
if (pattern.patterns.length > pattern.matches.length) continue;
|
||||
if (pattern.parse) {
|
||||
const matches = pattern.patterns.map((p, i) => {
|
||||
const matchResult = pattern.matches[i].match(p);
|
||||
if (matchResult) {
|
||||
const [, ...m] = matchResult;
|
||||
return m;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
bot.emit(`chat:${pattern.name}` as `chat:${string}`, matches);
|
||||
} else {
|
||||
(bot.emit as any)(`chat:${pattern.name}`, pattern.matches);
|
||||
}
|
||||
}
|
||||
|
||||
if (_patterns[ix]?.repeat) {
|
||||
_patterns[ix]!.position = 0;
|
||||
_patterns[ix]!.matches = [];
|
||||
} else {
|
||||
_patterns[ix] = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addDefaultPatterns();
|
||||
|
||||
// Handle incoming text packets
|
||||
bot._client.on('text', (data) => {
|
||||
let msg: any;
|
||||
|
||||
if (data.type === 'translation') {
|
||||
// Handle translation messages with parameters
|
||||
const params: string[] = [];
|
||||
if (data.parameters) {
|
||||
for (const param of data.parameters) {
|
||||
if (typeof param === 'string' && param.startsWith('%') && bot.registry.language[param.substring(1)] != null) {
|
||||
params.push(bot.registry.language[param.substring(1)]);
|
||||
} else {
|
||||
params.push(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
msg = new ChatMessage({ translate: data.message, with: params });
|
||||
} else if (['json', 'json_whisper', 'json_announcement'].includes(data.type)) {
|
||||
// Handle JSON/tellraw messages (Bedrock uses rawtext format)
|
||||
try {
|
||||
const jsonContent = (data.message || '').trim();
|
||||
if (jsonContent) {
|
||||
const parsed = typeof jsonContent === 'string' ? JSON.parse(jsonContent) : jsonContent;
|
||||
// Convert Bedrock rawtext format to Java-compatible format
|
||||
if (parsed.rawtext && Array.isArray(parsed.rawtext)) {
|
||||
// Convert rawtext array to extra array format that prismarine-chat understands
|
||||
const converted = {
|
||||
text: '',
|
||||
extra: parsed.rawtext.map((item: any) => {
|
||||
if (typeof item === 'string') return { text: item };
|
||||
if (item.text) return { text: item.text };
|
||||
if (item.translate) return { translate: item.translate, with: item.with };
|
||||
if (item.selector) return { text: item.selector }; // Simplified selector handling
|
||||
if (item.score) return { text: `${item.score.name}:${item.score.objective}` };
|
||||
return item;
|
||||
}),
|
||||
};
|
||||
msg = new ChatMessage(converted);
|
||||
} else {
|
||||
msg = new ChatMessage(parsed);
|
||||
}
|
||||
} else {
|
||||
msg = new ChatMessage({ text: '' });
|
||||
}
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, treat as plain text
|
||||
msg = ChatMessage.fromNotch(data.message || '');
|
||||
}
|
||||
} else {
|
||||
// Handle regular text messages
|
||||
msg = ChatMessage.fromNotch(data.message || '');
|
||||
}
|
||||
|
||||
if (['chat', 'whisper', 'announcement', 'json_whisper', 'json_announcement'].includes(data.type)) {
|
||||
(bot.emit as any)('message', msg, 'chat', data.source_name, null);
|
||||
(bot.emit as any)('messagestr', msg.toString(), data.type, msg, data.source_name, null);
|
||||
} else if (['popup', 'jukebox_popup'].includes(data.type)) {
|
||||
(bot.emit as any)('actionBar', msg, null);
|
||||
} else if (data.type === 'json') {
|
||||
// JSON messages are system/server messages
|
||||
(bot.emit as any)('message', msg, 'system', null);
|
||||
(bot.emit as any)('messagestr', msg.toString(), 'system', msg, null);
|
||||
} else {
|
||||
(bot.emit as any)('message', msg, data.type, null);
|
||||
(bot.emit as any)('messagestr', msg.toString(), data.type, msg, null);
|
||||
}
|
||||
});
|
||||
|
||||
function chatWithHeader(message: string | number) {
|
||||
if (typeof message === 'number') message = message.toString();
|
||||
if (typeof message !== 'string') {
|
||||
throw new Error('Chat message type must be a string or number: ' + typeof message);
|
||||
}
|
||||
|
||||
if (message.startsWith('/')) {
|
||||
// Send command via command_request packet (updated for 1.21.130)
|
||||
// Based on real client packet capture - command includes the leading slash
|
||||
const client = bot._client as any;
|
||||
bot._client.write('command_request', {
|
||||
command: message, // Keep the leading slash - real client sends it
|
||||
origin: {
|
||||
type: 'player',
|
||||
uuid: client.profile?.uuid || bot.player?.uuid || '',
|
||||
request_id: '',
|
||||
player_entity_id: 0n,
|
||||
},
|
||||
internal: false,
|
||||
version: 'latest',
|
||||
} as any);
|
||||
return;
|
||||
}
|
||||
|
||||
const lengthLimit = CHAT_LENGTH_LIMIT;
|
||||
const client = bot._client as any;
|
||||
|
||||
message.split('\n').forEach((subMessage) => {
|
||||
if (!subMessage) return;
|
||||
for (let i = 0; i < subMessage.length; i += lengthLimit) {
|
||||
const smallMsg = subMessage.substring(i, i + lengthLimit);
|
||||
|
||||
// Construct the text packet with category 'authored' for client-to-server chat
|
||||
// Updated for 1.21.130 format
|
||||
bot._client.write('text', {
|
||||
needs_translation: false,
|
||||
category: 'authored',
|
||||
chat: 'chat',
|
||||
type: 'chat',
|
||||
whisper: 'whisper',
|
||||
announcement: 'announcement',
|
||||
source_name: client.username || '',
|
||||
message: smallMsg,
|
||||
xuid: '',
|
||||
platform_chat_id: '',
|
||||
has_filtered_message: false,
|
||||
} as any);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function tabComplete(text: string, assumeCommand = false, sendBlockInSight = true, timeout = 5000): Promise<string[]> {
|
||||
// Tab completion is not implemented for Bedrock Edition
|
||||
// Bedrock uses a different command system that doesn't support client-side tab completion
|
||||
console.warn('tabComplete is not implemented for Bedrock Edition');
|
||||
return [];
|
||||
}
|
||||
|
||||
bot.whisper = (username: string, message: string) => {
|
||||
chatWithHeader(`/tell ${username} ${message}`);
|
||||
};
|
||||
|
||||
bot.chat = (message: string) => {
|
||||
chatWithHeader(message);
|
||||
};
|
||||
|
||||
bot.tabComplete = tabComplete;
|
||||
|
||||
function addDefaultPatterns() {
|
||||
if (!defaultChatPatterns) return;
|
||||
bot.addChatPattern('whisper', new RegExp(`^${USERNAME_REGEX} whispers(?: to you)?:? (.*)$`), {
|
||||
deprecated: true,
|
||||
});
|
||||
bot.addChatPattern('whisper', new RegExp(`^\\[${USERNAME_REGEX} -> \\w+\\s?\\] (.*)$`), {
|
||||
deprecated: true,
|
||||
});
|
||||
bot.addChatPattern('chat', LEGACY_VANILLA_CHAT_REGEX, { deprecated: true });
|
||||
}
|
||||
|
||||
function awaitMessage(...args: (string | RegExp | (string | RegExp)[])[]): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const resolveMessages = args.flatMap((x) => x);
|
||||
function messageListener(msg: string) {
|
||||
if (resolveMessages.some((x) => (x instanceof RegExp ? x.test(msg) : msg === x))) {
|
||||
resolve(msg);
|
||||
bot.off('messagestr', messageListener);
|
||||
}
|
||||
}
|
||||
bot.on('messagestr', messageListener);
|
||||
});
|
||||
}
|
||||
bot.awaitMessage = awaitMessage;
|
||||
}
|
||||
113
bridge/lib/mineflayer/lib/bedrockPlugins/chest.mts
Normal file
113
bridge/lib/mineflayer/lib/bedrockPlugins/chest.mts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Chest Plugin - Container opening convenience methods for Bedrock
|
||||
*
|
||||
* Provides bot.openContainer, bot.openChest, bot.openDispenser aliases
|
||||
* that wrap bot.openBlock/bot.openEntity with container type validation.
|
||||
*/
|
||||
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Entity } from 'prismarine-entity';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
|
||||
// Container block types that can be opened
|
||||
const CONTAINER_BLOCK_NAMES = [
|
||||
'chest',
|
||||
'trapped_chest',
|
||||
'ender_chest',
|
||||
'barrel',
|
||||
'dispenser',
|
||||
'dropper',
|
||||
'hopper',
|
||||
// Shulker boxes (all colors)
|
||||
'shulker_box',
|
||||
'white_shulker_box',
|
||||
'orange_shulker_box',
|
||||
'magenta_shulker_box',
|
||||
'light_blue_shulker_box',
|
||||
'yellow_shulker_box',
|
||||
'lime_shulker_box',
|
||||
'pink_shulker_box',
|
||||
'gray_shulker_box',
|
||||
'light_gray_shulker_box',
|
||||
'cyan_shulker_box',
|
||||
'purple_shulker_box',
|
||||
'blue_shulker_box',
|
||||
'brown_shulker_box',
|
||||
'green_shulker_box',
|
||||
'red_shulker_box',
|
||||
'black_shulker_box',
|
||||
'undyed_shulker_box',
|
||||
];
|
||||
|
||||
// Window types that are valid containers
|
||||
const CONTAINER_WINDOW_TYPES = [
|
||||
'minecraft:generic',
|
||||
'minecraft:chest',
|
||||
'minecraft:dispenser',
|
||||
'minecraft:ender_chest',
|
||||
'minecraft:shulker_box',
|
||||
'minecraft:hopper',
|
||||
'minecraft:container',
|
||||
'minecraft:dropper',
|
||||
'minecraft:trapped_chest',
|
||||
'minecraft:barrel',
|
||||
'minecraft:generic_9x1',
|
||||
'minecraft:generic_9x2',
|
||||
'minecraft:generic_9x3',
|
||||
'minecraft:generic_9x4',
|
||||
'minecraft:generic_9x5',
|
||||
'minecraft:generic_9x6',
|
||||
];
|
||||
|
||||
function isContainerBlock(block: Block): boolean {
|
||||
return CONTAINER_BLOCK_NAMES.some((name) => block.name.includes(name));
|
||||
}
|
||||
|
||||
function isContainerWindow(window: Window): boolean {
|
||||
return CONTAINER_WINDOW_TYPES.some((type) => window.type.startsWith(type));
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
/**
|
||||
* Open a container block or entity.
|
||||
*
|
||||
* @param containerToOpen - Block or Entity to open
|
||||
* @param direction - Direction to face when opening (default: up)
|
||||
* @param cursorPos - Cursor position on block face (default: center)
|
||||
* @returns Window for the opened container
|
||||
*/
|
||||
async function openContainer(containerToOpen: Block | Entity, direction?: Vec3, cursorPos?: Vec3): Promise<Window> {
|
||||
direction = direction ?? new Vec3(0, 1, 0);
|
||||
cursorPos = cursorPos ?? new Vec3(0.5, 0.5, 0.5);
|
||||
|
||||
let window: Window;
|
||||
|
||||
if (containerToOpen.constructor.name === 'Block') {
|
||||
const block = containerToOpen as Block;
|
||||
|
||||
if (!isContainerBlock(block)) {
|
||||
throw new Error(`Block '${block.name}' is not a container`);
|
||||
}
|
||||
|
||||
window = await bot.openBlock(block, direction, cursorPos);
|
||||
} else if (containerToOpen.constructor.name === 'Entity') {
|
||||
const entity = containerToOpen as Entity;
|
||||
window = await bot.openEntity(entity);
|
||||
} else {
|
||||
throw new Error('containerToOpen must be a Block or Entity');
|
||||
}
|
||||
|
||||
if (!isContainerWindow(window)) {
|
||||
throw new Error(`Opened window type '${window.type}' is not a container`);
|
||||
}
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
// Expose API
|
||||
bot.openContainer = openContainer;
|
||||
bot.openChest = openContainer;
|
||||
bot.openDispenser = openContainer;
|
||||
}
|
||||
1168
bridge/lib/mineflayer/lib/bedrockPlugins/craft.mts
Normal file
1168
bridge/lib/mineflayer/lib/bedrockPlugins/craft.mts
Normal file
File diff suppressed because it is too large
Load diff
304
bridge/lib/mineflayer/lib/bedrockPlugins/creative.mts
Normal file
304
bridge/lib/mineflayer/lib/bedrockPlugins/creative.mts
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* Creative Mode Plugin for Bedrock Edition
|
||||
*
|
||||
* Provides creative mode functionality:
|
||||
* - setInventorySlot: Pick items from creative inventory into player slots
|
||||
* - clearSlot: Remove item from a slot
|
||||
* - clearInventory: Clear all inventory slots
|
||||
* - flyTo: Fly to a destination in creative mode
|
||||
* - startFlying / stopFlying: Toggle creative flight
|
||||
*
|
||||
* Protocol:
|
||||
* - Items are picked using item_stack_request with craft_creative action
|
||||
* - Flow: craft_creative -> results_deprecated -> take to cursor -> place to slot
|
||||
* - creative_content packet provides available creative items
|
||||
*/
|
||||
|
||||
import type { BedrockBot } from '../../index.js';
|
||||
import type { Vec3 } from 'vec3';
|
||||
import itemLoader, { type Item } from 'prismarine-item';
|
||||
import { Vec3 as Vec3Constructor } from 'vec3';
|
||||
|
||||
import {
|
||||
getNextItemStackRequestId,
|
||||
getStackId,
|
||||
actions,
|
||||
sendRequest,
|
||||
waitForResponse,
|
||||
slot,
|
||||
cursor,
|
||||
fromPlayerSlot,
|
||||
ContainerIds,
|
||||
} from '../bedrock/index.mts';
|
||||
|
||||
const CREATIVE_OUTPUT_SLOT = 50;
|
||||
const FLYING_SPEED_PER_UPDATE = 0.5;
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const Item = (itemLoader as any)(bot.registry) as typeof Item;
|
||||
|
||||
// Track creative items from creative_content packet
|
||||
// Maps network_id -> entry_id (for use in craft_creative action)
|
||||
let networkIdToEntryId: Map<number, number> = new Map();
|
||||
|
||||
// Parse creative_content packet
|
||||
bot._client.on('creative_content', (packet: any) => {
|
||||
networkIdToEntryId.clear();
|
||||
|
||||
if (packet.items) {
|
||||
for (const entry of packet.items) {
|
||||
const item = entry.item;
|
||||
if (item && item.network_id) {
|
||||
// Map network_id to entry_id for craft_creative lookup
|
||||
networkIdToEntryId.set(item.network_id, entry.entry_id);
|
||||
}
|
||||
}
|
||||
bot.logger.debug(`Creative content: ${packet.items.length} items indexed`);
|
||||
}
|
||||
});
|
||||
|
||||
// Track pending slot updates to prevent duplicate requests
|
||||
const pendingSlotUpdates: Set<number> = new Set();
|
||||
|
||||
/**
|
||||
* Set an inventory slot to a specific item (creative mode only)
|
||||
*
|
||||
* @param slotIndex - The slot index (0-44)
|
||||
* @param item - The item to set, or null to clear
|
||||
* @param waitTimeout - Timeout to wait for rejection (default 400ms)
|
||||
*/
|
||||
async function setInventorySlot(slotIndex: number, item: Item | null, waitTimeout: number = 400): Promise<void> {
|
||||
if (slotIndex < 0 || slotIndex > 44) {
|
||||
throw new Error(`Invalid slot index: ${slotIndex}. Must be 0-44.`);
|
||||
}
|
||||
|
||||
const currentItem = bot.inventory.slots[slotIndex];
|
||||
|
||||
// If already same item, skip
|
||||
if (Item.equal(currentItem, item, true)) return;
|
||||
|
||||
// Prevent concurrent updates to same slot
|
||||
if (pendingSlotUpdates.has(slotIndex)) {
|
||||
throw new Error(`Setting slot ${slotIndex} cancelled due to calling bot.creative.setInventorySlot(${slotIndex}, ...) again`);
|
||||
}
|
||||
|
||||
pendingSlotUpdates.add(slotIndex);
|
||||
|
||||
try {
|
||||
if (item === null) {
|
||||
// Clear the slot - destroy the item
|
||||
await clearSlotInternal(slotIndex);
|
||||
} else {
|
||||
// Set item in slot using creative pick
|
||||
await setSlotItem(slotIndex, item);
|
||||
}
|
||||
|
||||
// Wait a bit to allow server to potentially reject
|
||||
if (waitTimeout > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTimeout));
|
||||
}
|
||||
} finally {
|
||||
pendingSlotUpdates.delete(slotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Clear a slot by destroying its contents
|
||||
*/
|
||||
async function clearSlotInternal(slotIndex: number): Promise<void> {
|
||||
const item = bot.inventory.slots[slotIndex];
|
||||
if (!item) return;
|
||||
|
||||
const loc = fromPlayerSlot(slotIndex, item);
|
||||
const requestId = getNextItemStackRequestId();
|
||||
|
||||
sendRequest(bot, requestId, actions().destroy(item.count, loc).build());
|
||||
|
||||
const success = await waitForResponse(bot, requestId);
|
||||
if (success) {
|
||||
bot.inventory.updateSlot(slotIndex, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Set item in slot using creative pick flow
|
||||
*
|
||||
* Protocol flow from packet capture:
|
||||
* 1. item_stack_request with:
|
||||
* - craft_creative action
|
||||
* - results_deprecated with the item
|
||||
* - take from creative_output (slot 50) to cursor
|
||||
* 2. item_stack_request to place from cursor to destination
|
||||
*/
|
||||
async function setSlotItem(slotIndex: number, item: Item): Promise<void> {
|
||||
// First clear the slot if it has an item
|
||||
const currentItem = bot.inventory.slots[slotIndex];
|
||||
if (currentItem) {
|
||||
await clearSlotInternal(slotIndex);
|
||||
}
|
||||
|
||||
// Step 1: Pick item from creative inventory to cursor
|
||||
const pickRequestId = getNextItemStackRequestId();
|
||||
|
||||
// Build the item in ItemLegacy format for results_deprecated
|
||||
const itemNotch = Item.toNotch(item, 0);
|
||||
const networkId = itemNotch.network_id;
|
||||
|
||||
// Look up the creative entry_id for this item's network_id
|
||||
const entryId = networkIdToEntryId.get(networkId);
|
||||
if (entryId === undefined) {
|
||||
throw new Error(`Item ${item.name} (network_id=${networkId}) not found in creative inventory`);
|
||||
}
|
||||
|
||||
const itemLegacy = {
|
||||
network_id: networkId,
|
||||
count: item.count,
|
||||
metadata: item.metadata || 0,
|
||||
stack_size: 64,
|
||||
block_runtime_id: itemNotch.block_runtime_id || 0,
|
||||
extra: { has_nbt: 0, can_place_on: [], can_destroy: [] },
|
||||
};
|
||||
|
||||
// Send pick request: craft_creative + results_deprecated
|
||||
// Note: Protocol investigation needed - currently returns status 7 error
|
||||
const actionList = actions()
|
||||
.craftCreative(entryId)
|
||||
.resultsDeprecated([itemLegacy])
|
||||
.build();
|
||||
|
||||
bot.logger.debug(`Creative pick: entryId=${entryId}, network_id=${networkId}, count=${item.count}`);
|
||||
|
||||
sendRequest(bot, pickRequestId, actionList);
|
||||
|
||||
const pickSuccess = await waitForResponse(bot, pickRequestId);
|
||||
if (!pickSuccess) {
|
||||
throw new Error('Failed to pick item from creative inventory');
|
||||
}
|
||||
|
||||
// Small delay between requests (like real client)
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
// Step 2: Place from cursor to destination slot
|
||||
const placeRequestId = getNextItemStackRequestId();
|
||||
const destLoc = fromPlayerSlot(slotIndex, null);
|
||||
|
||||
// Use the negative request ID as stack ID (like real client does)
|
||||
const cursorStackId = pickRequestId;
|
||||
|
||||
sendRequest(
|
||||
bot,
|
||||
placeRequestId,
|
||||
actions()
|
||||
.place(item.count, cursor(cursorStackId), slot(destLoc.containerId, destLoc.slot, 0))
|
||||
.build()
|
||||
);
|
||||
|
||||
const placeSuccess = await waitForResponse(bot, placeRequestId);
|
||||
if (!placeSuccess) {
|
||||
throw new Error('Failed to place item from cursor to slot');
|
||||
}
|
||||
|
||||
// Update local inventory state
|
||||
const newItem = new Item(item.type, item.count, item.metadata, item.nbt);
|
||||
newItem.slot = slotIndex;
|
||||
bot.inventory.updateSlot(slotIndex, newItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a specific slot
|
||||
*/
|
||||
function clearSlot(slotIndex: number): Promise<void> {
|
||||
return setInventorySlot(slotIndex, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all inventory slots
|
||||
*/
|
||||
async function clearInventory(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = 0; i < bot.inventory.slots.length; i++) {
|
||||
const item = bot.inventory.slots[i];
|
||||
if (item) {
|
||||
promises.push(setInventorySlot(i, null));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
// Flight state
|
||||
let normalGravity: number | null = null;
|
||||
|
||||
/**
|
||||
* Fly to a destination (straight line, ensure clear path)
|
||||
*/
|
||||
async function flyTo(destination: Vec3): Promise<void> {
|
||||
startFlying();
|
||||
|
||||
let vector = destination.minus(bot.entity.position);
|
||||
let magnitude = vecMagnitude(vector);
|
||||
|
||||
while (magnitude > FLYING_SPEED_PER_UPDATE) {
|
||||
bot.physics.gravity = 0;
|
||||
bot.entity.velocity = new Vec3Constructor(0, 0, 0);
|
||||
|
||||
// Move in small steps
|
||||
const normalizedVector = vector.scaled(1 / magnitude);
|
||||
bot.entity.position.add(normalizedVector.scaled(FLYING_SPEED_PER_UPDATE));
|
||||
|
||||
await sleep(50);
|
||||
|
||||
vector = destination.minus(bot.entity.position);
|
||||
magnitude = vecMagnitude(vector);
|
||||
}
|
||||
|
||||
// Final step
|
||||
bot.entity.position = destination;
|
||||
|
||||
// Wait for move event
|
||||
await new Promise<void>((resolve) => {
|
||||
bot.once('move', resolve);
|
||||
// Don't wait forever if move doesn't fire
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start flying (disable gravity)
|
||||
*/
|
||||
function startFlying(): void {
|
||||
if (normalGravity === null) {
|
||||
normalGravity = bot.physics.gravity;
|
||||
}
|
||||
bot.physics.gravity = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop flying (restore gravity)
|
||||
*/
|
||||
function stopFlying(): void {
|
||||
if (normalGravity !== null) {
|
||||
bot.physics.gravity = normalGravity;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose creative API
|
||||
bot.creative = {
|
||||
setInventorySlot,
|
||||
clearSlot,
|
||||
clearInventory,
|
||||
flyTo,
|
||||
startFlying,
|
||||
stopFlying,
|
||||
};
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function vecMagnitude(vec: Vec3): number {
|
||||
return Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
366
bridge/lib/mineflayer/lib/bedrockPlugins/digging.mts
Normal file
366
bridge/lib/mineflayer/lib/bedrockPlugins/digging.mts
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import type { Block } from 'prismarine-block';
|
||||
import type { Vec3 } from 'vec3';
|
||||
import { performance } from 'perf_hooks';
|
||||
// @ts-ignore
|
||||
import { createDoneTask, createTask } from '../promise_utils.js';
|
||||
|
||||
const BlockFaces = {
|
||||
BOTTOM: 0,
|
||||
TOP: 1,
|
||||
NORTH: 2,
|
||||
SOUTH: 3,
|
||||
WEST: 4,
|
||||
EAST: 5,
|
||||
};
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
let swingInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let continueBreakInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let waitTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let diggingTask = createDoneTask();
|
||||
|
||||
bot.targetDigBlock = null;
|
||||
bot.targetDigFace = null;
|
||||
bot.lastDigTime = null;
|
||||
|
||||
async function dig(block: Block, forceLook?: boolean | 'ignore', digFace?: Vec3 | 'auto' | 'raycast'): Promise<void> {
|
||||
if (block === null || block === undefined) {
|
||||
throw new Error('dig was called with an undefined or null block');
|
||||
}
|
||||
|
||||
if (!digFace || typeof digFace === 'function') {
|
||||
digFace = 'auto';
|
||||
}
|
||||
|
||||
const waitTime = bot.digTime(block);
|
||||
if (waitTime === Infinity) {
|
||||
throw new Error(`dig time for ${block?.name ?? block} is Infinity`);
|
||||
}
|
||||
|
||||
bot.targetDigFace = BlockFaces.TOP; // Default
|
||||
|
||||
if (forceLook !== 'ignore') {
|
||||
// Calculate which face to mine based on position
|
||||
if (digFace && typeof digFace === 'object' && (digFace.x || digFace.y || digFace.z)) {
|
||||
if (digFace.x) {
|
||||
bot.targetDigFace = digFace.x > 0 ? BlockFaces.EAST : BlockFaces.WEST;
|
||||
} else if (digFace.y) {
|
||||
bot.targetDigFace = digFace.y > 0 ? BlockFaces.TOP : BlockFaces.BOTTOM;
|
||||
} else if (digFace.z) {
|
||||
bot.targetDigFace = digFace.z > 0 ? BlockFaces.SOUTH : BlockFaces.NORTH;
|
||||
}
|
||||
await bot.lookAt(block.position.offset(0.5, 0.5, 0.5).offset(digFace.x * 0.5, digFace.y * 0.5, digFace.z * 0.5), forceLook);
|
||||
} else if (digFace === 'raycast') {
|
||||
// Use raycast to find visible face
|
||||
const delta = block.position.offset(0.5, 0.5, 0.5).minus(bot.entity.position.offset(0, bot.entity.height, 0));
|
||||
if (Math.abs(delta.y) > Math.abs(delta.x) && Math.abs(delta.y) > Math.abs(delta.z)) {
|
||||
bot.targetDigFace = delta.y > 0 ? BlockFaces.BOTTOM : BlockFaces.TOP;
|
||||
} else if (Math.abs(delta.x) > Math.abs(delta.z)) {
|
||||
bot.targetDigFace = delta.x > 0 ? BlockFaces.WEST : BlockFaces.EAST;
|
||||
} else {
|
||||
bot.targetDigFace = delta.z > 0 ? BlockFaces.NORTH : BlockFaces.SOUTH;
|
||||
}
|
||||
await bot.lookAt(block.position.offset(0.5, 0.5, 0.5), forceLook);
|
||||
} else {
|
||||
// auto - look at center
|
||||
await bot.lookAt(block.position.offset(0.5, 0.5, 0.5), forceLook);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel any existing dig
|
||||
if (bot.targetDigBlock) bot.stopDigging();
|
||||
|
||||
diggingTask = createTask();
|
||||
const pos = block.position;
|
||||
const face = bot.targetDigFace;
|
||||
|
||||
bot.targetDigBlock = block;
|
||||
|
||||
// Check if this is an instant break (crops, etc.)
|
||||
const isInstantBreak = waitTime <= 50;
|
||||
|
||||
if (isInstantBreak) {
|
||||
// For instant breaks, combine start_break + predict_break in same packet
|
||||
bot.swingArm('right', true, 'mine');
|
||||
bot.swingArm('right', true, 'mine');
|
||||
bot.swingArm('right', true, 'mine');
|
||||
bot.swingArm('right', true, 'mine');
|
||||
bot.swingArm('right', true, 'mine');
|
||||
|
||||
const heldItem = bot.heldItem;
|
||||
const itemStackRequest = heldItem
|
||||
? {
|
||||
requests: [
|
||||
{
|
||||
request_id: bot.getNextItemStackRequestId(),
|
||||
actions: [
|
||||
{
|
||||
type_id: 'mine_block',
|
||||
hotbar_slot: bot.quickBarSlot ?? 0,
|
||||
predicted_durability: (heldItem.durabilityUsed ?? 0) + 1,
|
||||
network_id: heldItem.stackId ?? 0,
|
||||
},
|
||||
],
|
||||
custom_names: [],
|
||||
cause: -1,
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await bot.sendPlayerAuthInputTransaction(
|
||||
{
|
||||
block_action: [
|
||||
{ action: 'start_break', position: { x: pos.x, y: pos.y, z: pos.z }, face: face },
|
||||
{ action: 'predict_break', position: { x: pos.x, y: pos.y, z: pos.z }, face: face },
|
||||
],
|
||||
item_stack_request: itemStackRequest,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Update local block state
|
||||
const airStateId = bot.registry.blocksByName.air?.defaultState ?? 0;
|
||||
bot._updateBlockState(bot.targetDigBlock.position, airStateId);
|
||||
|
||||
// Schedule abort_break after a tick
|
||||
waitTimeout = setTimeout(() => finishInstantDigging(pos, face), 50);
|
||||
} else {
|
||||
// Send start_break for non-instant breaks
|
||||
await bot.sendPlayerAuthInputTransaction(
|
||||
{
|
||||
block_action: [
|
||||
{
|
||||
action: 'start_break',
|
||||
position: { x: pos.x, y: pos.y, z: pos.z },
|
||||
face: face,
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
bot.swingArm('right', true, 'mine');
|
||||
|
||||
// Swing arm every 350ms
|
||||
swingInterval = setInterval(() => {
|
||||
bot.swingArm('right', true, 'mine');
|
||||
}, 350);
|
||||
|
||||
// Send continue_break every 50ms (every tick)
|
||||
continueBreakInterval = setInterval(async () => {
|
||||
if (bot.targetDigBlock) {
|
||||
await bot.sendPlayerAuthInputTransaction(
|
||||
{
|
||||
block_action: [
|
||||
{
|
||||
action: 'continue_break',
|
||||
position: { x: pos.x, y: pos.y, z: pos.z },
|
||||
face: face,
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// Schedule finish digging
|
||||
waitTimeout = setTimeout(() => finishDigging(pos, face), waitTime);
|
||||
}
|
||||
|
||||
async function finishInstantDigging(pos: { x: number; y: number; z: number }, face: number) {
|
||||
clearTimeout(waitTimeout!);
|
||||
waitTimeout = null;
|
||||
|
||||
// Send abort_break after instant break completes
|
||||
await bot.sendPlayerAuthInputTransaction(
|
||||
{
|
||||
block_action: [
|
||||
{
|
||||
action: 'abort_break',
|
||||
position: { x: pos.x, y: pos.y, z: pos.z },
|
||||
face: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
bot.targetDigBlock = null;
|
||||
bot.targetDigFace = null;
|
||||
bot.lastDigTime = performance.now();
|
||||
bot.emit('diggingCompleted', block);
|
||||
diggingTask.finish();
|
||||
}
|
||||
|
||||
async function finishDigging(pos: { x: number; y: number; z: number }, face: number) {
|
||||
clearInterval(swingInterval!);
|
||||
clearInterval(continueBreakInterval!);
|
||||
clearTimeout(waitTimeout!);
|
||||
swingInterval = null;
|
||||
continueBreakInterval = null;
|
||||
waitTimeout = null;
|
||||
|
||||
if (bot.targetDigBlock) {
|
||||
// Send continue_break + predict_break together with item_stack_request for tool durability
|
||||
const heldItem = bot.heldItem;
|
||||
const itemStackRequest = heldItem
|
||||
? {
|
||||
requests: [
|
||||
{
|
||||
request_id: bot.getNextItemStackRequestId(),
|
||||
actions: [
|
||||
{
|
||||
type_id: 'mine_block',
|
||||
hotbar_slot: bot.quickBarSlot ?? 0,
|
||||
predicted_durability: (heldItem.durabilityUsed ?? 0) + 1,
|
||||
network_id: heldItem.stackId ?? 0,
|
||||
},
|
||||
],
|
||||
custom_names: [],
|
||||
cause: -1,
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await bot.sendPlayerAuthInputTransaction(
|
||||
{
|
||||
block_action: [
|
||||
{ action: 'continue_break', position: pos, face: face },
|
||||
{ action: 'predict_break', position: pos, face: face },
|
||||
],
|
||||
item_stack_request: itemStackRequest,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Update local block state - use air block's stateId (not 0!)
|
||||
const airStateId = bot.registry.blocksByName.air?.defaultState ?? 0;
|
||||
bot._updateBlockState(bot.targetDigBlock.position, airStateId);
|
||||
|
||||
await bot.waitForTicks(3);
|
||||
await bot.sendPlayerAuthInputTransaction(
|
||||
{
|
||||
block_action: [
|
||||
{
|
||||
action: 'abort_break',
|
||||
position: { x: pos.x, y: pos.y, z: pos.z },
|
||||
face: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
bot.targetDigBlock = null;
|
||||
bot.targetDigFace = null;
|
||||
bot.lastDigTime = performance.now();
|
||||
}
|
||||
|
||||
// Listen for block update confirmation
|
||||
const eventName = `blockUpdate:${block.position}`;
|
||||
bot.on(eventName, onBlockUpdate);
|
||||
|
||||
const currentBlock = block;
|
||||
bot.stopDigging = () => {
|
||||
if (!bot.targetDigBlock) return;
|
||||
|
||||
bot.removeListener(eventName, onBlockUpdate);
|
||||
clearInterval(swingInterval!);
|
||||
clearInterval(continueBreakInterval!);
|
||||
clearTimeout(waitTimeout!);
|
||||
swingInterval = null;
|
||||
continueBreakInterval = null;
|
||||
waitTimeout = null;
|
||||
|
||||
// Send abort_break
|
||||
const abortPos = bot.targetDigBlock.position;
|
||||
bot.sendPlayerAuthInputTransaction(
|
||||
{
|
||||
block_action: [
|
||||
{
|
||||
action: 'abort_break',
|
||||
position: { x: abortPos.x, y: abortPos.y, z: abortPos.z },
|
||||
face: bot.targetDigFace ?? 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const abortedBlock = bot.targetDigBlock;
|
||||
bot.targetDigBlock = null;
|
||||
bot.targetDigFace = null;
|
||||
bot.lastDigTime = performance.now();
|
||||
bot.emit('diggingAborted', abortedBlock);
|
||||
bot.stopDigging = noop;
|
||||
diggingTask.cancel(new Error('Digging aborted'));
|
||||
};
|
||||
|
||||
function onBlockUpdate(oldBlock: Block | null, newBlock: Block | null) {
|
||||
// Block update received - check if block is now air
|
||||
if (newBlock?.type !== 0) return;
|
||||
|
||||
bot.removeListener(eventName, onBlockUpdate);
|
||||
clearInterval(swingInterval!);
|
||||
clearInterval(continueBreakInterval!);
|
||||
clearTimeout(waitTimeout!);
|
||||
swingInterval = null;
|
||||
continueBreakInterval = null;
|
||||
waitTimeout = null;
|
||||
bot.targetDigBlock = null;
|
||||
bot.targetDigFace = null;
|
||||
bot.lastDigTime = performance.now();
|
||||
bot.emit('diggingCompleted', newBlock);
|
||||
diggingTask.finish();
|
||||
}
|
||||
|
||||
await diggingTask.promise;
|
||||
}
|
||||
|
||||
bot.on('death', () => {
|
||||
bot.removeAllListeners('diggingAborted');
|
||||
bot.removeAllListeners('diggingCompleted');
|
||||
bot.stopDigging();
|
||||
});
|
||||
|
||||
function canDigBlock(block: Block): boolean {
|
||||
return block && block.diggable && block.position.offset(0.5, 0.5, 0.5).distanceTo(bot.entity.position.offset(0, 1.65, 0)) <= 5.1;
|
||||
}
|
||||
|
||||
function digTime(block: Block): number {
|
||||
let type = null;
|
||||
let enchantments: any[] = [];
|
||||
|
||||
const currentlyHeldItem = bot.heldItem;
|
||||
if (currentlyHeldItem) {
|
||||
type = currentlyHeldItem.type;
|
||||
enchantments = currentlyHeldItem.enchants || [];
|
||||
}
|
||||
|
||||
// Append helmet enchantments (Aqua Affinity affects dig speed)
|
||||
const headEquipmentSlot = bot.getEquipmentDestSlot?.('head');
|
||||
if (headEquipmentSlot !== undefined) {
|
||||
const headEquippedItem = bot.inventory?.slots?.[headEquipmentSlot];
|
||||
if (headEquippedItem?.enchants) {
|
||||
enchantments = enchantments.concat(headEquippedItem.enchants);
|
||||
}
|
||||
}
|
||||
|
||||
const creative = bot.game?.gameMode === 'creative';
|
||||
return block.digTime(type, creative, bot.entity?.isInWater ?? false, !(bot.entity?.onGround ?? true), enchantments, bot.entity?.effects ?? {});
|
||||
}
|
||||
|
||||
bot.dig = dig;
|
||||
bot.stopDigging = noop;
|
||||
bot.canDigBlock = canDigBlock;
|
||||
bot.digTime = digTime;
|
||||
}
|
||||
|
||||
function noop(err?: Error) {
|
||||
if (err) throw err;
|
||||
}
|
||||
601
bridge/lib/mineflayer/lib/bedrockPlugins/entities.mts
Normal file
601
bridge/lib/mineflayer/lib/bedrockPlugins/entities.mts
Normal file
|
|
@ -0,0 +1,601 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
import conv from '../conversions.js';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const NAMED_ENTITY_HEIGHT = 1.62;
|
||||
const NAMED_ENTITY_WIDTH = 0.6;
|
||||
const CROUCH_HEIGHT = NAMED_ENTITY_HEIGHT - 0.08;
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const { mobs, entitiesArray } = bot.registry;
|
||||
const Entity = require('prismarine-entity')(bot.registry);
|
||||
const Item = require('prismarine-item')(bot.registry);
|
||||
const ChatMessage = require('prismarine-chat')(bot.registry);
|
||||
|
||||
bot.findPlayer = bot.findPlayers = (filter: any) => {
|
||||
const filterFn = (entity: any) => {
|
||||
if (entity.type !== 'player') return false;
|
||||
if (filter === null) return true;
|
||||
if (typeof filter === 'object' && filter instanceof RegExp) {
|
||||
return entity.username.search(filter) !== -1;
|
||||
} else if (typeof filter === 'function') {
|
||||
return filter(entity);
|
||||
} else if (typeof filter === 'string') {
|
||||
return entity.username.toLowerCase() === filter.toLowerCase();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const resultSet = Object.values(bot.entities).filter(filterFn);
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
switch (resultSet.length) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return resultSet[0];
|
||||
default:
|
||||
return resultSet;
|
||||
}
|
||||
}
|
||||
return resultSet;
|
||||
};
|
||||
|
||||
bot.players = {};
|
||||
bot.uuidToUsername = {};
|
||||
bot.entities = {};
|
||||
|
||||
bot._playerFromUUID = (uuid: string) => Object.values(bot.players).find((player: any) => player.uuid === uuid);
|
||||
|
||||
bot.nearestEntity = (
|
||||
match = (entity: any) => {
|
||||
return true;
|
||||
}
|
||||
) => {
|
||||
let best: any = null;
|
||||
let bestDistance = Number.MAX_VALUE;
|
||||
|
||||
for (const entity of Object.values(bot.entities) as any[]) {
|
||||
if (entity === bot.entity || !match(entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dist = bot.entity.position.distanceSquared(entity.position);
|
||||
if (dist < bestDistance && entity.id) {
|
||||
best = entity;
|
||||
bestDistance = dist;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
};
|
||||
|
||||
// Reset list of players and entities on login
|
||||
bot._client.on('start_game', (packet) => {
|
||||
bot.players = {};
|
||||
bot.uuidToUsername = {};
|
||||
bot.entities = {};
|
||||
// login
|
||||
bot.entity = fetchEntity(packet.runtime_entity_id);
|
||||
bot.username = bot._client.username;
|
||||
bot.entity.username = bot._client.username;
|
||||
bot.entity.type = 'player';
|
||||
bot.entity.name = 'player';
|
||||
});
|
||||
|
||||
// bot._client.on('entity_equipment', (packet) => {
|
||||
// // entity equipment
|
||||
// const entity = fetchEntity(packet.entityId)
|
||||
// if (packet.equipments !== undefined) {
|
||||
// packet.equipments.forEach(equipment => entity.setEquipment(equipment.slot, equipment.item ? Item.fromNotch(equipment.item) : null))
|
||||
// } else {
|
||||
// entity.setEquipment(packet.slot, packet.item ? Item.fromNotch(packet.item) : null)
|
||||
// }
|
||||
// bot.emit('entityEquip', entity)
|
||||
// })
|
||||
|
||||
bot._client.on('add_player', (packet) => {
|
||||
// CHANGE
|
||||
// in case player_info packet was not sent before named_entity_spawn : ignore named_entity_spawn (see #213)
|
||||
//if (packet.uuid in bot.uuidToUsername) {
|
||||
// spawn named entity
|
||||
const runtime_id = packet.runtime_id ?? packet.entity_runtime_id ?? packet.runtime_entity_id;
|
||||
const entity = fetchEntity(runtime_id);
|
||||
entity.type = 'player';
|
||||
entity.name = 'player';
|
||||
entity.id = runtime_id;
|
||||
entity.username = bot.uuidToUsername[packet.uuid];
|
||||
entity.uuid = packet.uuid;
|
||||
entity.unique_id = packet.unique_entity_id ?? packet.unique_id;
|
||||
// entity.dataBlobs = packet.metadata
|
||||
if (bot.supportFeature('fixedPointPosition')) {
|
||||
entity.position.set(packet.position.x / 32, packet.position.y - NAMED_ENTITY_HEIGHT / 32, packet.position.z / 32);
|
||||
} else if (bot.supportFeature('doublePosition')) {
|
||||
entity.position.set(packet.position.x, packet.position.y - NAMED_ENTITY_HEIGHT, packet.position.z);
|
||||
} else {
|
||||
entity.position.set(packet.position.x, packet.position.y - NAMED_ENTITY_HEIGHT, packet.position.z);
|
||||
}
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw);
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch);
|
||||
entity.headYaw = conv.fromNotchianYawByte(packet.head_yaw ?? 0);
|
||||
|
||||
entity.height = NAMED_ENTITY_HEIGHT;
|
||||
entity.width = NAMED_ENTITY_WIDTH;
|
||||
entity.metadata = parseMetadata(packet.metadata, entity.metadata);
|
||||
if (bot.players[entity.username] !== undefined && !bot.players[entity.username].entity) {
|
||||
bot.players[entity.username].entity = entity;
|
||||
}
|
||||
bot.emit('entitySpawn', entity);
|
||||
//}
|
||||
});
|
||||
|
||||
function setEntityData(entity: any, type: any, entityData?: any) {
|
||||
if (entityData === undefined) {
|
||||
entityData = entitiesArray.find((entity: any) => entity.internalId === type);
|
||||
}
|
||||
if (entityData) {
|
||||
entity.displayName = entityData.displayName;
|
||||
entity.entityType = entityData.id;
|
||||
entity.name = entityData.name;
|
||||
entity.kind = entityData.category;
|
||||
entity.height = entityData.height;
|
||||
entity.width = entityData.width;
|
||||
} else {
|
||||
// unknown entity (item entity?)
|
||||
entity.type = 'other';
|
||||
entity.entityType = type;
|
||||
entity.displayName = 'unknown';
|
||||
entity.name = 'unknown';
|
||||
entity.kind = 'unknown';
|
||||
}
|
||||
}
|
||||
function add_entity(packet: any) {
|
||||
const entity = fetchEntity(packet.runtime_id ?? packet.runtime_entity_id);
|
||||
const entityData = bot.registry.entitiesByName[packet.entity_type?.replace('minecraft:', '')];
|
||||
|
||||
entity.type = entityData ? entityData.type || 'object' : 'object';
|
||||
|
||||
setEntityData(entity, entity.type, entityData);
|
||||
|
||||
if (packet.item) {
|
||||
entity.type = 'item';
|
||||
entity.item = packet.item;
|
||||
}
|
||||
|
||||
// if (bot.supportFeature('fixedPointPosition')) {
|
||||
// entity.position.set(packet.position.x / 32, packet.position.y / 32, packet.position.z / 32)
|
||||
// } else if (bot.supportFeature('doublePosition')) {
|
||||
// entity.position.set(packet.position.x, packet.position.y, packet.position.z)
|
||||
// }
|
||||
entity.position.set(packet.position.x, packet.position.y, packet.position.z);
|
||||
entity.velocity.set(packet.velocity.x, packet.velocity.y, packet.velocity.z);
|
||||
// else if (bot.supportFeature('consolidatedEntitySpawnPacket')) {
|
||||
// entity.headPitch = conv.fromNotchianPitchByte(packet.headPitch)
|
||||
// }
|
||||
|
||||
entity.unique_id = packet.entity_id_self ?? packet.unique_id; // 1.19 / 1.18
|
||||
if (entity.type !== 'item') {
|
||||
// NEEDS TO BE MOVED SOMEWHERE
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw) ?? 0; // conv.fromNotchianYawByte
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch) ?? 0; // conv.fromNotchianPitchByte
|
||||
entity.headYaw = conv.fromNotchianPitchByte(packet.head_yaw) ?? 0;
|
||||
}
|
||||
|
||||
if (packet.links) {
|
||||
// Might be wrong
|
||||
for (const link in packet.links) {
|
||||
const rider = fetchEntity((packet.links as any)[link].rider_entity_id);
|
||||
rider.vehicle = fetchEntity((packet.links as any)[link].ridden_entity_id);
|
||||
//rider.vehicle.position = rider.position
|
||||
bot.emit('entityAttach', rider, rider.vehicle);
|
||||
}
|
||||
}
|
||||
|
||||
//entity.objectData = packet.objectData
|
||||
bot.emit('update_attributes', packet);
|
||||
bot.emit('entitySpawn', entity);
|
||||
}
|
||||
//Add Item Entity !!!
|
||||
|
||||
bot._client.on('add_entity', add_entity);
|
||||
bot._client.on('add_item_entity', add_entity);
|
||||
|
||||
bot._client.on('set_entity_motion', (packet) => {
|
||||
// entity velocity
|
||||
const entity = fetchEntity(packet.runtime_entity_id);
|
||||
//console.log(packet.velocity)
|
||||
entity.velocity = new Vec3(packet.velocity.x, packet.velocity.y, packet.velocity.z);
|
||||
});
|
||||
|
||||
bot._client.on('remove_entity', (packet) => {
|
||||
// destroy entity
|
||||
const id = packet.entity_id_self;
|
||||
const entity = fetchEntity(id);
|
||||
if (!entity) return; // TODO: Fix this
|
||||
bot.emit('entityGone', entity);
|
||||
entity.isValid = false;
|
||||
if (entity.username && bot.players[entity.username]) {
|
||||
bot.players[entity.username].entity = null;
|
||||
}
|
||||
delete bot.entities[id];
|
||||
});
|
||||
function movePlayer(packet: any) {
|
||||
// entity teleport
|
||||
const entity = fetchEntity(packet.runtime_id ?? packet.entity_runtime_id ?? packet.runtime_entity_id);
|
||||
const position = packet.player_position ?? packet.position;
|
||||
// if (bot.supportFeature('fixedPointPosition')) {
|
||||
// entity.position.set(packet.position.x / 32, packet.position.y / 32, packet.position.z / 32)
|
||||
// }
|
||||
// if (bot.supportFeature('doublePosition')) {
|
||||
// entity.position.set(packet.position.x, packet.position.y, packet.position.z)
|
||||
// }
|
||||
entity.position.set(position.x, position.y - NAMED_ENTITY_HEIGHT, position.z); // FIND OUT WHY doublePosition NEEDED
|
||||
// set rotation !!!
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw ?? packet.rotation.z);
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch ?? packet.rotation.x);
|
||||
entity.headYaw = conv.fromNotchianYawByte(packet.head_yaw ?? 0);
|
||||
bot.emit('playerMoved', entity);
|
||||
bot.emit('entityMoved', entity);
|
||||
}
|
||||
bot._client.on('start_game', movePlayer);
|
||||
bot._client.on('move_player', movePlayer);
|
||||
|
||||
bot._client.on('move_entity', (packet) => {
|
||||
// entity teleport
|
||||
const entity = fetchEntity(packet.runtime_entity_id);
|
||||
// if (bot.supportFeature('fixedPointPosition')) {
|
||||
// entity.position.set(packet.position.x / 32, packet.position.y / 32, packet.position.z / 32)
|
||||
// }
|
||||
// if (bot.supportFeature('doublePosition')) {
|
||||
// entity.position.set(packet.position.x, packet.position.y, packet.position.z)
|
||||
// }
|
||||
entity.position.set(packet.position.x, packet.position.y, packet.position.z); // FIND OUT WHY doublePosition NEEDED
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw ?? packet.rotation.z);
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch ?? packet.rotation.x);
|
||||
entity.headYaw = conv.fromNotchianYawByte(packet.rotation.headYaw ?? 0);
|
||||
|
||||
bot.emit('entityMoved', entity);
|
||||
});
|
||||
|
||||
bot._client.on('move_entity_delta', (packet) => {
|
||||
// entity teleport
|
||||
const entity = fetchEntity(packet.runtime_entity_id);
|
||||
// if (bot.supportFeature('fixedPointPosition')) {
|
||||
// entity.position.set(packet.position.x / 32, packet.position.y / 32, packet.position.z / 32)
|
||||
// }
|
||||
// if (bot.supportFeature('doublePosition')) {
|
||||
// entity.position.set(packet.position.x, packet.position.y, packet.position.z)
|
||||
// }
|
||||
entity.position.set(packet.x ?? entity.position.x, packet.y ?? entity.position.y, packet.z ?? entity.position.z); // FIND OUT WHY doublePosition NEEDED
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.rot_z ?? entity.yaw);
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.rot_y ?? entity.pitch);
|
||||
entity.headYaw = conv.fromNotchianYawByte(packet.rot_x ?? entity.headYaw);
|
||||
|
||||
bot.emit('entityMoved', entity);
|
||||
});
|
||||
|
||||
bot._client.on('set_entity_link', (packet) => {
|
||||
// attach entity
|
||||
const entity = fetchEntity(packet.link.rider_entity_id);
|
||||
|
||||
if (entity == null) {
|
||||
bot.logger.warn('entity not found', packet.link.rider_entity_id);
|
||||
return;
|
||||
}
|
||||
if (packet.type === 0) {
|
||||
const vehicle = entity.vehicle;
|
||||
delete entity.vehicle;
|
||||
bot.emit('entityDetach', entity, vehicle);
|
||||
} else {
|
||||
entity.vehicle = fetchEntity(packet.link.ridden_entity_id);
|
||||
// entity.position = entity.vehicle.position
|
||||
// console.log(entity.position)
|
||||
bot.emit('entityAttach', entity, entity.vehicle);
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('set_entity_data', (packet) => {
|
||||
// REWRITE TO ADD ENTITY \ PLAYER
|
||||
//entity metadata
|
||||
const entity = fetchEntity(packet.runtime_entity_id);
|
||||
entity.metadata = parseMetadata(packet.metadata, entity.metadata);
|
||||
//console.log(packet)
|
||||
bot.emit('entityUpdate', entity);
|
||||
|
||||
// const typeSlot = (bot.supportFeature('itemsAreAlsoBlocks') ? 5 : 6) + (bot.supportFeature('entityMetadataHasLong') ? 1 : 0)
|
||||
// const slot = packet.metadata.find(e => e.type === typeSlot)
|
||||
// if (entity.name && (entity.name.toLowerCase() === 'item' || entity.name === 'item_stack') && slot) {
|
||||
// bot.emit('itemDrop', entity)
|
||||
// }
|
||||
//
|
||||
// const typePose = bot.supportFeature('entityMetadataHasLong') ? 19 : 18
|
||||
// const pose = packet.metadata.find(e => e.type === typePose)
|
||||
// if (pose && pose.value === 2) {
|
||||
// bot.emit('entitySleep', entity)
|
||||
// }
|
||||
//
|
||||
// const bitField = packet.metadata.find(p => p.key === 0)
|
||||
// if (bitField === undefined) {
|
||||
// return
|
||||
// }
|
||||
// if ((bitField.value & 2) !== 0) {
|
||||
// entity.crouching = true
|
||||
// bot.emit('entityCrouch', entity)
|
||||
// } else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event
|
||||
// entity.crouching = false
|
||||
// bot.emit('entityUncrouch', entity)
|
||||
// }
|
||||
});
|
||||
|
||||
bot._client.on('update_attributes', (packet) => {
|
||||
// MAKE COMPATABLE WITH PHYSICS
|
||||
const entity = fetchEntity(packet.runtime_entity_id);
|
||||
if (!entity.attributes) entity.attributes = {};
|
||||
for (const prop of packet.attributes) {
|
||||
entity.attributes[prop.name] = {
|
||||
value: prop.current,
|
||||
modifiers: prop.modifiers.map((x: any) => ({ ...x, uuid: x.id })),
|
||||
// extra info, bedrock only
|
||||
min: prop.min,
|
||||
max: prop.max,
|
||||
default: prop.default,
|
||||
};
|
||||
|
||||
if (prop.name === 'minecraft:movement') {
|
||||
entity.attributes[prop.name].value = entity.attributes[prop.name].default;
|
||||
}
|
||||
}
|
||||
bot.emit('entityAttributes', entity);
|
||||
});
|
||||
|
||||
bot._client.on('correct_player_move_prediction', (packet) => {
|
||||
if (packet.prediction_type === 'player') {
|
||||
if (bot.supportFeature('fixedPointPosition')) {
|
||||
bot.entity.position.set(packet.position.x / 32, packet.position.y - NAMED_ENTITY_HEIGHT / 32, packet.position.z / 32);
|
||||
} else if (bot.supportFeature('doublePosition')) {
|
||||
bot.entity.position.set(packet.position.x, packet.position.y - NAMED_ENTITY_HEIGHT, packet.position.z);
|
||||
} else {
|
||||
bot.entity.position.set(packet.position.x, packet.position.y - NAMED_ENTITY_HEIGHT, packet.position.z);
|
||||
bot.entity.velocity.set(packet.delta.x, packet.delta.y, packet.delta.z);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
bot.emit('entitySpawn', bot.entity);
|
||||
});
|
||||
|
||||
bot._client.on('player_list', (packet) => {
|
||||
// REWRITE
|
||||
// player list item(s)
|
||||
// if (bot.supportFeature('playerInfoActionIsBitfield')) {
|
||||
// for (const item of packet.records) {
|
||||
// console.log(item)
|
||||
// if (item.type === 'remove'){
|
||||
//
|
||||
// }
|
||||
// let player = bot.uuidToUsername[item.uuid] ? bot.players[bot.uuidToUsername[item.uuid]] : null
|
||||
// let newPlayer = false
|
||||
//
|
||||
// const obj = {
|
||||
// uuid: item.uuid
|
||||
// }
|
||||
//
|
||||
// if (!player) newPlayer = true
|
||||
//
|
||||
// player = player || obj
|
||||
//
|
||||
// if (packet.action & 1) {
|
||||
// obj.username = item.player.name
|
||||
// obj.displayName = player.displayName || new ChatMessage({ text: '', extra: [{ text: item.player.name }] })
|
||||
// }
|
||||
//
|
||||
// if (packet.action & 4) {
|
||||
// obj.gamemode = item.gamemode
|
||||
// }
|
||||
//
|
||||
// if (packet.action & 16) {
|
||||
// obj.ping = item.latency
|
||||
// }
|
||||
//
|
||||
// if (item.displayName) {
|
||||
// obj.displayName = new ChatMessage(JSON.parse(item.displayName))
|
||||
// } else if (packet.action & 32) obj.displayName = new ChatMessage({ text: '', extra: [{ text: player.username || obj.username }] })
|
||||
//
|
||||
// if (newPlayer) {
|
||||
// if (!obj.username) continue // Should be unreachable
|
||||
// player = bot.players[obj.username] = obj
|
||||
// bot.uuidToUsername[obj.uuid] = obj.username
|
||||
// } else {
|
||||
// Object.assign(player, obj)
|
||||
// }
|
||||
//
|
||||
// const playerEntity = Object.values(bot.entities).find(e => e.type === 'player' && e.username === player.username)
|
||||
// player.entity = playerEntity
|
||||
//
|
||||
// if (playerEntity === bot.entity) {
|
||||
// bot.player = player
|
||||
// }
|
||||
//
|
||||
// if (newPlayer) {
|
||||
// bot.emit('playerJoined', player)
|
||||
// } else {
|
||||
// bot.emit('playerUpdated', player)
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
packet.records.records.forEach((item: any) => {
|
||||
let player = bot.uuidToUsername[item.uuid] ? bot.players[bot.uuidToUsername[item.uuid]] : null;
|
||||
|
||||
if (packet.records.type === 'add') {
|
||||
let newPlayer = false;
|
||||
|
||||
// New Player
|
||||
if (!player) {
|
||||
if (!item.username) return;
|
||||
player = bot.players[item.username] = {
|
||||
username: item.username,
|
||||
uuid: item.uuid,
|
||||
displayName: new ChatMessage({ text: '', extra: [{ text: item.username }] }),
|
||||
profileKeys: item.xbox_user_id ?? null,
|
||||
};
|
||||
|
||||
bot.uuidToUsername[item.uuid] = item.username;
|
||||
bot.emit('playerJoined', player);
|
||||
newPlayer = true;
|
||||
} else {
|
||||
// Just an Update
|
||||
player = bot.players[item.username] = {
|
||||
username: item.username,
|
||||
uuid: item.uuid,
|
||||
displayName: new ChatMessage({ text: '', extra: [{ text: item.username }] }),
|
||||
profileKeys: item.xbox_user_id ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// if (item.username) {
|
||||
// player.username = new ChatMessage(item.username)
|
||||
// }
|
||||
const playerEntity = Object.values(bot.entities).find((e: any) => e.type === 'player' && e.uuid === item.uuid) as any;
|
||||
player.entity = playerEntity;
|
||||
if (player.entity)
|
||||
bot.players[item.username]['displayName'] = new ChatMessage({
|
||||
text: '',
|
||||
extra: [{ text: player.entity.nametag }],
|
||||
});
|
||||
|
||||
if (playerEntity === bot.entity) {
|
||||
bot.player = player;
|
||||
}
|
||||
|
||||
if (!newPlayer) {
|
||||
bot.emit('playerUpdated', player);
|
||||
}
|
||||
} else if (packet.records.type === 'remove') {
|
||||
if (!player) return;
|
||||
if (player.entity === bot.entity) return;
|
||||
|
||||
// delete entity
|
||||
if (player.entity) {
|
||||
const id = player.entity.id;
|
||||
const entity = fetchEntity(id);
|
||||
bot.emit('entityGone', entity);
|
||||
entity.isValid = false;
|
||||
player.entity = null;
|
||||
delete bot.entities[id];
|
||||
}
|
||||
|
||||
delete bot.players[player.username];
|
||||
delete bot.uuidToUsername[item.uuid];
|
||||
bot.emit('playerLeft', player);
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
bot.emit('playerUpdated', player);
|
||||
});
|
||||
});
|
||||
|
||||
function swingArm(arm = 'right', showHand = true, swingSource?: 'mine' | 'build') {
|
||||
//const hand = arm === 'right' ? 0 : 1
|
||||
const packet: any = {
|
||||
action_id: 'swing_arm',
|
||||
runtime_entity_id: bot.entity.id,
|
||||
data: 0,
|
||||
has_swing_source: !!swingSource,
|
||||
};
|
||||
if (swingSource) {
|
||||
packet.swing_source = swingSource;
|
||||
}
|
||||
bot._client.write('animate', packet);
|
||||
}
|
||||
|
||||
bot.swingArm = swingArm;
|
||||
bot.attack = attack;
|
||||
// bot.mount = mount
|
||||
// bot.dismount = dismount
|
||||
// bot.useOn = useOn
|
||||
// bot.moveVehicle = moveVehicle
|
||||
// useEntity
|
||||
function attackEntity(target: any) {
|
||||
itemUseOnEntity(target, 0);
|
||||
}
|
||||
function itemUseOnEntity(target: any, type: number) {
|
||||
const typeStr = ['attack', 'interact'][type];
|
||||
const transaction = {
|
||||
transaction: {
|
||||
legacy: {
|
||||
legacy_request_id: 0,
|
||||
},
|
||||
transaction_type: 'item_use_on_entity',
|
||||
actions: [],
|
||||
transaction_data: {
|
||||
entity_runtime_id: target.id,
|
||||
action_type: typeStr,
|
||||
hotbar_slot: bot.quickBarSlot,
|
||||
held_item: bot.heldItem,
|
||||
player_pos: bot.entity.position,
|
||||
click_pos: {
|
||||
// in case with interact its pos on hitbox of entity that is being interacted with
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
bot._client.write('inventory_transaction', transaction);
|
||||
}
|
||||
|
||||
function attack(target: any, swing = true) {
|
||||
// arm animation comes before the use_entity packet on 1.8
|
||||
if (bot.supportFeature('armAnimationBeforeUse')) {
|
||||
if (swing) {
|
||||
bot.swingArm(); // in inventory
|
||||
}
|
||||
attackEntity(target);
|
||||
} else {
|
||||
attackEntity(target);
|
||||
if (swing) {
|
||||
bot.swingArm(); // in inventory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetchEntity(id: any) {
|
||||
function searchByUUID(obj: any, unique_id: any) {
|
||||
for (const key in obj) {
|
||||
if (obj[key].unique_id === unique_id) {
|
||||
return obj[key];
|
||||
}
|
||||
}
|
||||
return null; // Если нет совпадений
|
||||
}
|
||||
if (id < 0) {
|
||||
let entity = searchByUUID(bot.entities, id);
|
||||
if (entity) {
|
||||
return entity;
|
||||
} else {
|
||||
// delete bot.entities[id]
|
||||
// throw Error('UNEXPECTED!!! Couldn\'t find entity!')
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return bot.entities[id] || (bot.entities[id] = new Entity(id));
|
||||
}
|
||||
|
||||
// Expose fetchEntity on bot for other plugins (like bossbar)
|
||||
(bot as any).fetchEntity = fetchEntity;
|
||||
}
|
||||
|
||||
function parseMetadata(metadata: any, entityMetadata: any = {}) {
|
||||
if (metadata !== undefined) {
|
||||
for (const { key, value } of metadata) {
|
||||
entityMetadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return entityMetadata;
|
||||
}
|
||||
23
bridge/lib/mineflayer/lib/bedrockPlugins/experience.mts
Normal file
23
bridge/lib/mineflayer/lib/bedrockPlugins/experience.mts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot.experience = {
|
||||
level: null,
|
||||
points: null,
|
||||
progress: null,
|
||||
};
|
||||
bot.on('entityAttributes', (entity) => {
|
||||
if (entity !== bot.entity) return;
|
||||
if (!entity.attributes) return;
|
||||
if ('minecraft:player.level' in entity.attributes) {
|
||||
bot.experience.level = entity.attributes['minecraft:player.level'].value;
|
||||
}
|
||||
if ('minecraft:player.experience' in entity.attributes) {
|
||||
let attribute = entity.attributes['minecraft:player.experience'];
|
||||
bot.experience.points = attribute.value;
|
||||
// something wrong here !
|
||||
bot.experience.progress = ((attribute.value - attribute.default) / (attribute.max - attribute.default)) * 100;
|
||||
}
|
||||
bot.emit('experience');
|
||||
});
|
||||
}
|
||||
91
bridge/lib/mineflayer/lib/bedrockPlugins/fishing.mts
Normal file
91
bridge/lib/mineflayer/lib/bedrockPlugins/fishing.mts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import type { Bot } from '../..'
|
||||
import { createDoneTask, createTask } from '../promise_utils.js'
|
||||
|
||||
export default function inject(bot: Bot) {
|
||||
let fishingTask = createDoneTask()
|
||||
let lastBobber: any = null
|
||||
|
||||
// In Bedrock, the fishing bobber is called "minecraft:fishing_hook"
|
||||
const FISHING_HOOK_TYPE = 'minecraft:fishing_hook'
|
||||
|
||||
// Handle fishing hook entity spawn
|
||||
bot._client.on('add_entity', (packet: {
|
||||
entity_type: string
|
||||
runtime_id: number | bigint
|
||||
unique_id?: number | bigint
|
||||
}) => {
|
||||
if (packet.entity_type === FISHING_HOOK_TYPE && !fishingTask.done && !lastBobber) {
|
||||
// Store the bobber entity reference
|
||||
const entityId = typeof packet.runtime_id === 'bigint'
|
||||
? Number(packet.runtime_id)
|
||||
: packet.runtime_id
|
||||
lastBobber = bot.entities[entityId]
|
||||
|
||||
// If entity isn't immediately available, wait for it
|
||||
if (!lastBobber) {
|
||||
// Store the ID to track it later
|
||||
lastBobber = { id: entityId, _pending: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle entity events for fishing hook
|
||||
// In Bedrock, fish_hook_hook event indicates a fish is hooked
|
||||
bot._client.on('entity_event', (packet: {
|
||||
runtime_entity_id: number | bigint
|
||||
event_id: string
|
||||
}) => {
|
||||
if (!lastBobber || fishingTask.done) return
|
||||
|
||||
// When fish_hook_hook event fires, a fish is on the hook!
|
||||
if (packet.event_id === 'fish_hook_hook') {
|
||||
const entityId = typeof packet.runtime_entity_id === 'bigint'
|
||||
? Number(packet.runtime_entity_id)
|
||||
: packet.runtime_entity_id
|
||||
|
||||
const bobberId = lastBobber._pending ? lastBobber.id : lastBobber.id
|
||||
if (entityId === bobberId) {
|
||||
// Reel in the fish!
|
||||
bot.activateItem()
|
||||
lastBobber = null
|
||||
fishingTask.finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle bobber entity removal
|
||||
bot._client.on('remove_entity', (packet: {
|
||||
entity_id_self: number | bigint
|
||||
}) => {
|
||||
if (!lastBobber) return
|
||||
|
||||
const removedId = typeof packet.entity_id_self === 'bigint'
|
||||
? Number(packet.entity_id_self)
|
||||
: packet.entity_id_self
|
||||
|
||||
const bobberId = lastBobber._pending ? lastBobber.id : lastBobber.id
|
||||
if (removedId === bobberId) {
|
||||
lastBobber = null
|
||||
if (!fishingTask.done) {
|
||||
fishingTask.cancel(new Error('Fishing cancelled'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function fish(): Promise<void> {
|
||||
if (!fishingTask.done) {
|
||||
fishingTask.cancel(new Error('Fishing cancelled due to calling bot.fish() again'))
|
||||
}
|
||||
|
||||
fishingTask = createTask()
|
||||
lastBobber = null
|
||||
|
||||
// Cast the fishing rod
|
||||
bot.activateItem()
|
||||
|
||||
// Wait for fish to bite
|
||||
await fishingTask.promise
|
||||
}
|
||||
|
||||
bot.fish = fish
|
||||
}
|
||||
172
bridge/lib/mineflayer/lib/bedrockPlugins/game.mts
Normal file
172
bridge/lib/mineflayer/lib/bedrockPlugins/game.mts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { createRequire } from 'module';
|
||||
import { createTask } from '../promise_utils.js';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const nbt = require('prismarine-nbt');
|
||||
|
||||
const difficultyNames = ['peaceful', 'easy', 'normal', 'hard'];
|
||||
//const gameModes = ['survival', 'creative', 'adventure']
|
||||
|
||||
// const dimensionNames = {
|
||||
// '-1': 'minecraft:nether',
|
||||
// 0: 'minecraft:overworld',
|
||||
// 1: 'minecraft:end'
|
||||
// }
|
||||
|
||||
// const parseGameMode = gameModeBits => gameModes[(gameModeBits & 0b11)] // lower two bits
|
||||
|
||||
interface GameOptions {
|
||||
brand?: string;
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot, options: GameOptions = {}) {
|
||||
// function getBrandCustomChannelName () {
|
||||
// if (bot.supportFeature('customChannelMCPrefixed')) {
|
||||
// return 'MC|Brand'
|
||||
// } else if (bot.supportFeature('customChannelIdentifier')) {
|
||||
// return 'minecraft:brand'
|
||||
// }
|
||||
// throw new Error('Unsupported brand channel name')
|
||||
// }
|
||||
|
||||
function handleItemRegistryPacketData(packet: any) {
|
||||
if (bot.registry.handleItemRegistry) {
|
||||
bot.registry.handleItemRegistry(packet);
|
||||
(bot as any).item_registry_task.finish();
|
||||
(bot as any).item_registry_task = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleStartGamePacketData(packet: any) {
|
||||
bot.game.levelType = packet.generator ?? (packet.generator === 2 ? 'flat' : 'default');
|
||||
bot.game.hardcore = packet.player_gamemode === 'hardcore';
|
||||
bot.game.gameMode = packet.player_gamemode;
|
||||
|
||||
bot.game.dimension = packet.dimension;
|
||||
|
||||
bot.registry.handleStartGame(packet);
|
||||
if (packet.itemStates) {
|
||||
(bot as any).item_registry_task.finish();
|
||||
(bot as any).item_registry_task = null;
|
||||
}
|
||||
|
||||
bot._client.queue('serverbound_loading_screen', {
|
||||
type: 1,
|
||||
});
|
||||
bot._client.queue('serverbound_loading_screen', {
|
||||
type: 2,
|
||||
});
|
||||
bot._client.queue('interact', {
|
||||
action_id: 'mouse_over_entity',
|
||||
target_entity_id: 0n,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
});
|
||||
bot._client.queue('set_local_player_as_initialized', {
|
||||
runtime_entity_id: `${bot.entity.id}`,
|
||||
});
|
||||
|
||||
// CODE BELOW MIGHT BE WRONG
|
||||
// if (bot.supportFeature('dimensionIsAnInt')) {
|
||||
// bot.game.dimension = dimensionNames[packet.dimension]
|
||||
// } else if (bot.supportFeature('dimensionIsAString')) {
|
||||
// bot.game.dimension = packet.dimension
|
||||
// } else if (bot.supportFeature('dimensionIsAWorld')) {
|
||||
// bot.game.dimension = packet.worldName
|
||||
// } else {
|
||||
// throw new Error('Unsupported dimension type in start_game packet')
|
||||
// }
|
||||
|
||||
// if (packet.dimensionCodec) {
|
||||
// bot.registry.loadDimensionCodec(packet.dimensionCodec)
|
||||
// }
|
||||
// CODE BELOW MIGHT BE WRONG FOR BEDROCK
|
||||
// if (bot.supportFeature('dimensionDataInCodec')) { // 1.19+
|
||||
// if (packet.world_gamemode) { // login
|
||||
// bot.game.dimension = packet.worldType.replace('minecraft:', '')
|
||||
// const { minY, height } = bot.registry.dimensionsByName[bot.game.dimension]
|
||||
// bot.game.minY = minY
|
||||
// bot.game.height = height
|
||||
// } else if (packet.dimension) { // respawn
|
||||
// bot.game.dimension = packet.dimension.replace('minecraft:', '')
|
||||
// }
|
||||
// } else if (bot.supportFeature('dimensionDataIsAvailable')) { // 1.18
|
||||
//console.log(bot.registry.dimensionsByName)
|
||||
//const { minY, height } = bot.registry.dimensionsByName[bot.game.dimension]
|
||||
// CODE BELOW SHOULD BE OPTIMIZED FOR BEDROCK
|
||||
if (bot.registry.dimensionsByName) {
|
||||
const { minY, height } = bot.registry.dimensionsByName[bot.game.dimension];
|
||||
bot.game.minY = minY;
|
||||
bot.game.height = height;
|
||||
} else {
|
||||
// depends on game version
|
||||
bot.game.minY = -64;
|
||||
bot.game.height = 384;
|
||||
}
|
||||
if (packet.difficulty) {
|
||||
bot.game.difficulty = difficultyNames[packet.difficulty];
|
||||
}
|
||||
}
|
||||
|
||||
bot.game = {} as any;
|
||||
(bot as any).item_registry_task = createTask();
|
||||
|
||||
// const brandChannel = getBrandCustomChannelName()
|
||||
// bot._client.registerChannel(brandChannel, ['string', []])
|
||||
|
||||
bot._client.on('start_game', (packet) => {
|
||||
handleStartGamePacketData(packet);
|
||||
|
||||
// bot.game.maxPlayers = packet.maxPlayers
|
||||
// if (packet.enableRespawnScreen) {
|
||||
// bot.game.enableRespawnScreen = packet.enableRespawnScreen
|
||||
// }
|
||||
// if (packet.viewDistance) {
|
||||
// bot.game.serverViewDistance = packet.viewDistance
|
||||
// }
|
||||
|
||||
bot.emit('login');
|
||||
bot.emit('game');
|
||||
|
||||
// varint length-prefixed string as data
|
||||
//bot._client.writeChannel(brandChannel, options.brand)
|
||||
});
|
||||
|
||||
bot._client.on('item_registry', (packet) => {
|
||||
handleItemRegistryPacketData(packet);
|
||||
});
|
||||
|
||||
bot._client.on('respawn', (packet) => {
|
||||
//handleRespawnPacketData(packet)
|
||||
bot.emit('game');
|
||||
});
|
||||
|
||||
// bot._client.on('game_state_change', (packet) => {
|
||||
// if (packet?.reason === 4 && packet?.gameMode === 1) {
|
||||
// bot._client.write('client_command', { action: 0 })
|
||||
// }
|
||||
// if (packet.reason === 3) {
|
||||
// bot.game.gameMode = parseGameMode(packet.gameMode)
|
||||
// bot.emit('game')
|
||||
// }
|
||||
// })
|
||||
|
||||
// bot._client.on('difficulty', (packet) => {
|
||||
// bot.game.difficulty = difficultyNames[packet.difficulty]
|
||||
// })
|
||||
|
||||
// bot._client.on(brandChannel, (serverBrand) => {
|
||||
// bot.game.serverBrand = serverBrand
|
||||
// })
|
||||
|
||||
// mimic the vanilla 1.17 client to prevent anticheat kicks
|
||||
// bot._client.on('ping', (data) => {
|
||||
// bot._client.write('pong', {
|
||||
// id: data.id
|
||||
// })
|
||||
// })
|
||||
}
|
||||
131
bridge/lib/mineflayer/lib/bedrockPlugins/health.mts
Normal file
131
bridge/lib/mineflayer/lib/bedrockPlugins/health.mts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
|
||||
interface HealthOptions {
|
||||
respawn?: boolean;
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot, options: HealthOptions = {}) {
|
||||
bot.isAlive = true;
|
||||
|
||||
// undocumented, bedrock-specific fields
|
||||
(bot as any).respawnLocked = true; // lock respawn before player initialized
|
||||
(bot as any).awaitingRespawn = false; // used to prevent double events
|
||||
(bot as any).respawnQueued = false; // used to prevent sending multiple respawn packets
|
||||
(bot as any).spawned = false; // keep track of spawned state
|
||||
(bot as any).deathHandled = false;
|
||||
|
||||
bot._client.on('respawn', (packet) => {
|
||||
if (packet.state === 0 && !(bot as any).awaitingRespawn && !(bot as any).respawnLocked) {
|
||||
// respawn avaliable
|
||||
(bot as any).awaitingRespawn = true;
|
||||
bot.emit('respawn');
|
||||
}
|
||||
if (packet.state === 1) {
|
||||
// ready state
|
||||
bot.entity.position = new Vec3(packet.position.x, packet.position.y, packet.position.z);
|
||||
(bot as any).respawnQueued = false;
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('play_status', (packet) => {
|
||||
// after login
|
||||
if (packet.status === 'player_spawn') {
|
||||
(bot as any).respawnLocked = false;
|
||||
handleSpawn();
|
||||
bot.emit('entitySpawn', bot.entity);
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('entity_event', (packet) => {
|
||||
// after respawn button press
|
||||
if (packet.runtime_entity_id !== bot.entity.id) return;
|
||||
if (packet.event_id === 'death_animation') {
|
||||
handleDeath();
|
||||
}
|
||||
if (packet.event_id === 'respawn') {
|
||||
handleSpawn();
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('set_health', (packet) => {
|
||||
bot.health = packet.health;
|
||||
bot.food = 20;
|
||||
bot.foodSaturation = 5;
|
||||
if (packet.health > 0) {
|
||||
handleSpawn();
|
||||
}
|
||||
bot.emit('health');
|
||||
});
|
||||
|
||||
bot.on('entityAttributes', (entity) => {
|
||||
if (entity !== bot.entity) return;
|
||||
if (!entity.attributes) return;
|
||||
|
||||
if ('minecraft:player.hunger' in entity.attributes) {
|
||||
bot.food = entity.attributes['minecraft:player.hunger'].value;
|
||||
}
|
||||
|
||||
if ('minecraft:player.saturation' in entity.attributes) {
|
||||
bot.foodSaturation = entity.attributes['minecraft:player.saturation'].value;
|
||||
}
|
||||
|
||||
let health_changed = false;
|
||||
|
||||
if ('minecraft:health' in entity.attributes) {
|
||||
health_changed = bot.health !== entity.attributes['minecraft:health'].value;
|
||||
bot.health = entity.attributes['minecraft:health'].value;
|
||||
}
|
||||
|
||||
if (health_changed) {
|
||||
bot.emit('health');
|
||||
}
|
||||
|
||||
if (bot.health <= 0) {
|
||||
if (bot.isAlive) {
|
||||
handleDeath();
|
||||
if (options.respawn) {
|
||||
respawn();
|
||||
}
|
||||
}
|
||||
} else if (bot.health > 0 && !bot.isAlive) {
|
||||
handleSpawn();
|
||||
}
|
||||
});
|
||||
|
||||
function handleSpawn() {
|
||||
if (!(bot as any).spawned && ((bot as any).awaitingRespawn || (bot as any).respawnLocked)) {
|
||||
(bot as any).awaitingRespawn = false;
|
||||
bot.isAlive = true;
|
||||
(bot as any).spawned = true;
|
||||
(bot as any).deathHandled = false;
|
||||
bot.emit('spawn');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeath() {
|
||||
if (!(bot as any).deathHandled) {
|
||||
bot.isAlive = false;
|
||||
(bot as any).spawned = false;
|
||||
(bot as any).deathHandled = true;
|
||||
bot.emit('death');
|
||||
}
|
||||
}
|
||||
|
||||
const respawn = () => {
|
||||
if (bot.isAlive) return;
|
||||
if ((bot as any).respawnQueued) return;
|
||||
bot._client.write('respawn', {
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
state: 2,
|
||||
runtime_entity_id: bot.entity.id,
|
||||
});
|
||||
(bot as any).respawnQueued = true;
|
||||
};
|
||||
|
||||
bot.respawn = respawn;
|
||||
}
|
||||
103
bridge/lib/mineflayer/lib/bedrockPlugins/input-data-service.mts
Normal file
103
bridge/lib/mineflayer/lib/bedrockPlugins/input-data-service.mts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
interface ControlState {
|
||||
forward: boolean;
|
||||
back: boolean;
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
jump: boolean;
|
||||
sprint: boolean;
|
||||
sneak: boolean;
|
||||
}
|
||||
|
||||
interface InputData {
|
||||
down_left: boolean;
|
||||
down_right: boolean;
|
||||
down: boolean;
|
||||
up: boolean;
|
||||
left: boolean;
|
||||
right: boolean;
|
||||
up_left: boolean;
|
||||
up_right: boolean;
|
||||
sprint_down: boolean;
|
||||
sprinting: boolean;
|
||||
start_sprinting: boolean;
|
||||
stop_sprinting: boolean;
|
||||
}
|
||||
|
||||
export class InputDataService {
|
||||
#data: InputData = {
|
||||
down_left: false,
|
||||
down_right: false,
|
||||
down: false,
|
||||
up: false,
|
||||
left: false,
|
||||
right: false,
|
||||
up_left: false,
|
||||
up_right: false,
|
||||
sprint_down: false,
|
||||
sprinting: false,
|
||||
start_sprinting: false,
|
||||
stop_sprinting: false,
|
||||
};
|
||||
|
||||
#prevControlState: ControlState = {
|
||||
forward: false,
|
||||
back: false,
|
||||
left: false,
|
||||
right: false,
|
||||
jump: false,
|
||||
sprint: false,
|
||||
sneak: false,
|
||||
};
|
||||
|
||||
update(controlState: ControlState) {
|
||||
const prevData = { ...this.#data };
|
||||
|
||||
this.#data.up = controlState.forward;
|
||||
this.#data.down = controlState.back;
|
||||
this.#data.right = controlState.right;
|
||||
this.#data.left = controlState.left;
|
||||
this.#data.up_right = controlState.forward && controlState.right;
|
||||
this.#data.up_left = controlState.forward && controlState.left;
|
||||
|
||||
if (this.#prevControlState.sprint !== controlState.sprint) {
|
||||
this.#data.sprint_down = controlState.sprint;
|
||||
this.#data.sprinting = controlState.sprint;
|
||||
}
|
||||
|
||||
this.#data.start_sprinting = !this.#isMoving(this.#prevControlState) && this.#isMoving(controlState);
|
||||
this.#data.stop_sprinting = this.#isMoving(this.#prevControlState) && !this.#isMoving(controlState);
|
||||
|
||||
this.#prevControlState = { ...controlState };
|
||||
return {
|
||||
diff: this.#diff(this.#data, prevData),
|
||||
};
|
||||
}
|
||||
|
||||
#isMoving(controlState: ControlState) {
|
||||
return (controlState.forward && !controlState.back) || (controlState.back && !controlState.forward) || (controlState.left && !controlState.right) || (controlState.right && !controlState.left);
|
||||
}
|
||||
|
||||
#diff(obj1: Record<string, any>, obj2: Record<string, any>): Record<string, any> {
|
||||
var result: Record<string, any> = {};
|
||||
var change;
|
||||
for (var key in obj1) {
|
||||
if (typeof obj2[key] == 'object' && typeof obj1[key] == 'object') {
|
||||
change = this.#diff(obj1[key], obj2[key]);
|
||||
if (this.#isEmptyObject(change) === false) {
|
||||
result[key] = change;
|
||||
}
|
||||
} else if (obj2[key] != obj1[key]) {
|
||||
result[key] = obj1[key] ?? obj2[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#isEmptyObject(obj: Record<string, any>) {
|
||||
var name;
|
||||
for (name in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
1635
bridge/lib/mineflayer/lib/bedrockPlugins/inventory.mts
Normal file
1635
bridge/lib/mineflayer/lib/bedrockPlugins/inventory.mts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,81 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
//0-8, null = uninitialized
|
||||
// which quick bar slot is selected
|
||||
bot.quickBarSlot = null;
|
||||
// TODO: make it load into slots properly
|
||||
bot.inventory = {
|
||||
slots: [],
|
||||
};
|
||||
bot.heldItem = {
|
||||
// null?
|
||||
network_id: 0,
|
||||
};
|
||||
|
||||
bot.selectedSlot = null;
|
||||
bot.usingHeldItem = false;
|
||||
|
||||
bot._client.on('inventory_content', (packet) => {
|
||||
if (!bot.inventory)
|
||||
bot.inventory = {
|
||||
slots: [],
|
||||
};
|
||||
bot.inventory[packet.window_id] = packet.input;
|
||||
});
|
||||
|
||||
bot._client.on('inventory_slot', (packet) => {
|
||||
if (!bot.inventory)
|
||||
bot.inventory = {
|
||||
slots: [],
|
||||
};
|
||||
bot.inventory[packet.window_id] = [];
|
||||
bot.inventory[packet.window_id][packet.slot] = packet.item;
|
||||
if (bot.inventory && bot.selectedSlot) {
|
||||
bot.heldItem = bot.inventory.inventory[bot.selectedSlot];
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('player_hotbar', (packet) => {
|
||||
if (packet.select_slot) {
|
||||
bot.selectedSlot = packet.selected_slot;
|
||||
if (bot.inventory.inventory) {
|
||||
bot.heldItem = bot.inventory[packet.window_id][packet.selected_slot];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function useItem(slotNumber: number) {
|
||||
bot.usingHeldItem = true;
|
||||
bot._client.write('inventory_transaction', {
|
||||
transaction: {
|
||||
legacy: {
|
||||
legacy_request_id: 0,
|
||||
},
|
||||
transaction_type: 'item_use',
|
||||
actions: [],
|
||||
transaction_data: {
|
||||
action_type: 'click_air',
|
||||
block_position: { x: 0, y: 0, z: 0 },
|
||||
face: bot.entity.yaw, // facing?
|
||||
hotbar_slot: slotNumber + 1,
|
||||
held_item: bot.inventory.inventory[slotNumber - 1],
|
||||
player_pos: bot.entity.position,
|
||||
click_pos: { x: 0, y: 0, z: 0 },
|
||||
block_runtime_id: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
function swingArm(arm = 'right', showHand = true) {
|
||||
//const hand = arm === 'right' ? 0 : 1
|
||||
const packet = {
|
||||
action_id: 'swing_arm',
|
||||
runtime_entity_id: (bot.entity as any).runtime_entity_id,
|
||||
};
|
||||
bot._client.write('animate', packet);
|
||||
}
|
||||
|
||||
bot.swingArm = swingArm;
|
||||
bot.useItem = useItem;
|
||||
}
|
||||
12
bridge/lib/mineflayer/lib/bedrockPlugins/kick.mts
Normal file
12
bridge/lib/mineflayer/lib/bedrockPlugins/kick.mts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot._client.on('disconnect', (packet) => {
|
||||
const kicked = packet.reason.indexOf('kick') !== -1;
|
||||
bot.emit('kicked', packet.message ?? packet.reason, kicked);
|
||||
});
|
||||
bot.quit = (reason) => {
|
||||
reason = reason ?? 'disconnect.quitting';
|
||||
bot.end(reason);
|
||||
};
|
||||
}
|
||||
18
bridge/lib/mineflayer/lib/bedrockPlugins/particle.mts
Normal file
18
bridge/lib/mineflayer/lib/bedrockPlugins/particle.mts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// REQ BEDROCK PARTICLES IMPLEMENTATION
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const Particle = require('../particle')(bot.registry);
|
||||
|
||||
bot._client.on('level_event', (packet) => {
|
||||
if (packet.event.startsWith('particle')) {
|
||||
bot.emit('particle', new Particle(packet.event, packet.position, new Vec3(0, 0, 0)));
|
||||
}
|
||||
});
|
||||
bot._client.on('spawn_particle_effect', (packet) => {
|
||||
bot.emit('particle', new Particle(packet.particle_name, packet.position, new Vec3(0, 0, 0)));
|
||||
});
|
||||
}
|
||||
601
bridge/lib/mineflayer/lib/bedrockPlugins/physics.mts
Normal file
601
bridge/lib/mineflayer/lib/bedrockPlugins/physics.mts
Normal file
|
|
@ -0,0 +1,601 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
import assert from 'assert';
|
||||
import math from '../math.js';
|
||||
import conv from '../conversions.js';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { createDoneTask, createTask } from '../promise_utils.js';
|
||||
|
||||
import { InputDataService } from './input-data-service.mts';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const { Physics, PlayerState } = require('./attribute-patch.js');
|
||||
|
||||
const PI = Math.PI;
|
||||
const PI_2 = Math.PI * 2;
|
||||
const PHYSICS_INTERVAL_MS = 50;
|
||||
const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000;
|
||||
|
||||
interface PhysicsOptions {
|
||||
physicsEnabled?: boolean;
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot, { physicsEnabled }: PhysicsOptions = {}) {
|
||||
const world = {
|
||||
getBlock: (pos: Vec3) => {
|
||||
return bot.blockAt(pos, false);
|
||||
},
|
||||
};
|
||||
const physics = Physics(bot.registry, world);
|
||||
physics.sprintingUUID = 'd208fc00-42aa-4aad-9276-d5446530de43';
|
||||
physics.sprintSpeed = Math.fround(physics.sprintSpeed);
|
||||
physics.playerSpeed = Math.fround(physics.playerSpeed);
|
||||
|
||||
const positionUpdateSentEveryTick = true; // depends on server settings, non-auth movement sends updates only when pos/rot changes
|
||||
|
||||
bot.jumpQueued = false;
|
||||
bot.jumpTicks = 0; // autojump cooldown
|
||||
|
||||
const controlState = {
|
||||
forward: false,
|
||||
back: false,
|
||||
left: false,
|
||||
right: false,
|
||||
jump: false,
|
||||
sprint: false,
|
||||
sneak: false,
|
||||
};
|
||||
let lastSentJumping = false;
|
||||
let lastSentSprinting = false;
|
||||
let lastSentSneaking = false;
|
||||
let lastSentYaw: number | null = null;
|
||||
let lastSentPitch: number | null = null;
|
||||
let lastSentHeadYaw: number | null = null;
|
||||
|
||||
let doPhysicsTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let lastPhysicsFrameTime: number | null = null;
|
||||
let shouldUsePhysics = false;
|
||||
bot.physicsEnabled = physicsEnabled ?? true;
|
||||
|
||||
let tick = 0n;
|
||||
|
||||
const inputDataService = new InputDataService();
|
||||
|
||||
const lastSent: protocolTypes.packet_player_auth_input = {
|
||||
pitch: 0,
|
||||
yaw: 0, // change
|
||||
position: new Vec3(0, 0, 0), // change
|
||||
move_vector: { x: 0, z: 0 }, // change
|
||||
head_yaw: 0, // change
|
||||
input_data: {
|
||||
ascend: false,
|
||||
descend: false,
|
||||
north_jump: false,
|
||||
jump_down: false,
|
||||
sprint_down: false,
|
||||
change_height: false,
|
||||
jumping: false,
|
||||
auto_jumping_in_water: false,
|
||||
sneaking: false,
|
||||
sneak_down: false,
|
||||
up: false,
|
||||
down: false,
|
||||
left: false,
|
||||
right: false,
|
||||
up_left: false,
|
||||
up_right: false,
|
||||
want_up: false,
|
||||
want_down: false,
|
||||
want_down_slow: false,
|
||||
want_up_slow: false,
|
||||
sprinting: false,
|
||||
ascend_block: false,
|
||||
descend_block: false,
|
||||
sneak_toggle_down: false,
|
||||
persist_sneak: false,
|
||||
start_sprinting: false,
|
||||
stop_sprinting: false,
|
||||
start_sneaking: false,
|
||||
stop_sneaking: false,
|
||||
start_swimming: false,
|
||||
stop_swimming: false,
|
||||
start_jumping: false,
|
||||
start_gliding: false,
|
||||
stop_gliding: false,
|
||||
item_interact: false,
|
||||
block_action: false,
|
||||
item_stack_request: false,
|
||||
handled_teleport: false,
|
||||
emoting: false,
|
||||
missed_swing: false,
|
||||
start_crawling: false,
|
||||
stop_crawling: false,
|
||||
start_flying: false,
|
||||
stop_flying: false,
|
||||
received_server_data: false,
|
||||
client_predicted_vehicle: false,
|
||||
paddling_left: false,
|
||||
paddling_right: false,
|
||||
block_breaking_delay_enabled: true,
|
||||
horizontal_collision: false,
|
||||
vertical_collision: true,
|
||||
down_left: false,
|
||||
down_right: false,
|
||||
start_using_item: false,
|
||||
camera_relative_movement_enabled: false,
|
||||
rot_controlled_by_move_direction: false,
|
||||
start_spin_attack: false,
|
||||
stop_spin_attack: false,
|
||||
hotbar_only_touch: false,
|
||||
jump_released_raw: false,
|
||||
jump_pressed_raw: false,
|
||||
jump_current_raw: false,
|
||||
sneak_released_raw: false,
|
||||
sneak_pressed_raw: false,
|
||||
sneak_current_raw: false,
|
||||
},
|
||||
input_mode: 'mouse',
|
||||
play_mode: 'screen',
|
||||
interaction_model: 'touch',
|
||||
interact_rotation: { x: 0, z: 0 },
|
||||
// gaze_direction: undefined,
|
||||
tick: tick,
|
||||
delta: new Vec3(0, 0, 0), // velocity change
|
||||
transaction: undefined,
|
||||
block_action: undefined,
|
||||
analogue_move_vector: { x: 0, z: 0 }, // for versions (1.19.80) > 1.19.30
|
||||
camera_orientation: { x: 0, y: 0, z: 0 },
|
||||
raw_move_vector: { x: 0, z: 0 },
|
||||
};
|
||||
|
||||
// This function should be executed each tick (every 0.05 seconds)
|
||||
// How it works: https://gafferongames.com/post/fix_your_timestep/
|
||||
let timeAccumulator = 0;
|
||||
let subchunkContainingPlayer: Vec3 | null = null;
|
||||
|
||||
function getChunkCoordinates(pos: Vec3) {
|
||||
let chunkX = Math.floor(pos.x / 16);
|
||||
let chunkZ = Math.floor(pos.z / 16);
|
||||
let subchunkY = Math.floor(pos.y / 16);
|
||||
return new Vec3(chunkX, subchunkY, chunkZ);
|
||||
}
|
||||
|
||||
function updateCamera() {
|
||||
if ((bot as any).cameraState) {
|
||||
const maxPitch = 0.5 * Math.PI;
|
||||
const minPitch = -0.5 * Math.PI;
|
||||
const pitch = bot.entity.pitch + (bot as any).cameraState.pitch * 300;
|
||||
bot.look(bot.entity.yaw + (bot as any).cameraState.yaw * 300, Math.max(minPitch, Math.min(maxPitch, pitch)), true);
|
||||
}
|
||||
}
|
||||
|
||||
function doPhysics() {
|
||||
const now = performance.now();
|
||||
const deltaSeconds = (now - lastPhysicsFrameTime!) / 1000;
|
||||
lastPhysicsFrameTime = now;
|
||||
|
||||
timeAccumulator += deltaSeconds;
|
||||
|
||||
while (timeAccumulator >= PHYSICS_TIMESTEP) {
|
||||
if (bot.physicsEnabled && shouldUsePhysics) {
|
||||
updateCamera();
|
||||
physics.simulatePlayer(new PlayerState(bot, controlState), world).apply(bot);
|
||||
let subchunkContainingPlayerNew = getChunkCoordinates(bot.entity.position);
|
||||
if (subchunkContainingPlayerNew !== subchunkContainingPlayer) {
|
||||
subchunkContainingPlayer = subchunkContainingPlayerNew;
|
||||
bot.emit('subchunkContainingPlayerChanged', subchunkContainingPlayerNew);
|
||||
}
|
||||
bot.emit('physicsTick');
|
||||
bot.emit('physicTick'); // Deprecated, only exists to support old plugins. May be removed in the future
|
||||
}
|
||||
updatePosition(PHYSICS_TIMESTEP);
|
||||
timeAccumulator -= PHYSICS_TIMESTEP;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
clearInterval(doPhysicsTimer!);
|
||||
doPhysicsTimer = null;
|
||||
}
|
||||
|
||||
let player_auth_input_transaction: any = {};
|
||||
let tasks_queue: Array<() => void> = [];
|
||||
|
||||
bot.sendPlayerAuthInputTransaction = async function (params = {}, wait = true) {
|
||||
Object.assign(player_auth_input_transaction, params);
|
||||
|
||||
// if (wait) return await once(bot, 'updatePlayerPosition')
|
||||
if (wait) await new Promise<void>((resolve) => tasks_queue.push(resolve)); // Fix this, temp workaround for too many listeners
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function updateCameraOrentation() {
|
||||
const pitchRadians = lastSent.pitch * (Math.PI / 180);
|
||||
const yawRadians = lastSent.yaw * (Math.PI / 180);
|
||||
|
||||
lastSent.camera_orientation.x = -Math.cos(pitchRadians) * Math.sin(yawRadians);
|
||||
lastSent.camera_orientation.y = -Math.sin(pitchRadians);
|
||||
lastSent.camera_orientation.z = Math.cos(pitchRadians) * Math.cos(yawRadians);
|
||||
}
|
||||
|
||||
function updateInteractRotation() {
|
||||
lastSent.interact_rotation.x = lastSent.pitch;
|
||||
lastSent.interact_rotation.z = lastSent.head_yaw;
|
||||
}
|
||||
|
||||
function updateTransactions() {
|
||||
if (player_auth_input_transaction?.transaction) {
|
||||
lastSent.input_data.item_interact = true;
|
||||
lastSent.transaction = player_auth_input_transaction.transaction;
|
||||
delete player_auth_input_transaction.transaction;
|
||||
} else {
|
||||
lastSent.input_data.item_interact = false;
|
||||
lastSent.transaction = undefined;
|
||||
}
|
||||
|
||||
if (player_auth_input_transaction?.block_action) {
|
||||
lastSent.input_data.block_action = true;
|
||||
lastSent.block_action = player_auth_input_transaction.block_action;
|
||||
delete player_auth_input_transaction.block_action;
|
||||
} else {
|
||||
lastSent.input_data.block_action = false;
|
||||
lastSent.block_action = undefined;
|
||||
}
|
||||
if (player_auth_input_transaction?.item_stack_request) {
|
||||
lastSent.input_data.item_stack_request = true;
|
||||
lastSent.item_stack_request = player_auth_input_transaction.item_stack_request.requests[0];
|
||||
delete player_auth_input_transaction.item_stack_request;
|
||||
} else {
|
||||
lastSent.input_data.item_stack_request = false;
|
||||
delete lastSent.item_stack_request;
|
||||
}
|
||||
for (const resolve of tasks_queue) resolve();
|
||||
tasks_queue = [];
|
||||
}
|
||||
|
||||
function updateMoveVector() {
|
||||
let moveVector = { x: 0, z: 0 };
|
||||
let max_value = controlState.sneak ? 0.3 : 1;
|
||||
|
||||
if (controlState.forward) {
|
||||
moveVector.z += max_value;
|
||||
}
|
||||
if (controlState.back) {
|
||||
moveVector.z -= max_value;
|
||||
}
|
||||
if (controlState.left) {
|
||||
moveVector.x -= max_value;
|
||||
}
|
||||
if (controlState.right) {
|
||||
moveVector.x += max_value;
|
||||
}
|
||||
|
||||
let magnitude = (moveVector.x ** 2 + moveVector.z ** 2) ** 0.5;
|
||||
|
||||
if (magnitude > 1) {
|
||||
moveVector.x /= magnitude;
|
||||
moveVector.z /= magnitude;
|
||||
}
|
||||
|
||||
lastSent.move_vector = moveVector;
|
||||
lastSent.raw_move_vector = moveVector;
|
||||
}
|
||||
|
||||
function updateAuthoritativeMovementFlags() {
|
||||
const inputDataDiff = inputDataService.update(controlState);
|
||||
|
||||
lastSent.input_data.up = controlState.forward;
|
||||
lastSent.input_data.down = controlState.back;
|
||||
lastSent.input_data.right = controlState.right;
|
||||
lastSent.input_data.left = controlState.left;
|
||||
|
||||
lastSent.input_data.up_right = controlState.forward && controlState.right;
|
||||
lastSent.input_data.up_left = controlState.forward && controlState.left;
|
||||
|
||||
if (lastSent.input_data.start_jumping === controlState.jump) {
|
||||
lastSent.input_data.start_jumping = false;
|
||||
lastSent.input_data.jump_pressed_raw = false;
|
||||
}
|
||||
if (controlState.jump !== lastSentJumping) {
|
||||
lastSentJumping = controlState.jump;
|
||||
lastSent.input_data.jumping = controlState.jump;
|
||||
lastSent.input_data.want_up = controlState.jump;
|
||||
lastSent.input_data.jump_down = controlState.jump;
|
||||
lastSent.input_data.start_jumping = controlState.jump;
|
||||
lastSent.input_data.jump_current_raw = controlState.jump;
|
||||
lastSent.input_data.jump_pressed_raw = controlState.jump;
|
||||
lastSent.input_data.jump_released_raw = !controlState.jump;
|
||||
}
|
||||
if (controlState.sprint !== lastSentSprinting) {
|
||||
lastSentSprinting = controlState.sprint;
|
||||
lastSent.input_data.sprint_down = controlState.sprint;
|
||||
lastSent.input_data.sprinting = controlState.sprint;
|
||||
lastSent.input_data.stop_sprinting = !controlState.sprint;
|
||||
}
|
||||
if (controlState.sneak !== lastSentSneaking) {
|
||||
lastSentSneaking = controlState.sneak;
|
||||
lastSent.input_data.sneak_down = controlState.sneak;
|
||||
lastSent.input_data.sneaking = controlState.sneak;
|
||||
lastSent.input_data.stop_sneaking = !controlState.sneak;
|
||||
lastSent.input_data.sneak_current_raw = controlState.sneak;
|
||||
lastSent.input_data.sneak_pressed_raw = controlState.sneak;
|
||||
lastSent.input_data.sneak_released_raw = !controlState.sneak;
|
||||
}
|
||||
|
||||
lastSent.input_data.vertical_collision = bot.entity.isCollidedVertically;
|
||||
lastSent.input_data.horizontal_collision = bot.entity.isCollidedHorizontally;
|
||||
for (const [property, value] of Object.entries(inputDataDiff.diff)) {
|
||||
lastSent.input_data[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function sendMovementUpdate(position: Vec3, yaw: number, pitch: number) {
|
||||
lastSent.tick = lastSent.tick + BigInt(1);
|
||||
|
||||
// sends data, no logic
|
||||
const oldPos = new Vec3(lastSent.x, lastSent.y, lastSent.z);
|
||||
|
||||
lastSent.delta = bot.entity.velocity;
|
||||
|
||||
lastSent.position = new Vec3(position.x, position.y + bot.entity.height, position.z);
|
||||
|
||||
lastSent.yaw = yaw;
|
||||
lastSent.pitch = pitch;
|
||||
lastSent.head_yaw = yaw;
|
||||
lastSent.item_stack_request;
|
||||
|
||||
updateCameraOrentation();
|
||||
updateInteractRotation();
|
||||
updateTransactions();
|
||||
updateMoveVector();
|
||||
updateAuthoritativeMovementFlags();
|
||||
|
||||
bot._client.write('player_auth_input', lastSent);
|
||||
|
||||
bot.emit('move', oldPos);
|
||||
}
|
||||
|
||||
function deltaYaw(yaw1: number, yaw2: number | null) {
|
||||
let dYaw = (yaw1 - (yaw2 ?? 0)) % PI_2;
|
||||
if (dYaw < -PI) dYaw += PI_2;
|
||||
else if (dYaw > PI) dYaw -= PI_2;
|
||||
|
||||
return dYaw;
|
||||
}
|
||||
|
||||
function updatePosition(dt: number) {
|
||||
// bot.isAlive = true // TODO: MOVE TO HEALTH
|
||||
// If you're dead, you're probably on the ground though ...
|
||||
if (!bot.isAlive) bot.entity.onGround = true;
|
||||
|
||||
// Increment the yaw in baby steps so that notchian clients (not the server) can keep up.
|
||||
const dYaw = deltaYaw(bot.entity.yaw, lastSentYaw);
|
||||
const dPitch = bot.entity.pitch - (lastSentPitch || 0);
|
||||
|
||||
// Vanilla doesn't clamp yaw, so we don't want to do it either
|
||||
const maxDeltaYaw = dt * physics.yawSpeed;
|
||||
const maxDeltaPitch = dt * physics.pitchSpeed;
|
||||
|
||||
lastSentYaw = (lastSentYaw ?? 0) + math.clamp(-maxDeltaYaw, dYaw, maxDeltaYaw);
|
||||
lastSentPitch = (lastSentPitch ?? 0) + math.clamp(-maxDeltaPitch, dPitch, maxDeltaPitch);
|
||||
|
||||
const yaw = Math.fround(conv.toNotchianYaw(lastSentYaw));
|
||||
const pitch = Math.fround(conv.toNotchianPitch(lastSentPitch));
|
||||
const position = bot.entity.position;
|
||||
|
||||
if (!positionUpdateSentEveryTick) {
|
||||
// in case with non-auth movement
|
||||
// Only send a position update if necessary, select the appropriate packet
|
||||
const positionUpdated = lastSent.x !== position.x || lastSent.y !== position.y || lastSent.z !== position.z;
|
||||
// bot.isAlive = true // GET IT TO THE BOT
|
||||
const lookUpdated = lastSent.yaw !== yaw || lastSent.pitch !== pitch;
|
||||
|
||||
if ((positionUpdated || lookUpdated) && bot.isAlive) {
|
||||
sendMovementUpdate(position, yaw, pitch);
|
||||
}
|
||||
} else {
|
||||
sendMovementUpdate(position, yaw, pitch);
|
||||
}
|
||||
}
|
||||
|
||||
bot.physics = physics;
|
||||
|
||||
function getMetadataForFlag(flag: string, state: boolean) {
|
||||
let metadata: any = {
|
||||
key: 'flags',
|
||||
type: 'long',
|
||||
value: {},
|
||||
};
|
||||
|
||||
metadata.value[flag] = state;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
bot.setControlState = (control: string, state: boolean) => {
|
||||
assert.ok(control in controlState, `invalid control: ${control}`);
|
||||
assert.ok(typeof state === 'boolean', `invalid state: ${state}`);
|
||||
if ((controlState as any)[control] === state) return;
|
||||
(controlState as any)[control] = state;
|
||||
if (control === 'jump' && state) {
|
||||
bot.jumpQueued = true;
|
||||
}
|
||||
if (bot.registry.version['<=']('1.19.1')) {
|
||||
// // version might be wrong
|
||||
if (['sneak', 'sprint'].indexOf(control) !== -1) {
|
||||
let packet: any = {
|
||||
runtime_entity_id: bot.entity.id,
|
||||
metadata: [getMetadataForFlag('sneaking', state)],
|
||||
tick: 0,
|
||||
};
|
||||
if (bot.registry.version['>=']('1.19.1')) {
|
||||
// version might be wrong
|
||||
packet['properties'] = {
|
||||
ints: [],
|
||||
floats: [],
|
||||
};
|
||||
}
|
||||
bot._client.write('set_entity_data', packet);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bot.getControlState = (control: string) => {
|
||||
assert.ok(control in controlState, `invalid control: ${control}`);
|
||||
return (controlState as any)[control];
|
||||
};
|
||||
|
||||
bot.clearControlStates = () => {
|
||||
for (const control in controlState) {
|
||||
bot.setControlState(control, false);
|
||||
}
|
||||
};
|
||||
|
||||
bot.controlState = {} as any;
|
||||
|
||||
for (const control of Object.keys(controlState)) {
|
||||
Object.defineProperty(bot.controlState, control, {
|
||||
get() {
|
||||
return (controlState as any)[control];
|
||||
},
|
||||
set(state) {
|
||||
bot.setControlState(control, state);
|
||||
return state;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let lookingTask = createDoneTask();
|
||||
|
||||
bot.on('move', () => {
|
||||
if (!lookingTask.done && Math.abs(deltaYaw(bot.entity.yaw, lastSentYaw)) < 0.001) {
|
||||
lookingTask.finish();
|
||||
}
|
||||
});
|
||||
|
||||
bot.look = async (yaw: number, pitch: number, force?: boolean) => {
|
||||
force = true;
|
||||
if (!lookingTask.done) {
|
||||
lookingTask.finish(); // finish the previous one
|
||||
}
|
||||
lookingTask = createTask();
|
||||
|
||||
if (!bot.entity.headYaw) {
|
||||
// needs a fix?
|
||||
bot.entity.headYaw = 0;
|
||||
}
|
||||
|
||||
// this is done to bypass certain anticheat checks that detect the player's sensitivity
|
||||
// by calculating the gcd of how much they move the mouse each tick
|
||||
const sensitivity = conv.fromNotchianPitch(0.15); // this is equal to 100% sensitivity in vanilla
|
||||
const yawChange = Math.round((yaw - bot.entity.yaw) / sensitivity) * sensitivity;
|
||||
|
||||
const headYawChange = Math.round((yaw - bot.entity.headYaw) / sensitivity) * sensitivity;
|
||||
const pitchChange = Math.round((pitch - bot.entity.pitch) / sensitivity) * sensitivity;
|
||||
|
||||
if (yawChange === 0 && pitchChange === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force) {
|
||||
bot.entity.yaw = yaw;
|
||||
bot.entity.headYaw = yaw;
|
||||
bot.entity.pitch = pitch;
|
||||
|
||||
lastSentYaw = yaw;
|
||||
lastSentPitch = pitch;
|
||||
return;
|
||||
} else {
|
||||
bot.entity.yaw += yawChange;
|
||||
bot.entity.headYaw += yawChange;
|
||||
bot.entity.pitch += pitchChange;
|
||||
}
|
||||
|
||||
await lookingTask.promise;
|
||||
};
|
||||
|
||||
bot.lookAt = async (point: Vec3, force?: boolean) => {
|
||||
force = true;
|
||||
const delta = point.minus(bot.entity.position.offset(0, bot.entity.height, 0));
|
||||
const yaw = Math.atan2(-delta.x, -delta.z);
|
||||
const headYaw = Math.atan2(-delta.x, -delta.z);
|
||||
const groundDistance = Math.sqrt(delta.x * delta.x + delta.z * delta.z);
|
||||
const pitch = Math.atan2(delta.y, groundDistance);
|
||||
await bot.look(yaw, pitch, force);
|
||||
};
|
||||
|
||||
// player position and look (clientbound) server to client
|
||||
const setPosition = (packet: any) => {
|
||||
const packetId = BigInt(packet.runtime_id ?? packet.runtime_entity_id ?? 0);
|
||||
const botId = BigInt(bot.entity?.id ?? 0);
|
||||
if (packetId !== botId) {
|
||||
bot.logger.debug(`move_player: ignoring, packet runtime_id=${packetId} != bot.entity.id=${botId}`);
|
||||
return;
|
||||
}
|
||||
bot.logger.debug(`move_player: updating position, runtime_id=${packetId}`);
|
||||
bot.entity.height = 1.62;
|
||||
bot.entity.velocity.set(0, 0, 0);
|
||||
|
||||
// If flag is set, then the corresponding value is relative, else it is absolute
|
||||
const pos = bot.entity.position;
|
||||
const position = packet.player_position ?? packet.position;
|
||||
const start_game_packet = !!packet.player_position;
|
||||
// Bedrock sends position + eye height (head position), so subtract height to get foot position
|
||||
pos.set(position.x, position.y - bot.entity.height, position.z);
|
||||
|
||||
const newYaw = packet.yaw ?? packet.rotation.z;
|
||||
const newPitch = packet.pitch ?? packet.rotation.x;
|
||||
bot.entity.yaw = newYaw; // conv.fromNotchianYaw(newYaw)
|
||||
bot.entity.pitch = newPitch; // conv.fromNotchianPitch(newPitch)
|
||||
bot.entity.onGround = false; // if pos delta Y == 0 -> on ground
|
||||
|
||||
sendMovementUpdate(pos, newYaw, newPitch);
|
||||
|
||||
shouldUsePhysics = true;
|
||||
bot.entity.timeSinceOnGround = 0;
|
||||
lastSentYaw = bot.entity.yaw;
|
||||
if (start_game_packet)
|
||||
bot._client.once('spawn', async (packet) => {
|
||||
shouldUsePhysics = true;
|
||||
if (doPhysicsTimer === null) {
|
||||
await bot.waitForChunksToLoad();
|
||||
lastPhysicsFrameTime = performance.now();
|
||||
doPhysicsTimer = doPhysicsTimer ?? setInterval(doPhysics, PHYSICS_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
bot.emit('forcedMove');
|
||||
};
|
||||
|
||||
bot._client.on('move_player', setPosition);
|
||||
bot._client.on('start_game', setPosition);
|
||||
|
||||
bot.waitForTicks = async function (ticks: number) {
|
||||
if (ticks <= 0) return;
|
||||
await new Promise<void>((resolve) => {
|
||||
const tickListener = () => {
|
||||
ticks--;
|
||||
if (ticks === 0) {
|
||||
bot.removeListener('physicsTick', tickListener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
bot.on('physicsTick', tickListener);
|
||||
});
|
||||
};
|
||||
|
||||
// bot.on('mount', () => { shouldUsePhysics = false })
|
||||
bot.on('respawn', () => {
|
||||
shouldUsePhysics = false;
|
||||
});
|
||||
|
||||
bot.on('spawn', async () => {
|
||||
shouldUsePhysics = true;
|
||||
if (doPhysicsTimer === null) {
|
||||
await bot.waitForChunksToLoad();
|
||||
lastPhysicsFrameTime = performance.now();
|
||||
doPhysicsTimer = doPhysicsTimer ?? setInterval(doPhysics, PHYSICS_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('end', cleanup);
|
||||
}
|
||||
22
bridge/lib/mineflayer/lib/bedrockPlugins/rain.mts
Normal file
22
bridge/lib/mineflayer/lib/bedrockPlugins/rain.mts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot.isRaining = false;
|
||||
bot.thunderState = 0;
|
||||
bot.rainState = 0;
|
||||
bot._client.on('level_event', (packet) => {
|
||||
if (packet.event === 'start_rain') {
|
||||
bot.isRaining = true;
|
||||
bot.emit('rain');
|
||||
} else if (packet.event === 'stop_rain') {
|
||||
bot.isRaining = false;
|
||||
bot.emit('rain');
|
||||
} else if (packet.event === 'start_thunder') {
|
||||
bot.thunderState = 1; // this value requires checking against java
|
||||
bot.emit('weatherUpdate');
|
||||
} else if (packet.event === 'stop_thunder') {
|
||||
bot.thunderState = 0; // this value requires checking against java
|
||||
bot.emit('weatherUpdate');
|
||||
}
|
||||
});
|
||||
}
|
||||
15
bridge/lib/mineflayer/lib/bedrockPlugins/resource_pack.mts
Normal file
15
bridge/lib/mineflayer/lib/bedrockPlugins/resource_pack.mts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import assert from 'assert';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
function acceptResourcePack() {
|
||||
assert(false, 'Not supported');
|
||||
}
|
||||
|
||||
function denyResourcePack() {
|
||||
assert(false, 'Not supported');
|
||||
}
|
||||
|
||||
bot.acceptResourcePack = acceptResourcePack;
|
||||
bot.denyResourcePack = denyResourcePack;
|
||||
}
|
||||
64
bridge/lib/mineflayer/lib/bedrockPlugins/scoreboard.mts
Normal file
64
bridge/lib/mineflayer/lib/bedrockPlugins/scoreboard.mts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const ScoreBoard = require('../scoreboard')(bot);
|
||||
const scoreboards: Record<string, any> = {};
|
||||
|
||||
bot._client.on('set_display_objective', (packet) => {
|
||||
//console.log(packet)
|
||||
// const { name, position } = packet
|
||||
// const scoreboard = scoreboards[name]
|
||||
//
|
||||
// if (scoreboard !== undefined) {
|
||||
// bot.emit('scoreboardPosition', position, scoreboard, ScoreBoard.positions[position])
|
||||
// ScoreBoard.positions[position] = scoreboard
|
||||
// }
|
||||
//
|
||||
// if (packet.action === 0) {
|
||||
// const { name } = packet
|
||||
// const scoreboard = new ScoreBoard(packet)
|
||||
// scoreboards[name] = scoreboard
|
||||
//
|
||||
// bot.emit('scoreboardCreated', scoreboard)
|
||||
// }
|
||||
//
|
||||
// if (packet.action === 2) {
|
||||
// scoreboards[packet.name].setTitle(packet.displayText)
|
||||
// bot.emit('scoreboardTitleChanged', scoreboards[packet.name])
|
||||
// }
|
||||
});
|
||||
|
||||
bot._client.on('remove_objective', (packet) => {
|
||||
//console.log(packet)
|
||||
// bot.emit('scoreboardDeleted', scoreboards[packet.name])
|
||||
// delete scoreboards[packet.name]
|
||||
});
|
||||
|
||||
bot._client.on('set_score', (packet) => {
|
||||
//console.log(packet)
|
||||
// const scoreboard = scoreboards[packet.scoreName]
|
||||
// if (scoreboard !== undefined && packet.action === 0) {
|
||||
// const updated = scoreboard.add(packet.itemName, packet.value)
|
||||
// bot.emit('scoreUpdated', scoreboard, updated)
|
||||
// }
|
||||
//
|
||||
// if (packet.action === 1) {
|
||||
// if (scoreboard !== undefined) {
|
||||
// const removed = scoreboard.remove(packet.itemName)
|
||||
// return bot.emit('scoreRemoved', scoreboard, removed)
|
||||
// }
|
||||
//
|
||||
// for (const sb of Object.values(scoreboards)) {
|
||||
// if (packet.itemName in sb.itemsMap) {
|
||||
// const removed = sb.remove(packet.itemName)
|
||||
// return bot.emit('scoreRemoved', sb, removed)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
});
|
||||
|
||||
bot.scoreboards = scoreboards;
|
||||
bot.scoreboard = ScoreBoard.positions;
|
||||
}
|
||||
264
bridge/lib/mineflayer/lib/bedrockPlugins/simple_inventory.mts
Normal file
264
bridge/lib/mineflayer/lib/bedrockPlugins/simple_inventory.mts
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
import type { BedrockBot, EquipmentDestination } from '../../index.js';
|
||||
import itemLoader, { type Item } from 'prismarine-item';
|
||||
import assert from 'assert';
|
||||
|
||||
// In Bedrock Edition, hotbar is slots 0-8 in the inventory window
|
||||
const QUICK_BAR_START = 0;
|
||||
const QUICK_BAR_COUNT = 9;
|
||||
|
||||
// Armor slot mappings (Java compatible for API consistency)
|
||||
// Bedrock armor container has 4 slots (0-3): head, torso, legs, feet
|
||||
// We use Java-compatible indices for prismarine-windows compatibility
|
||||
const armorSlots: Record<string, number> = {
|
||||
head: 36, // armor slot 0
|
||||
torso: 37, // armor slot 1
|
||||
legs: 38, // armor slot 2
|
||||
feet: 39, // armor slot 3
|
||||
'off-hand': 45, // offhand slot (Java compatible)
|
||||
};
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const Item = (itemLoader as any)(bot.registry) as typeof Item;
|
||||
|
||||
async function equip(item: Item | number, destination: EquipmentDestination | null): Promise<void> {
|
||||
// Convert item ID to item object if needed
|
||||
if (typeof item === 'number') {
|
||||
item = bot.inventory.findInventoryItem(item);
|
||||
}
|
||||
if (item == null || typeof item !== 'object') {
|
||||
throw new Error('Invalid item object in equip (item is null or typeof item is not object)');
|
||||
}
|
||||
|
||||
// Default to hand if no destination specified
|
||||
if (!destination || destination === null) {
|
||||
destination = 'hand';
|
||||
}
|
||||
|
||||
const sourceSlot = item.slot;
|
||||
let destSlot = getDestSlot(destination);
|
||||
|
||||
// Already in correct slot
|
||||
if (sourceSlot === destSlot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Equipping armor or offhand - just move directly
|
||||
if (destination !== 'hand') {
|
||||
await bot.moveSlotItem(sourceSlot, destSlot);
|
||||
return;
|
||||
}
|
||||
|
||||
// Equipping to hand - check if item is already in hotbar
|
||||
if (sourceSlot >= QUICK_BAR_START && sourceSlot < QUICK_BAR_START + QUICK_BAR_COUNT) {
|
||||
// Item is in hotbar, just change selection
|
||||
bot.setQuickBarSlot(sourceSlot - QUICK_BAR_START);
|
||||
return;
|
||||
}
|
||||
|
||||
// Item is in inventory, need to move to hotbar
|
||||
// Find empty hotbar slot
|
||||
destSlot = bot.inventory.firstEmptySlotRange(QUICK_BAR_START, QUICK_BAR_START + QUICK_BAR_COUNT);
|
||||
if (destSlot == null) {
|
||||
// No empty slot - swap with currently selected hotbar slot
|
||||
// This will place the inventory item in the hotbar and move the
|
||||
// hotbar item to where the inventory item was
|
||||
destSlot = QUICK_BAR_START + bot.quickBarSlot;
|
||||
}
|
||||
|
||||
// Move/swap the item to the hotbar slot
|
||||
await bot.moveSlotItem(sourceSlot, destSlot);
|
||||
// Select the destination slot as the new hand
|
||||
bot.setQuickBarSlot(destSlot - QUICK_BAR_START);
|
||||
}
|
||||
async function unequip(destination: EquipmentDestination | null): Promise<void> {
|
||||
if (!destination) {
|
||||
destination = 'hand';
|
||||
}
|
||||
|
||||
if (destination === 'hand') {
|
||||
await equipEmpty();
|
||||
} else {
|
||||
await disrobe(destination);
|
||||
}
|
||||
}
|
||||
|
||||
async function equipEmpty(): Promise<void> {
|
||||
// First, try to find an empty hotbar slot and select it
|
||||
for (let i = 0; i < QUICK_BAR_COUNT; ++i) {
|
||||
if (!bot.inventory.slots[QUICK_BAR_START + i]) {
|
||||
bot.setQuickBarSlot(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No empty hotbar slot, try to move held item to inventory
|
||||
const emptySlot = bot.inventory.firstEmptyInventorySlot(false); // false = don't check hotbar first (we already did)
|
||||
if (emptySlot === null) {
|
||||
// No room in inventory, toss the item
|
||||
if (bot.heldItem) {
|
||||
await bot.tossStack(bot.heldItem);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Move held item to empty inventory slot
|
||||
const equipSlot = QUICK_BAR_START + bot.quickBarSlot;
|
||||
if (bot.inventory.slots[equipSlot]) {
|
||||
await bot.moveSlotItem(equipSlot, emptySlot);
|
||||
}
|
||||
}
|
||||
|
||||
async function disrobe(destination: EquipmentDestination): Promise<void> {
|
||||
const destSlot = getDestSlot(destination);
|
||||
const itemAtSlot = bot.inventory.slots[destSlot];
|
||||
|
||||
if (!itemAtSlot) {
|
||||
return; // Nothing to unequip
|
||||
}
|
||||
|
||||
// Find an empty inventory slot to move the armor to
|
||||
const emptySlot = bot.inventory.firstEmptyInventorySlot();
|
||||
if (emptySlot === null) {
|
||||
// No room in inventory, toss the item
|
||||
await bot.tossStack(itemAtSlot);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move armor piece to inventory
|
||||
await bot.moveSlotItem(destSlot, emptySlot);
|
||||
}
|
||||
async function toss(itemType: number, metadata: number | null, count: number | null): Promise<void> {
|
||||
// Find items matching the type/metadata in inventory
|
||||
const matchingItems = bot.inventory.slots.filter((item) => {
|
||||
if (!item) return false;
|
||||
if (item.type !== itemType) return false;
|
||||
if (metadata != null && item.metadata !== metadata) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matchingItems.length === 0) {
|
||||
throw new Error(`No item with type ${itemType} found in inventory`);
|
||||
}
|
||||
|
||||
let remaining = count ?? 1;
|
||||
|
||||
for (const item of matchingItems) {
|
||||
if (remaining <= 0) break;
|
||||
|
||||
const tossCount = Math.min(remaining, item.count);
|
||||
|
||||
// Send inventory_transaction for dropping items
|
||||
bot._client.write('inventory_transaction', {
|
||||
transaction: {
|
||||
legacy: {
|
||||
legacy_request_id: 0,
|
||||
legacy_transactions: [],
|
||||
},
|
||||
transaction_type: 'normal',
|
||||
actions: [
|
||||
{
|
||||
source_type: 'world_interaction',
|
||||
flags: 0,
|
||||
slot: 0,
|
||||
old_item: { network_id: 0 },
|
||||
new_item: Item.toNotch(
|
||||
Object.assign(Object.create(Object.getPrototypeOf(item)), item, {
|
||||
count: tossCount,
|
||||
}),
|
||||
0
|
||||
),
|
||||
},
|
||||
{
|
||||
source_type: 'container',
|
||||
inventory_id: 'inventory',
|
||||
slot: item.slot,
|
||||
old_item: Item.toNotch(item, 0),
|
||||
new_item:
|
||||
item.count - tossCount > 0
|
||||
? Item.toNotch(
|
||||
Object.assign(Object.create(Object.getPrototypeOf(item)), item, {
|
||||
count: item.count - tossCount,
|
||||
}),
|
||||
0
|
||||
)
|
||||
: { network_id: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Update local inventory state
|
||||
if (item.count - tossCount > 0) {
|
||||
item.count -= tossCount;
|
||||
bot.inventory.updateSlot(item.slot, item);
|
||||
} else {
|
||||
bot.inventory.updateSlot(item.slot, null);
|
||||
}
|
||||
|
||||
remaining -= tossCount;
|
||||
}
|
||||
}
|
||||
async function tossStack(item: Item): Promise<void> {
|
||||
assert.ok(item, 'Item is required for tossStack');
|
||||
|
||||
// Open inventory if not already open
|
||||
if (!bot.currentWindow) {
|
||||
await bot.openInventory();
|
||||
}
|
||||
|
||||
// Step 1: Take item to cursor
|
||||
await bot.clickWindow(item.slot, 0, 0);
|
||||
|
||||
// Step 2: Drop from cursor (slot -999 triggers drop action)
|
||||
await bot.clickWindow(-999, 0, 0);
|
||||
|
||||
// Close inventory
|
||||
if (bot.currentWindow) {
|
||||
bot.closeWindow(bot.currentWindow);
|
||||
}
|
||||
}
|
||||
function setQuickBarSlot(slot: number): void {
|
||||
assert.ok(slot >= 0 && slot < 9, `Invalid quickBarSlot: ${slot}`);
|
||||
if (bot.quickBarSlot === slot) return; // Already selected
|
||||
|
||||
bot.quickBarSlot = slot;
|
||||
|
||||
const selectedSlot = QUICK_BAR_START + slot;
|
||||
const hotbarItem = bot.inventory.slots[selectedSlot];
|
||||
|
||||
bot._client.write('mob_equipment', {
|
||||
runtime_entity_id: bot.entity.id,
|
||||
item: hotbarItem ? Item.toNotch(hotbarItem, 0) : { network_id: 0 },
|
||||
slot: selectedSlot,
|
||||
selected_slot: selectedSlot,
|
||||
window_id: 'inventory',
|
||||
});
|
||||
|
||||
bot.updateHeldItem();
|
||||
}
|
||||
function getDestSlot(destination: string): number {
|
||||
if (destination === 'hand') {
|
||||
return QUICK_BAR_START + bot.quickBarSlot;
|
||||
}
|
||||
const destSlot = armorSlots[destination];
|
||||
assert.ok(destSlot != null, `invalid destination: ${destination}`);
|
||||
return destSlot;
|
||||
}
|
||||
|
||||
async function leftMouse(slot: number): Promise<void> {
|
||||
return bot.clickWindow(slot, 0, 0);
|
||||
}
|
||||
|
||||
async function rightMouse(slot: number): Promise<void> {
|
||||
return bot.clickWindow(slot, 1, 0);
|
||||
}
|
||||
|
||||
bot.equip = equip;
|
||||
bot.unequip = unequip;
|
||||
bot.toss = toss;
|
||||
bot.tossStack = tossStack;
|
||||
bot.setQuickBarSlot = setQuickBarSlot;
|
||||
bot.getEquipmentDestSlot = getDestSlot;
|
||||
bot.simpleClick = { leftMouse, rightMouse };
|
||||
bot.QUICK_BAR_START = QUICK_BAR_START; // 0 for Bedrock (36 for Java)
|
||||
}
|
||||
31
bridge/lib/mineflayer/lib/bedrockPlugins/sound.mts
Normal file
31
bridge/lib/mineflayer/lib/bedrockPlugins/sound.mts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot._client.on('play_sound', (packet) => {
|
||||
const soundName = packet.name;
|
||||
const volume = packet.volume;
|
||||
const pitch = packet.pitch;
|
||||
|
||||
bot.emit('soundEffectHeard', soundName, packet.coordinates, volume, pitch);
|
||||
});
|
||||
|
||||
bot._client.on('level_sound_event', (packet) => {
|
||||
const soundId = packet.sound_id; // diff field type
|
||||
const soundCategory = packet.extra_data; // diff field type
|
||||
const volume = 1;
|
||||
const pitch = 1;
|
||||
|
||||
bot.emit('hardcodedSoundEffectHeard', soundId, soundCategory, packet.position, volume, pitch);
|
||||
});
|
||||
|
||||
bot._client.on('level_event', (packet) => {
|
||||
if (packet.event.indexOf('sound') !== -1) {
|
||||
const soundId = packet.event; // diff field type
|
||||
const soundCategory = packet.data; // diff field type
|
||||
const volume = 1;
|
||||
const pitch = 1;
|
||||
|
||||
bot.emit('hardcodedSoundEffectHeard', soundId, soundCategory, packet.position, volume, pitch);
|
||||
}
|
||||
});
|
||||
}
|
||||
17
bridge/lib/mineflayer/lib/bedrockPlugins/spawn_point.mts
Normal file
17
bridge/lib/mineflayer/lib/bedrockPlugins/spawn_point.mts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { Vec3 } from 'vec3';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot.spawnPoint = new Vec3(0, 0, 0);
|
||||
bot._client.on('set_spawn_position', (packet) => {
|
||||
if (packet.spawn_type === 'player') {
|
||||
bot.spawnPoint = new Vec3(packet.player_position.x, packet.player_position.y, packet.player_position.z);
|
||||
}
|
||||
// else if (packet.spawn_type === "world") {
|
||||
// if(bot.spawnPoint === new Vec3(0, 0, 0)) {
|
||||
// bot.spawnPoint = new Vec3(packet.world_position.x, packet.world_position.y, packet.world_position.z)
|
||||
// }
|
||||
// }
|
||||
bot.emit('game');
|
||||
});
|
||||
}
|
||||
12
bridge/lib/mineflayer/lib/bedrockPlugins/tablist.mts
Normal file
12
bridge/lib/mineflayer/lib/bedrockPlugins/tablist.mts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const ChatMessage = require('prismarine-chat')(bot.registry);
|
||||
|
||||
bot.tablist = {
|
||||
header: new ChatMessage(''),
|
||||
footer: new ChatMessage(''),
|
||||
};
|
||||
}
|
||||
7
bridge/lib/mineflayer/lib/bedrockPlugins/team.mts
Normal file
7
bridge/lib/mineflayer/lib/bedrockPlugins/team.mts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
// Unsupported in bedrock
|
||||
bot.teams = {};
|
||||
bot.teamMap = {};
|
||||
}
|
||||
63
bridge/lib/mineflayer/lib/bedrockPlugins/time.mts
Normal file
63
bridge/lib/mineflayer/lib/bedrockPlugins/time.mts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
function longToBigInt(arr: bigint | number | number[]): bigint {
|
||||
if (typeof arr === 'bigint') return arr;
|
||||
if (typeof arr === 'number') return BigInt(arr);
|
||||
if (Array.isArray(arr)) {
|
||||
return BigInt.asIntN(64, BigInt(arr[0]) << 32n) | BigInt(arr[1]);
|
||||
}
|
||||
return 0n;
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot.time = {
|
||||
doDaylightCycle: null,
|
||||
bigTime: null,
|
||||
time: null,
|
||||
timeOfDay: null,
|
||||
day: null,
|
||||
isDay: null,
|
||||
moonPhase: null,
|
||||
bigAge: null,
|
||||
age: null,
|
||||
};
|
||||
|
||||
// Initialize age from start_game.current_tick (world tick count)
|
||||
bot._client.on('start_game', (packet) => {
|
||||
if (packet.current_tick != null) {
|
||||
const age = longToBigInt(packet.current_tick);
|
||||
bot.time.bigAge = age;
|
||||
bot.time.age = Number(age);
|
||||
}
|
||||
});
|
||||
|
||||
// Update age from tick_sync.response_time (server's current tick)
|
||||
bot._client.on('tick_sync', (packet) => {
|
||||
if (packet.response_time != null && packet.response_time !== 0n) {
|
||||
const age = BigInt(packet.response_time);
|
||||
bot.time.bigAge = age;
|
||||
bot.time.age = Number(age);
|
||||
}
|
||||
});
|
||||
|
||||
bot._client.on('set_time', (packet) => {
|
||||
let time = BigInt(packet.time);
|
||||
|
||||
if (time < 0n) {
|
||||
bot.time.doDaylightCycle = false;
|
||||
time = -time;
|
||||
} else {
|
||||
bot.time.doDaylightCycle = true;
|
||||
}
|
||||
|
||||
bot.time.bigTime = time;
|
||||
bot.time.time = Number(time);
|
||||
bot.time.timeOfDay = bot.time.time % 24000;
|
||||
bot.time.day = Math.floor(bot.time.time / 24000);
|
||||
// Match Java: isDay when timeOfDay is in range [0, 13000)
|
||||
bot.time.isDay = bot.time.timeOfDay >= 0 && bot.time.timeOfDay < 13000;
|
||||
bot.time.moonPhase = bot.time.day % 8;
|
||||
|
||||
bot.emit('time');
|
||||
});
|
||||
}
|
||||
12
bridge/lib/mineflayer/lib/bedrockPlugins/title.mts
Normal file
12
bridge/lib/mineflayer/lib/bedrockPlugins/title.mts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type { BedrockBot } from '../../index.js';
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
bot._client.on('set_title', (packet) => {
|
||||
if (packet.type === 'set_title') {
|
||||
bot.emit('title', packet.text, 'title');
|
||||
}
|
||||
if (packet.type === 'set_subtitle') {
|
||||
bot.emit('title', packet.text, 'subtitle');
|
||||
}
|
||||
});
|
||||
}
|
||||
449
bridge/lib/mineflayer/lib/bedrockPlugins/villager.mts
Normal file
449
bridge/lib/mineflayer/lib/bedrockPlugins/villager.mts
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
/**
|
||||
* Villager Plugin - Bedrock Edition implementation for villager trading
|
||||
*
|
||||
* Provides:
|
||||
* - bot.openVillager(villagerEntity) - Opens trading UI and returns Villager window
|
||||
* - bot.trade(villager, index, count) - Execute a trade
|
||||
*
|
||||
* Bedrock Protocol:
|
||||
* - Open trade: interact {action_id: "open_inventory", target_entity_id}
|
||||
* - Server responds: container_open + update_trade
|
||||
* - Execute trade: item_stack_request with craft_recipe action
|
||||
* - Close: container_close
|
||||
*
|
||||
* Trade data comes via update_trade packet with NBT structure:
|
||||
* offers.value.Recipes.value.value = [
|
||||
* { buyA, buyB?, sell, buyCountA, maxUses, uses, tier, traderExp, netId, ... }
|
||||
* ]
|
||||
*/
|
||||
|
||||
import type { Entity } from 'prismarine-entity';
|
||||
import type { Window } from 'prismarine-windows';
|
||||
import type { BedrockBot, VillagerTrade, Villager } from '../../index.js';
|
||||
import itemLoader, { type Item } from 'prismarine-item';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import {
|
||||
actions,
|
||||
getNextItemStackRequestId,
|
||||
getStackId,
|
||||
sendRequest,
|
||||
waitForResponse,
|
||||
ContainerIds,
|
||||
slot as makeSlot,
|
||||
type SlotLocation,
|
||||
} from '../bedrock/index.mts';
|
||||
|
||||
// NBT item structure from update_trade packet
|
||||
interface NbtItem {
|
||||
type: string;
|
||||
value: {
|
||||
Name?: { value: string };
|
||||
Count?: { value: number };
|
||||
Damage?: { value: number };
|
||||
Block?: { value: { name?: { value: string } } };
|
||||
};
|
||||
}
|
||||
|
||||
// Trade recipe from update_trade packet
|
||||
interface NbtTradeRecipe {
|
||||
buyA?: NbtItem;
|
||||
buyB?: NbtItem;
|
||||
sell?: NbtItem;
|
||||
buyCountA?: { value: number };
|
||||
buyCountB?: { value: number };
|
||||
maxUses?: { value: number };
|
||||
uses?: { value: number };
|
||||
tier?: { value: number };
|
||||
traderExp?: { value: number };
|
||||
netId?: { value: number };
|
||||
priceMultiplierA?: { value: number };
|
||||
priceMultiplierB?: { value: number };
|
||||
demand?: { value: number };
|
||||
rewardExp?: { value: number };
|
||||
}
|
||||
|
||||
// update_trade packet structure
|
||||
interface UpdateTradePacket {
|
||||
window_id: number;
|
||||
window_type: string;
|
||||
size: number;
|
||||
trade_tier: number;
|
||||
villager_unique_id: bigint | string;
|
||||
entity_unique_id?: bigint | string;
|
||||
display_name?: string;
|
||||
new_trading_ui?: boolean;
|
||||
economic_trades?: boolean;
|
||||
offers?: {
|
||||
type: string;
|
||||
value: {
|
||||
Recipes?: {
|
||||
type: string;
|
||||
value: {
|
||||
type: string;
|
||||
value: NbtTradeRecipe[];
|
||||
};
|
||||
};
|
||||
TierExpRequirements?: any;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function inject(bot: BedrockBot) {
|
||||
const Item = (itemLoader as any)(bot.registry) as typeof import('prismarine-item').Item;
|
||||
|
||||
// Track active trade sessions
|
||||
let activeTradeWindowId: number | null = null;
|
||||
let activeVillagerEntityId: number | bigint | null = null;
|
||||
|
||||
/**
|
||||
* Parse NBT item to prismarine-item
|
||||
*/
|
||||
function parseNbtItem(nbtItem: NbtItem | undefined): Item | null {
|
||||
if (!nbtItem?.value?.Name?.value) return null;
|
||||
|
||||
const name = String(nbtItem.value.Name.value).replace('minecraft:', '');
|
||||
const count = nbtItem.value.Count?.value ?? 1;
|
||||
const damage = nbtItem.value.Damage?.value ?? 0;
|
||||
|
||||
// Look up item in registry
|
||||
const itemDef = bot.registry.itemsByName[name];
|
||||
if (!itemDef) {
|
||||
bot.logger.warn(`Unknown item in trade: ${name}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Item(itemDef.id, count, damage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse trades from update_trade packet
|
||||
*/
|
||||
function parseTrades(packet: UpdateTradePacket): VillagerTrade[] {
|
||||
const trades: VillagerTrade[] = [];
|
||||
|
||||
const recipes = packet.offers?.value?.Recipes?.value?.value;
|
||||
if (!Array.isArray(recipes)) {
|
||||
bot.logger.warn('No recipes found in update_trade packet');
|
||||
return trades;
|
||||
}
|
||||
|
||||
for (const recipe of recipes) {
|
||||
const inputItem1 = parseNbtItem(recipe.buyA);
|
||||
const inputItem2 = parseNbtItem(recipe.buyB);
|
||||
const outputItem = parseNbtItem(recipe.sell);
|
||||
|
||||
if (!inputItem1 || !outputItem) {
|
||||
bot.logger.warn('Invalid trade recipe - missing input or output');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate real price based on demand and price multiplier
|
||||
const baseCost = recipe.buyCountA?.value ?? inputItem1.count;
|
||||
const demand = recipe.demand?.value ?? 0;
|
||||
const priceMultiplier = recipe.priceMultiplierA?.value ?? 0.05;
|
||||
const demandDiff = Math.max(0, Math.floor(baseCost * demand * priceMultiplier));
|
||||
const realPrice = Math.min(Math.max(baseCost + demandDiff, 1), inputItem1.stackSize || 64);
|
||||
|
||||
// Override inputItem1 count with buyCountA if specified
|
||||
if (recipe.buyCountA?.value) {
|
||||
inputItem1.count = recipe.buyCountA.value;
|
||||
}
|
||||
|
||||
const trade: VillagerTrade = {
|
||||
inputItem1,
|
||||
inputItem2: inputItem2 ?? null,
|
||||
outputItem,
|
||||
hasItem2: inputItem2 !== null && inputItem2.count > 0,
|
||||
tradeDisabled: (recipe.uses?.value ?? 0) >= (recipe.maxUses?.value ?? 1),
|
||||
nbTradeUses: recipe.uses?.value ?? 0,
|
||||
maximumNbTradeUses: recipe.maxUses?.value ?? 1,
|
||||
xp: recipe.traderExp?.value,
|
||||
demand: recipe.demand?.value,
|
||||
priceMultiplier: recipe.priceMultiplierA?.value,
|
||||
realPrice,
|
||||
// Bedrock-specific fields
|
||||
tier: recipe.tier?.value,
|
||||
netId: recipe.netId?.value,
|
||||
rewardExp: recipe.rewardExp?.value,
|
||||
};
|
||||
|
||||
trades.push(trade);
|
||||
}
|
||||
|
||||
return trades;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for update_trade packet
|
||||
*/
|
||||
function waitForTradeData(windowId: number, timeout = 5000): Promise<VillagerTrade[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
bot._client.removeListener('update_trade', handler);
|
||||
reject(new Error('Timeout waiting for trade data'));
|
||||
}, timeout);
|
||||
|
||||
const handler = (packet: UpdateTradePacket) => {
|
||||
if (packet.window_id === windowId) {
|
||||
clearTimeout(timer);
|
||||
bot._client.removeListener('update_trade', handler);
|
||||
const trades = parseTrades(packet);
|
||||
resolve(trades);
|
||||
}
|
||||
};
|
||||
|
||||
bot._client.on('update_trade', handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open villager trading window
|
||||
*
|
||||
* @param villagerEntity - The villager entity to trade with
|
||||
* @returns Villager window with trades
|
||||
*/
|
||||
async function openVillager(villagerEntity: Entity): Promise<Villager> {
|
||||
// Verify entity is a villager or wandering trader
|
||||
const entityType = String(villagerEntity.type || villagerEntity.name || '');
|
||||
if (!entityType.includes('villager') && !entityType.includes('wandering_trader')) {
|
||||
throw new Error(`Entity is not a villager or trader: ${entityType}`);
|
||||
}
|
||||
|
||||
// Send interact packet to open trade window
|
||||
bot._client.write('interact', {
|
||||
action_id: 'open_inventory',
|
||||
target_entity_id: villagerEntity.id,
|
||||
has_position: false,
|
||||
});
|
||||
|
||||
// Wait for window to open
|
||||
const windowPromise = new Promise<Window>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
bot.removeListener('windowOpen', onWindowOpen);
|
||||
reject(new Error('Timeout waiting for trade window to open'));
|
||||
}, 10000);
|
||||
|
||||
const onWindowOpen = (window: Window) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(window);
|
||||
};
|
||||
|
||||
bot.once('windowOpen', onWindowOpen);
|
||||
});
|
||||
|
||||
const window = await windowPromise;
|
||||
activeTradeWindowId = window.id;
|
||||
activeVillagerEntityId = villagerEntity.id;
|
||||
|
||||
// Wait for trade data
|
||||
const trades = await waitForTradeData(window.id);
|
||||
|
||||
// Create Villager object extending the window
|
||||
const villager = Object.assign(window, {
|
||||
trades,
|
||||
selectedTrade: null as VillagerTrade | null,
|
||||
trade: async (index: number, count?: number) => {
|
||||
await trade(villager as any, index, count);
|
||||
},
|
||||
}) as unknown as Villager;
|
||||
|
||||
// Emit ready event
|
||||
(villager as any).emit('ready');
|
||||
|
||||
return villager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a trade with a villager
|
||||
*
|
||||
* @param villager - The villager window (from openVillager)
|
||||
* @param index - Trade index to execute
|
||||
* @param count - Number of times to execute trade (default: max available)
|
||||
*/
|
||||
async function trade(villager: Villager, index: number | string, count?: number): Promise<void> {
|
||||
const tradeIndex = typeof index === 'string' ? parseInt(index, 10) : index;
|
||||
|
||||
if (!villager.trades || tradeIndex < 0 || tradeIndex >= villager.trades.length) {
|
||||
throw new Error(`Invalid trade index: ${tradeIndex}`);
|
||||
}
|
||||
|
||||
const tradeData = villager.trades[tradeIndex];
|
||||
villager.selectedTrade = tradeData;
|
||||
|
||||
// Calculate available trades
|
||||
const availableTrades = tradeData.maximumNbTradeUses - tradeData.nbTradeUses;
|
||||
if (availableTrades <= 0) {
|
||||
throw new Error('Trade is disabled (max uses reached)');
|
||||
}
|
||||
|
||||
const timesToTrade = count ?? availableTrades;
|
||||
if (timesToTrade > availableTrades) {
|
||||
throw new Error(`Cannot trade ${timesToTrade} times, only ${availableTrades} available`);
|
||||
}
|
||||
|
||||
// Check if we have enough items
|
||||
const realPrice = tradeData.realPrice ?? tradeData.inputItem1.count;
|
||||
const neededItem1 = realPrice * timesToTrade;
|
||||
const itemCount1 = countItems(tradeData.inputItem1.type, tradeData.inputItem1.metadata);
|
||||
|
||||
if (itemCount1 < neededItem1) {
|
||||
throw new Error(`Not enough ${tradeData.inputItem1.name} to trade (need ${neededItem1}, have ${itemCount1})`);
|
||||
}
|
||||
|
||||
if (tradeData.hasItem2 && tradeData.inputItem2) {
|
||||
const neededItem2 = tradeData.inputItem2.count * timesToTrade;
|
||||
const itemCount2 = countItems(tradeData.inputItem2.type, tradeData.inputItem2.metadata);
|
||||
if (itemCount2 < neededItem2) {
|
||||
throw new Error(`Not enough ${tradeData.inputItem2.name} to trade (need ${neededItem2}, have ${itemCount2})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute trade using item_stack_request
|
||||
for (let i = 0; i < timesToTrade; i++) {
|
||||
await executeOneTrade(villager, tradeData);
|
||||
tradeData.nbTradeUses++;
|
||||
if (tradeData.nbTradeUses >= tradeData.maximumNbTradeUses) {
|
||||
tradeData.tradeDisabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count items in inventory of given type
|
||||
*/
|
||||
function countItems(itemType: number, metadata?: number | null): number {
|
||||
let count = 0;
|
||||
for (const item of bot.inventory.slots) {
|
||||
if (item && item.type === itemType) {
|
||||
if (metadata === null || metadata === undefined || item.metadata === metadata) {
|
||||
count += item.count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find inventory slot with item
|
||||
*/
|
||||
function findItemSlot(itemType: number, metadata?: number | null, minCount = 1): number | null {
|
||||
for (let i = 0; i < bot.inventory.slots.length; i++) {
|
||||
const item = bot.inventory.slots[i];
|
||||
if (item && item.type === itemType && item.count >= minCount) {
|
||||
if (metadata === null || metadata === undefined || item.metadata === metadata) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single trade transaction
|
||||
*
|
||||
* Bedrock trade execution uses item_stack_request with:
|
||||
* 1. place actions to put items in trade input slots
|
||||
* 2. craft_recipe or trade action
|
||||
* 3. take action to get output
|
||||
*/
|
||||
async function executeOneTrade(villager: Villager, tradeData: VillagerTrade): Promise<void> {
|
||||
const netId = (tradeData as any).netId;
|
||||
if (!netId) {
|
||||
throw new Error('Trade missing network ID - cannot execute');
|
||||
}
|
||||
|
||||
// Find items in inventory
|
||||
const realPrice = tradeData.realPrice ?? tradeData.inputItem1.count;
|
||||
const slot1 = findItemSlot(tradeData.inputItem1.type, tradeData.inputItem1.metadata, realPrice);
|
||||
if (slot1 === null) {
|
||||
throw new Error(`Cannot find ${tradeData.inputItem1.name} in inventory`);
|
||||
}
|
||||
|
||||
const item1 = bot.inventory.slots[slot1]!;
|
||||
const stackId1 = getStackId(item1);
|
||||
|
||||
let slot2: number | null = null;
|
||||
let item2: Item | null = null;
|
||||
let stackId2 = 0;
|
||||
|
||||
if (tradeData.hasItem2 && tradeData.inputItem2) {
|
||||
slot2 = findItemSlot(tradeData.inputItem2.type, tradeData.inputItem2.metadata, tradeData.inputItem2.count);
|
||||
if (slot2 === null) {
|
||||
throw new Error(`Cannot find ${tradeData.inputItem2.name} in inventory`);
|
||||
}
|
||||
item2 = bot.inventory.slots[slot2]!;
|
||||
stackId2 = getStackId(item2);
|
||||
}
|
||||
|
||||
// Find empty slot for output
|
||||
let outputSlot = -1;
|
||||
for (let i = 0; i < 36; i++) {
|
||||
if (!bot.inventory.slots[i]) {
|
||||
outputSlot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (outputSlot === -1) {
|
||||
throw new Error('No empty inventory slot for trade output');
|
||||
}
|
||||
|
||||
// Build trade request
|
||||
// Trade slots: 0 = input1, 1 = input2, 2 = output
|
||||
const requestId = getNextItemStackRequestId();
|
||||
const outputCount = tradeData.outputItem.count;
|
||||
|
||||
// Build result items for results_deprecated
|
||||
const resultItems = [
|
||||
{
|
||||
network_id: tradeData.outputItem.type,
|
||||
count: outputCount,
|
||||
metadata: tradeData.outputItem.metadata ?? 0,
|
||||
block_runtime_id: 0,
|
||||
extra: { has_nbt: 0, can_place_on: [], can_destroy: [] },
|
||||
},
|
||||
];
|
||||
|
||||
// Use craft_recipe action with consume and place actions
|
||||
// From packet captures, villager trades use similar format to crafting
|
||||
const builder = actions()
|
||||
.craftRecipe(netId, 1)
|
||||
.resultsDeprecated(resultItems, 1)
|
||||
.consume(realPrice, makeSlot('hotbar_and_inventory', slot1, stackId1));
|
||||
|
||||
if (tradeData.hasItem2 && tradeData.inputItem2 && slot2 !== null) {
|
||||
builder.consume(tradeData.inputItem2.count, makeSlot('hotbar_and_inventory', slot2, stackId2));
|
||||
}
|
||||
|
||||
// Place output directly to inventory
|
||||
builder.place(outputCount, makeSlot('creative_output', 50, requestId), makeSlot('hotbar_and_inventory', outputSlot, 0));
|
||||
|
||||
bot.logger.debug(`Executing trade: netId=${netId}, requestId=${requestId}`);
|
||||
|
||||
sendRequest(bot, requestId, builder.build());
|
||||
|
||||
const success = await waitForResponse(bot, requestId);
|
||||
if (!success) {
|
||||
throw new Error('Trade failed - server rejected request');
|
||||
}
|
||||
|
||||
// Create output item in destination slot
|
||||
const newItem = new Item(tradeData.outputItem.type, outputCount, tradeData.outputItem.metadata ?? 0);
|
||||
(newItem as any).stackId = requestId;
|
||||
bot.inventory.updateSlot(outputSlot, newItem);
|
||||
|
||||
bot.logger.debug(`Trade successful: ${tradeData.outputItem.name} x${outputCount} → slot ${outputSlot}`);
|
||||
}
|
||||
|
||||
// Listen for trade window close
|
||||
bot._client.on('container_close', (packet: { window_id: number }) => {
|
||||
if (packet.window_id === activeTradeWindowId) {
|
||||
activeTradeWindowId = null;
|
||||
activeVillagerEntityId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Expose API
|
||||
bot.openVillager = openVillager;
|
||||
bot.trade = trade;
|
||||
}
|
||||
109
bridge/lib/mineflayer/lib/bossbar.js
Normal file
109
bridge/lib/mineflayer/lib/bossbar.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
const colors = ['pink', 'blue', 'red', 'green', 'yellow', 'purple', 'white']
|
||||
const divisions = [0, 6, 10, 12, 20]
|
||||
|
||||
function loader (registry) {
|
||||
const ChatMessage = require('prismarine-chat')(registry)
|
||||
return class BossBar {
|
||||
constructor (uuid, title, health, dividers, color, flags) {
|
||||
this._entityUUID = uuid
|
||||
this.title = title
|
||||
this._health = health
|
||||
this._dividers = divisions[dividers]
|
||||
this._color = colors[color]
|
||||
this._shouldDarkenSky = flags & 0x1
|
||||
this._isDragonBar = flags & 0x2
|
||||
this._createFog = flags & 0x4
|
||||
}
|
||||
|
||||
set entityUUID (uuid) {
|
||||
this._entityUUID = uuid
|
||||
}
|
||||
|
||||
set title (title) {
|
||||
if (title && typeof title === 'object' && title.type === 'string' && 'value' in title) {
|
||||
this._title = title.value
|
||||
} else {
|
||||
const chatMsg = ChatMessage.fromNotch(title)
|
||||
if (chatMsg !== undefined && chatMsg !== null) {
|
||||
this._title = chatMsg
|
||||
} else if (typeof title === 'string') {
|
||||
this._title = title
|
||||
} else {
|
||||
this._title = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set health (health) {
|
||||
this._health = health
|
||||
}
|
||||
|
||||
set dividers (dividers) {
|
||||
this._dividers = divisions[dividers]
|
||||
}
|
||||
|
||||
set color (color) {
|
||||
this._color = colors[color]
|
||||
}
|
||||
|
||||
set flags (flags) {
|
||||
this._shouldDarkenSky = flags & 0x1
|
||||
this._isDragonBar = flags & 0x2
|
||||
this._createFog = flags & 0x4
|
||||
}
|
||||
|
||||
get flags () {
|
||||
return (this._shouldDarkenSky) | (this._isDragonBar << 1) | (this._createFog << 2)
|
||||
}
|
||||
|
||||
set shouldDarkenSky (darkenSky) {
|
||||
this._shouldDarkenSky = darkenSky
|
||||
}
|
||||
|
||||
set isDragonBar (dragonBar) {
|
||||
this._isDragonBar = dragonBar
|
||||
}
|
||||
|
||||
get createFog () {
|
||||
return this._createFog
|
||||
}
|
||||
|
||||
set createFog (createFog) {
|
||||
this._createFog = createFog
|
||||
}
|
||||
|
||||
get entityUUID () {
|
||||
return this._entityUUID
|
||||
}
|
||||
|
||||
get title () {
|
||||
return this._title
|
||||
}
|
||||
|
||||
get health () {
|
||||
return this._health
|
||||
}
|
||||
|
||||
get dividers () {
|
||||
return this._dividers
|
||||
}
|
||||
|
||||
get color () {
|
||||
return this._color
|
||||
}
|
||||
|
||||
get shouldDarkenSky () {
|
||||
return this._shouldDarkenSky
|
||||
}
|
||||
|
||||
get isDragonBar () {
|
||||
return this._isDragonBar
|
||||
}
|
||||
|
||||
get shouldCreateFog () {
|
||||
return this._createFog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loader
|
||||
40
bridge/lib/mineflayer/lib/conversions.js
Normal file
40
bridge/lib/mineflayer/lib/conversions.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
const math = require('./math')
|
||||
const euclideanMod = math.euclideanMod
|
||||
const PI = Math.PI
|
||||
const PI_2 = Math.PI * 2
|
||||
const TO_RAD = PI / 180
|
||||
const TO_DEG = 1 / TO_RAD
|
||||
const FROM_NOTCH_BYTE = 360 / 256
|
||||
// From minecraft.wiki: Velocity is believed to be in units of 1/8000 of a block per server tick (50ms)
|
||||
const FROM_NOTCH_VEL = 1 / 8000
|
||||
|
||||
exports.toRadians = toRadians
|
||||
exports.toDegrees = toDegrees
|
||||
exports.fromNotchianYaw = fromNotchianYaw
|
||||
exports.fromNotchianPitch = fromNotchianPitch
|
||||
exports.fromNotchVelocity = fromNotchVelocity
|
||||
exports.toNotchianYaw = yaw => toDegrees(PI - yaw)
|
||||
exports.toNotchianPitch = pitch => toDegrees(-pitch)
|
||||
exports.fromNotchianYawByte = yaw => fromNotchianYaw(yaw * FROM_NOTCH_BYTE)
|
||||
exports.fromNotchianPitchByte = pitch => fromNotchianPitch(pitch * FROM_NOTCH_BYTE)
|
||||
|
||||
function toRadians (degrees) {
|
||||
return TO_RAD * degrees
|
||||
}
|
||||
|
||||
function toDegrees (radians) {
|
||||
return TO_DEG * radians
|
||||
}
|
||||
|
||||
function fromNotchianYaw (yaw) {
|
||||
return euclideanMod(PI - toRadians(yaw), PI_2)
|
||||
}
|
||||
|
||||
function fromNotchianPitch (pitch) {
|
||||
return euclideanMod(toRadians(-pitch) + PI, PI_2) - PI
|
||||
}
|
||||
|
||||
function fromNotchVelocity (vel) {
|
||||
return new Vec3(vel.x * FROM_NOTCH_VEL, vel.y * FROM_NOTCH_VEL, vel.z * FROM_NOTCH_VEL)
|
||||
}
|
||||
36
bridge/lib/mineflayer/lib/inventory-packet-logger.mts
Normal file
36
bridge/lib/mineflayer/lib/inventory-packet-logger.mts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Interface for packet logging. Implement this to create custom packet loggers.
|
||||
*/
|
||||
export interface IPacketLogger {
|
||||
/**
|
||||
* Log a packet
|
||||
* @param direction 'C' for client->server, 'S' for server->client
|
||||
* @param name Packet name
|
||||
* @param packet Packet data
|
||||
*/
|
||||
log(direction: 'C' | 'S', name: string, packet: any): void;
|
||||
|
||||
/**
|
||||
* Attach logger to a bot's client
|
||||
* @param client The bot._client instance
|
||||
*/
|
||||
attachToBot(client: any): void;
|
||||
|
||||
/**
|
||||
* Set the registry for item name resolution
|
||||
* @param registry The bot.registry instance
|
||||
*/
|
||||
setRegistry?(registry: any): void;
|
||||
|
||||
/**
|
||||
* Log a custom message/event for debugging
|
||||
* @param msg Message text
|
||||
* @param data Optional additional data
|
||||
*/
|
||||
message?(msg: string, data?: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* Close the logger and flush any pending writes
|
||||
*/
|
||||
close(): void;
|
||||
}
|
||||
251
bridge/lib/mineflayer/lib/loader.js
Normal file
251
bridge/lib/mineflayer/lib/loader.js
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
const { EventEmitter } = require('events')
|
||||
const pluginLoader = require('./plugin_loader')
|
||||
const { Logger, LogLevel, LoggerColors } = require('./logger/logger.mts')
|
||||
const plugins = {
|
||||
bed: require('./plugins/bed'),
|
||||
title: require('./plugins/title'),
|
||||
block_actions: require('./plugins/block_actions'),
|
||||
blocks: require('./plugins/blocks'),
|
||||
book: require('./plugins/book'),
|
||||
boss_bar: require('./plugins/boss_bar'),
|
||||
breath: require('./plugins/breath'),
|
||||
chat: require('./plugins/chat'),
|
||||
chest: require('./plugins/chest'),
|
||||
command_block: require('./plugins/command_block'),
|
||||
craft: require('./plugins/craft'),
|
||||
creative: require('./plugins/creative'),
|
||||
digging: require('./plugins/digging'),
|
||||
enchantment_table: require('./plugins/enchantment_table'),
|
||||
entities: require('./plugins/entities'),
|
||||
experience: require('./plugins/experience'),
|
||||
explosion: require('./plugins/explosion'),
|
||||
fishing: require('./plugins/fishing'),
|
||||
furnace: require('./plugins/furnace'),
|
||||
game: require('./plugins/game'),
|
||||
health: require('./plugins/health'),
|
||||
inventory: require('./plugins/inventory'),
|
||||
kick: require('./plugins/kick'),
|
||||
physics: require('./plugins/physics'),
|
||||
place_block: require('./plugins/place_block'),
|
||||
rain: require('./plugins/rain'),
|
||||
ray_trace: require('./plugins/ray_trace'),
|
||||
resource_pack: require('./plugins/resource_pack'),
|
||||
scoreboard: require('./plugins/scoreboard'),
|
||||
team: require('./plugins/team'),
|
||||
settings: require('./plugins/settings'),
|
||||
simple_inventory: require('./plugins/simple_inventory'),
|
||||
sound: require('./plugins/sound'),
|
||||
spawn_point: require('./plugins/spawn_point'),
|
||||
tablist: require('./plugins/tablist'),
|
||||
time: require('./plugins/time'),
|
||||
villager: require('./plugins/villager'),
|
||||
anvil: require('./plugins/anvil'),
|
||||
place_entity: require('./plugins/place_entity'),
|
||||
generic_place: require('./plugins/generic_place'),
|
||||
particle: require('./plugins/particle')
|
||||
}
|
||||
|
||||
const bedrockPlugins = {
|
||||
bed: require('./bedrockPlugins/bed.mts').default, // 100% - sleep/wake with animate packet
|
||||
title: require('./bedrockPlugins/title.mts').default, // 100%
|
||||
|
||||
block_actions: require('./bedrockPlugins/block_actions.mts').default, // // 40% destroyStage unavalible for bedrock, calculates client-side, piston and noteblocks fix req
|
||||
blocks: require('./bedrockPlugins/blocks.mts').default, // 80% WIP WORLD LOADER (block entities etc) doors bbs calc wrong (wrong state calc?)
|
||||
|
||||
book: require('./bedrockPlugins/book.mts').default, // 100% - writeBook/signBook via book_edit packet
|
||||
boss_bar: require('./bedrockPlugins/bossbar.mts').default, // 0% - 80% possible 100%
|
||||
|
||||
breath: require('./bedrockPlugins/breath.mts').default, // 100%
|
||||
chat: require('./bedrockPlugins/chat.mts').default, // 100% - fixed text and command_request packets for 1.21.130
|
||||
|
||||
chest: require('./bedrockPlugins/chest.mts').default, // 100% - openContainer/openChest/openDispenser
|
||||
//command_block: require('./bedrockPlugins/command_block'), // 0% req inventory
|
||||
craft: require('./bedrockPlugins/craft.mts').default, // 100% - crafting via craft_recipe item_stack_request
|
||||
creative: require('./bedrockPlugins/creative.mts').default, // partial - flying works, setInventorySlot pending (status 7 error)
|
||||
digging: require('./bedrockPlugins/digging.mts').default, // 100% - uses player_auth_input block_action
|
||||
//enchantment_table: require('./bedrockPlugins/enchantment_table'), // 0% req inv
|
||||
|
||||
entities: require('./bedrockPlugins/entities.mts').default, // 100% working? (no item entities) yaw and pitch conversion req fix (pviewer rotates player too fast)
|
||||
|
||||
experience: require('./bedrockPlugins/experience.mts').default, // 100%
|
||||
// explosion: require('./bedrockPlugins/explosion'), // 0% - 90% req logical checks, tho maybe its calc by server?
|
||||
fishing: require('./bedrockPlugins/fishing.mts').default, // 100% - uses entity_event fish_hook_hook
|
||||
// furnace: require('./bedrockPlugins/furnace'), // 0% req inv
|
||||
|
||||
game: require('./bedrockPlugins/game.mts').default, // 70% - 100% | works, req impl other stuff
|
||||
health: require('./bedrockPlugins/health.mts').default, // 100%
|
||||
|
||||
//inventory: require('./bedrockPlugins/inventory_minimal.mts').default, // placeholder way?
|
||||
|
||||
inventory: require('./bedrockPlugins/inventory.mts').default, // 100% - all click modes (0-4 incl creative), containers, transfers, activateBlock/Entity/consume
|
||||
kick: require('./bedrockPlugins/kick.mts').default, // 100% done
|
||||
|
||||
physics: require('./bedrockPlugins/physics.mts').default, // req blocks_.js and minecraft-data update (for effects)
|
||||
|
||||
//place_block: require('./bedrockPlugins/place_block'), // req player auth input logic
|
||||
rain: require('./bedrockPlugins/rain.mts').default, // 100%, might require small check
|
||||
|
||||
ray_trace: plugins.ray_trace, // 100% unchanged? HeadYaw implement?
|
||||
|
||||
resource_pack: require('./bedrockPlugins/resource_pack.mts').default, // 100%. not needed since bedrock does not support/handled at login by bedrock-protocol
|
||||
|
||||
scoreboard: require('./bedrockPlugins/scoreboard.mts').default, // badly implemented 10% 0 functions
|
||||
|
||||
team: require('./bedrockPlugins/team.mts').default, // 0% req investigation
|
||||
//settings: require('./bedrockPlugins/settings'), // 0% only SOME settings are exposed (better to only leave chunks)
|
||||
simple_inventory: require('./bedrockPlugins/simple_inventory.mts').default, // 100% - equip/unequip/toss for all slots including armor and offhand
|
||||
sound: require('./bedrockPlugins/sound.mts').default, // 100%
|
||||
spawn_point: require('./bedrockPlugins/spawn_point.mts').default, // 100%, might require small logical checking
|
||||
tablist: require('./bedrockPlugins/tablist.mts').default, // 0% bedrock does not have it but possible to make a conversion
|
||||
|
||||
time: require('./bedrockPlugins/time.mts').default, // doesnt have AGE
|
||||
|
||||
villager: require('./bedrockPlugins/villager.mts').default, // 80% - openVillager, trade with item_stack_request
|
||||
//anvil: require('./bedrockPlugins/anvil'), // 0% req inv
|
||||
//place_entity: require('./bedrockPlugins/place_entity'), // 0% - 80% | 100% possible
|
||||
//generic_place: require('./bedrockPlugins/generic_place'), // 0% not sure why but possible
|
||||
|
||||
particle: require('./bedrockPlugins/particle.mts').default // mostly works, tho needs to be unified a bit
|
||||
}
|
||||
|
||||
const minecraftData = require('minecraft-data')
|
||||
|
||||
// TODO: how to export supported version if both bedrock and java supported?
|
||||
const { testedVersions, latestSupportedVersion, oldestSupportedVersion } = require('./version')(false)
|
||||
|
||||
const BEDROCK_PREFIX = 'bedrock_'
|
||||
|
||||
module.exports = {
|
||||
createBot,
|
||||
Location: require('./location'),
|
||||
Painting: require('./painting'),
|
||||
ScoreBoard: require('./scoreboard'),
|
||||
BossBar: require('./bossbar'),
|
||||
Particle: require('./particle'),
|
||||
Logger,
|
||||
LogLevel,
|
||||
LoggerColors,
|
||||
latestSupportedVersion,
|
||||
oldestSupportedVersion,
|
||||
testedVersions,
|
||||
supportFeature: (feature, version) => minecraftData(version).supportFeature(feature)
|
||||
}
|
||||
|
||||
function createBot (options = {}) {
|
||||
options.isBedrock = options.version.indexOf(BEDROCK_PREFIX) !== -1
|
||||
options.username = options.username ?? 'Player'
|
||||
options.version = options.version.replace(BEDROCK_PREFIX, '') ?? false
|
||||
options.plugins = options.plugins ?? {}
|
||||
options.hideErrors = options.hideErrors ?? false
|
||||
options.logErrors = options.logErrors ?? true
|
||||
options.loadInternalPlugins = options.loadInternalPlugins ?? true
|
||||
options.client = options.client ?? null
|
||||
options.brand = options.brand ?? 'vanilla'
|
||||
options.respawn = options.respawn ?? true
|
||||
options.fakeWorldPath = options.fakeWorldPath ?? null
|
||||
options.offline = options.auth === 'offline'
|
||||
options.packetLogger = options.packetLogger ?? null
|
||||
|
||||
// Logger configuration
|
||||
if (options.logLevel !== undefined) {
|
||||
Logger.level = options.logLevel
|
||||
} else if (options.debug) {
|
||||
Logger.level = LogLevel.Debug
|
||||
}
|
||||
|
||||
const bot = new EventEmitter()
|
||||
bot._client = options.client
|
||||
bot.end = (reason) => bot._client.end(reason)
|
||||
|
||||
// Create logger instance for this bot
|
||||
bot.logger = new Logger(options.username, LoggerColors.Aqua)
|
||||
|
||||
if (options.logErrors) {
|
||||
bot.on('error', err => {
|
||||
if (!options.hideErrors) {
|
||||
bot.logger.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pluginLoader(bot, options)
|
||||
let pluginsToLoad = options.isBedrock ? bedrockPlugins : plugins
|
||||
|
||||
const internalPlugins = Object.keys(pluginsToLoad)
|
||||
.filter(key => {
|
||||
if (typeof options.plugins[key] === 'function') return false
|
||||
if (options.plugins[key] === false) return false
|
||||
return options.plugins[key] || options.loadInternalPlugins
|
||||
}).map(key => pluginsToLoad[key])
|
||||
const externalPlugins = Object.keys(options.plugins)
|
||||
.filter(key => {
|
||||
return typeof options.plugins[key] === 'function'
|
||||
}).map(key => options.plugins[key])
|
||||
bot.loadPlugins([...internalPlugins, ...externalPlugins])
|
||||
|
||||
let protocol = options.isBedrock ? require('bedrock-protocol') : require('minecraft-protocol')
|
||||
|
||||
// Allow overriding protocol version for newer servers
|
||||
const protocolOpts = { ...options };
|
||||
if (options.isBedrock && options.bedrockProtocolVersion) {
|
||||
protocolOpts.version = options.bedrockProtocolVersion;
|
||||
}
|
||||
bot._client = bot._client ?? protocol.createClient(protocolOpts)
|
||||
|
||||
// Packet logger setup - accepts IPacketLogger instance
|
||||
if (options.packetLogger && options.isBedrock) {
|
||||
const logger = options.packetLogger
|
||||
if (logger && typeof logger.attachToBot === 'function') {
|
||||
bot.packetLogger = logger
|
||||
logger.attachToBot(bot._client)
|
||||
bot.on('end', () => logger.close())
|
||||
// Set registry when available for item name resolution
|
||||
bot.once('inject_allowed', () => {
|
||||
if (typeof logger.setRegistry === 'function') {
|
||||
logger.setRegistry(bot.registry)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.on('connect', () => {
|
||||
bot.emit('connect')
|
||||
})
|
||||
bot._client.on('error', (err) => {
|
||||
bot.emit('error', err)
|
||||
})
|
||||
bot._client.on('end', (reason) => {
|
||||
bot.emit('end', reason)
|
||||
})
|
||||
|
||||
bot._client.on('close', (reason) => {
|
||||
bot.emit('end', reason)
|
||||
})
|
||||
if (!bot._client.wait_connect) next() // unknown purpose
|
||||
else bot._client.once('connect_allowed', next)
|
||||
function next () {
|
||||
const { testedVersions, latestSupportedVersion, oldestSupportedVersion } = require('./version')(options.isBedrock)
|
||||
const serverPingVersion = options.isBedrock ? BEDROCK_PREFIX + options.version : bot._client.version
|
||||
bot.registry = require('prismarine-registry')(serverPingVersion)
|
||||
if (!bot.registry?.version) throw new Error(`Server version '${serverPingVersion}' is not supported, no data for version`)
|
||||
|
||||
const versionData = bot.registry.version
|
||||
if (versionData['>'](latestSupportedVersion)) {
|
||||
throw new Error(`Server version '${serverPingVersion}' is not supported. Latest supported version is '${latestSupportedVersion}'.`)
|
||||
} else if (versionData['<'](oldestSupportedVersion)) {
|
||||
throw new Error(`Server version '${serverPingVersion}' is not supported. Oldest supported version is '${oldestSupportedVersion}'.`)
|
||||
}
|
||||
|
||||
bot.protocolVersion = versionData.version
|
||||
bot.majorVersion = versionData.majorVersion
|
||||
bot.version = options.isBedrock ? BEDROCK_PREFIX + versionData.minecraftVersion : versionData.minecraftVersion
|
||||
bot.supportFeature = bot.registry.supportFeature
|
||||
setTimeout(() => bot.emit('inject_allowed'), 0)
|
||||
}
|
||||
|
||||
bot.close = function(){
|
||||
bot._client?.close()
|
||||
}
|
||||
|
||||
return bot
|
||||
}
|
||||
14
bridge/lib/mineflayer/lib/location.js
Normal file
14
bridge/lib/mineflayer/lib/location.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
const CHUNK_SIZE = new Vec3(16, 16, 16)
|
||||
|
||||
class Location {
|
||||
constructor (absoluteVector) {
|
||||
this.floored = absoluteVector.floored()
|
||||
this.blockPoint = this.floored.modulus(CHUNK_SIZE)
|
||||
this.chunkCorner = this.floored.minus(this.blockPoint)
|
||||
this.blockIndex = this.blockPoint.x + CHUNK_SIZE.x * this.blockPoint.z + CHUNK_SIZE.x * CHUNK_SIZE.z * this.blockPoint.y
|
||||
this.biomeBlockIndex = this.blockPoint.x + CHUNK_SIZE.x * this.blockPoint.z
|
||||
this.chunkYIndex = Math.floor(absoluteVector.y / 16)
|
||||
}
|
||||
}
|
||||
module.exports = Location
|
||||
36
bridge/lib/mineflayer/lib/logger/logger-colors.mts
Normal file
36
bridge/lib/mineflayer/lib/logger/logger-colors.mts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* ANSI color codes for terminal output.
|
||||
* Adapted from @serenityjs/logger
|
||||
*/
|
||||
export const LoggerColors = {
|
||||
Black: '\u001B[38;2;0;0;0m',
|
||||
DarkBlue: '\u001B[38;2;0;0;170m',
|
||||
DarkGreen: '\u001B[38;2;0;170;0m',
|
||||
DarkAqua: '\u001B[38;2;0;170;170m',
|
||||
DarkRed: '\u001B[38;2;170;0;0m',
|
||||
DarkPurple: '\u001B[38;2;170;0;170m',
|
||||
Gold: '\u001B[38;2;255;170;0m',
|
||||
Gray: '\u001B[38;2;170;170;170m',
|
||||
DarkGray: '\u001B[38;2;85;85;85m',
|
||||
Blue: '\u001B[38;2;85;85;255m',
|
||||
Green: '\u001B[38;2;85;255;85m',
|
||||
Aqua: '\u001B[38;2;85;255;255m',
|
||||
Red: '\u001B[38;2;255;85;85m',
|
||||
LightPurple: '\u001B[38;2;255;85;255m',
|
||||
Yellow: '\u001B[38;2;255;255;85m',
|
||||
White: '\u001B[38;2;255;255;255m',
|
||||
MinecoinGold: '\u001B[38;2;221;214;5m',
|
||||
MaterialQuartz: '\u001B[38;2;227;212;209m',
|
||||
MaterialIron: '\u001B[38;2;206;202;202m',
|
||||
MaterialNetherite: '\u001B[38;2;68;58;59m',
|
||||
MaterialRedstone: '\u001B[38;2;151;22;7m',
|
||||
MaterialCopper: '\u001B[38;2;180;104;77m',
|
||||
MaterialGold: '\u001B[38;2;222;177;45m',
|
||||
MaterialEmerald: '\u001B[38;2;71;160;54m',
|
||||
MaterialDiamond: '\u001B[38;2;44;186;168m',
|
||||
MaterialLapis: '\u001B[38;2;33;73;123m',
|
||||
MaterialAmethyst: '\u001B[38;2;154;92;198m',
|
||||
Reset: '\u001B[0m',
|
||||
} as const;
|
||||
|
||||
export type LoggerColors = (typeof LoggerColors)[keyof typeof LoggerColors];
|
||||
172
bridge/lib/mineflayer/lib/logger/logger.mts
Normal file
172
bridge/lib/mineflayer/lib/logger/logger.mts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* A colorized logger for mineflayer bots.
|
||||
* Adapted from @serenityjs/logger
|
||||
*/
|
||||
import { formatMinecraftColorCode } from './minecraft-colors.mts';
|
||||
import { LoggerColors } from './logger-colors.mts';
|
||||
|
||||
export { LoggerColors } from './logger-colors.mts';
|
||||
export { formatMinecraftColorCode, stripMinecraftColorCodes } from './minecraft-colors.mts';
|
||||
|
||||
/**
|
||||
* Log level for filtering messages.
|
||||
*/
|
||||
export const LogLevel = {
|
||||
Debug: 0,
|
||||
Info: 1,
|
||||
Warn: 2,
|
||||
Error: 3,
|
||||
None: 4,
|
||||
} as const;
|
||||
|
||||
export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
|
||||
|
||||
/**
|
||||
* Format a date as MM-DD-YYYY HH:mm:ss
|
||||
*/
|
||||
function formatTimestamp(date: Date = new Date()): string {
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
const month = pad(date.getMonth() + 1);
|
||||
const day = pad(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
const hours = pad(date.getHours());
|
||||
const minutes = pad(date.getMinutes());
|
||||
const seconds = pad(date.getSeconds());
|
||||
return `${month}-${day}-${year} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A colorized logger for applications.
|
||||
*/
|
||||
export class Logger {
|
||||
/**
|
||||
* Global log level. Messages below this level are not shown.
|
||||
*/
|
||||
public static level: LogLevel = LogLevel.Info;
|
||||
|
||||
/**
|
||||
* Whether debug messages should be shown (shortcut for level = Debug).
|
||||
*/
|
||||
public static get DEBUG(): boolean {
|
||||
return Logger.level === LogLevel.Debug;
|
||||
}
|
||||
|
||||
public static set DEBUG(value: boolean) {
|
||||
Logger.level = value ? LogLevel.Debug : LogLevel.Info;
|
||||
}
|
||||
|
||||
/**
|
||||
* The module name of the logger.
|
||||
*/
|
||||
public readonly name: string;
|
||||
|
||||
/**
|
||||
* The color of module name.
|
||||
*/
|
||||
public readonly color: string;
|
||||
|
||||
/**
|
||||
* Constructs a new logger.
|
||||
* @param name - The module name.
|
||||
* @param color - The color of the module name (from LoggerColors).
|
||||
*/
|
||||
constructor(name: string, color: string = LoggerColors.White) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the formatted prefix for log messages.
|
||||
*/
|
||||
private prefix(level?: string, levelColor?: string): string {
|
||||
const timestamp = `${LoggerColors.DarkGray}<${LoggerColors.Reset}${formatTimestamp()}${LoggerColors.DarkGray}>`;
|
||||
const module = `${LoggerColors.DarkGray}[${this.color}${this.name}${LoggerColors.DarkGray}]`;
|
||||
|
||||
if (level && levelColor) {
|
||||
const levelTag = `${LoggerColors.DarkGray}[${levelColor}${level}${LoggerColors.DarkGray}]`;
|
||||
return `${timestamp} ${module} ${levelTag}${LoggerColors.Reset}`;
|
||||
}
|
||||
|
||||
return `${timestamp} ${module}${LoggerColors.Reset}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorize arguments by converting Minecraft color codes.
|
||||
*/
|
||||
private colorize(...args: unknown[]): unknown[] {
|
||||
return args.map((arg) => {
|
||||
if (typeof arg === 'string') {
|
||||
return formatMinecraftColorCode(arg);
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message to the console.
|
||||
*/
|
||||
public log(...args: unknown[]): void {
|
||||
console.log(this.prefix(), ...this.colorize(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an info message to the console.
|
||||
*/
|
||||
public info(...args: unknown[]): void {
|
||||
if (Logger.level > LogLevel.Info) return;
|
||||
console.log(this.prefix('Info', LoggerColors.DarkAqua), ...this.colorize(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message to the console.
|
||||
*/
|
||||
public warn(...args: unknown[]): void {
|
||||
if (Logger.level > LogLevel.Warn) return;
|
||||
console.log(this.prefix('Warning', LoggerColors.Yellow), ...this.colorize(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error message to the console.
|
||||
*/
|
||||
public error(...args: unknown[]): void {
|
||||
if (Logger.level > LogLevel.Error) return;
|
||||
console.log(this.prefix('Error', LoggerColors.DarkRed), ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a success message to the console.
|
||||
*/
|
||||
public success(...args: unknown[]): void {
|
||||
if (Logger.level > LogLevel.Info) return;
|
||||
console.log(this.prefix('Success', LoggerColors.Green), ...this.colorize(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug message to the console.
|
||||
* Only shown when DEBUG is true or level is Debug.
|
||||
*/
|
||||
public debug(...args: unknown[]): void {
|
||||
if (Logger.level > LogLevel.Debug) return;
|
||||
console.log(this.prefix('DEBUG', LoggerColors.Red), ...this.colorize(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a chat message to the console.
|
||||
*/
|
||||
public chat(sender: string, message: string): void {
|
||||
if (Logger.level > LogLevel.Info) return;
|
||||
console.log(this.prefix('Chat', LoggerColors.DarkAqua), this.colorize(sender)[0], '>', this.colorize(message)[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a child logger with a sub-module name.
|
||||
*/
|
||||
public child(name: string, color?: string): Logger {
|
||||
return new Logger(`${this.name}:${name}`, color ?? this.color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default logger instance for the bot.
|
||||
*/
|
||||
export const defaultLogger = new Logger('Bot', LoggerColors.Aqua);
|
||||
60
bridge/lib/mineflayer/lib/logger/minecraft-colors.mts
Normal file
60
bridge/lib/mineflayer/lib/logger/minecraft-colors.mts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Minecraft color code to ANSI escape sequence mapping.
|
||||
* Converts Minecraft's § color codes to terminal colors.
|
||||
* Adapted from @serenityjs/logger
|
||||
*/
|
||||
const ansiColors: Record<string, string> = {
|
||||
'0': '\u001B[38;2;0;0;0m', // Black
|
||||
'1': '\u001B[38;2;0;0;170m', // Dark Blue
|
||||
'2': '\u001B[38;2;0;170;0m', // Dark Green
|
||||
'3': '\u001B[38;2;0;170;170m', // Dark Aqua
|
||||
'4': '\u001B[38;2;170;0;0m', // Dark Red
|
||||
'5': '\u001B[38;2;170;0;170m', // Dark Purple
|
||||
'6': '\u001B[38;2;255;170;0m', // Gold
|
||||
'7': '\u001B[38;2;170;170;170m', // Gray
|
||||
'8': '\u001B[38;2;85;85;85m', // Dark Gray
|
||||
'9': '\u001B[38;2;85;85;255m', // Blue
|
||||
a: '\u001B[38;2;85;255;85m', // Green
|
||||
b: '\u001B[38;2;85;255;255m', // Aqua
|
||||
c: '\u001B[38;2;255;85;85m', // Red
|
||||
d: '\u001B[38;2;255;85;255m', // Light Purple
|
||||
e: '\u001B[38;2;255;255;85m', // Yellow
|
||||
f: '\u001B[38;2;255;255;255m', // White
|
||||
g: '\u001B[38;2;221;214;5m', // Minecoin Gold
|
||||
h: '\u001B[38;2;227;212;209m', // Material Quartz
|
||||
i: '\u001B[38;2;206;202;202m', // Material Iron
|
||||
j: '\u001B[38;2;68;58;59m', // Material Netherite
|
||||
m: '\u001B[38;2;151;22;7m', // Material Redstone
|
||||
n: '\u001B[38;2;180;104;77m', // Material Copper
|
||||
p: '\u001B[38;2;222;177;45m', // Material Gold
|
||||
q: '\u001B[38;2;71;160;54m', // Material Emerald
|
||||
r: '\u001B[0m', // Reset
|
||||
s: '\u001B[38;2;44;186;168m', // Material Diamond
|
||||
t: '\u001B[38;2;33;73;123m', // Material Lapis
|
||||
u: '\u001B[38;2;154;92;198m', // Material Amethyst
|
||||
};
|
||||
|
||||
const colorCodeRegex = /§[\da-fk-or-u]/g;
|
||||
|
||||
/**
|
||||
* Converts Minecraft color codes (§) to ANSI escape sequences.
|
||||
* @param text - Text containing Minecraft color codes
|
||||
* @returns Text with ANSI escape sequences
|
||||
*/
|
||||
export function formatMinecraftColorCode(text: string): string {
|
||||
return (
|
||||
text.replace(colorCodeRegex, (match) => {
|
||||
const code = match.charAt(1);
|
||||
return ansiColors[code] || '';
|
||||
}) + '\u001B[0m'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips Minecraft color codes from text.
|
||||
* @param text - Text containing Minecraft color codes
|
||||
* @returns Plain text without color codes
|
||||
*/
|
||||
export function stripMinecraftColorCodes(text: string): string {
|
||||
return text.replace(colorCodeRegex, '');
|
||||
}
|
||||
8
bridge/lib/mineflayer/lib/math.js
Normal file
8
bridge/lib/mineflayer/lib/math.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
exports.clamp = function clamp (min, x, max) {
|
||||
return Math.max(min, Math.min(x, max))
|
||||
}
|
||||
|
||||
exports.euclideanMod = function euclideanMod (numerator, denominator) {
|
||||
const result = numerator % denominator
|
||||
return result < 0 ? result + denominator : result
|
||||
}
|
||||
7
bridge/lib/mineflayer/lib/painting.js
Normal file
7
bridge/lib/mineflayer/lib/painting.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
function Painting (id, pos, name, direction) {
|
||||
this.id = id
|
||||
this.position = pos
|
||||
this.name = name
|
||||
this.direction = direction
|
||||
}
|
||||
module.exports = Painting
|
||||
44
bridge/lib/mineflayer/lib/particle.js
Normal file
44
bridge/lib/mineflayer/lib/particle.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
|
||||
module.exports = loader
|
||||
|
||||
function loader (registry) {
|
||||
class Particle {
|
||||
constructor (id, position, offset, count = 1, movementSpeed = 0, longDistanceRender = false) {
|
||||
this.id = id
|
||||
|
||||
if(registry.particles)
|
||||
Object.assign(this, registry.particles[id] || registry.particlesByName[id])
|
||||
this.position = position
|
||||
this.offset = offset
|
||||
this.count = count
|
||||
this.movementSpeed = movementSpeed
|
||||
this.longDistanceRender = longDistanceRender
|
||||
}
|
||||
|
||||
static fromNetwork (packet) {
|
||||
if (registry.supportFeature('updatedParticlesPacket')) {
|
||||
// TODO: We add extra data that's inside packet.particle.data that varies by the particle's .type
|
||||
return new Particle(
|
||||
packet.particle.type,
|
||||
new Vec3(packet.x, packet.y, packet.z),
|
||||
new Vec3(packet.offsetX, packet.offsetY, packet.offsetZ),
|
||||
packet.amount,
|
||||
packet.velocityOffset,
|
||||
packet.longDistance
|
||||
)
|
||||
} else {
|
||||
return new Particle(
|
||||
packet.particleId,
|
||||
new Vec3(packet.x, packet.y, packet.z),
|
||||
new Vec3(packet.offsetX, packet.offsetY, packet.offsetZ),
|
||||
packet.particles,
|
||||
packet.particleData,
|
||||
packet.longDistance
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Particle
|
||||
}
|
||||
52
bridge/lib/mineflayer/lib/plugin_loader.js
Normal file
52
bridge/lib/mineflayer/lib/plugin_loader.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
const assert = require('assert')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot, options) {
|
||||
let loaded = false
|
||||
const pluginList = []
|
||||
bot.once('inject_allowed', onInjectAllowed)
|
||||
|
||||
function onInjectAllowed () {
|
||||
loaded = true
|
||||
injectPlugins()
|
||||
}
|
||||
|
||||
function loadPlugin (plugin) {
|
||||
assert.ok(typeof plugin === 'function', 'plugin needs to be a function')
|
||||
|
||||
if (hasPlugin(plugin)) {
|
||||
return
|
||||
}
|
||||
|
||||
pluginList.push(plugin)
|
||||
|
||||
if (loaded) {
|
||||
plugin(bot, options)
|
||||
}
|
||||
}
|
||||
|
||||
function loadPlugins (plugins) {
|
||||
// While type checking if already done in the other function, it's useful to do
|
||||
// it here to prevent situations where only half the plugin list is loaded.
|
||||
assert.ok(plugins.filter(plugin => typeof plugin === 'function').length === plugins.length, 'plugins need to be an array of functions')
|
||||
|
||||
plugins.forEach((plugin) => {
|
||||
loadPlugin(plugin)
|
||||
})
|
||||
}
|
||||
|
||||
function injectPlugins () {
|
||||
pluginList.forEach((plugin) => {
|
||||
plugin(bot, options)
|
||||
})
|
||||
}
|
||||
|
||||
function hasPlugin (plugin) {
|
||||
return pluginList.indexOf(plugin) >= 0
|
||||
}
|
||||
|
||||
bot.loadPlugin = loadPlugin
|
||||
bot.loadPlugins = loadPlugins
|
||||
bot.hasPlugin = hasPlugin
|
||||
}
|
||||
115
bridge/lib/mineflayer/lib/plugins/anvil.js
Normal file
115
bridge/lib/mineflayer/lib/plugins/anvil.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
const assert = require('assert')
|
||||
const { sleep } = require('../promise_utils')
|
||||
const { once } = require('../promise_utils')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
const Item = require('prismarine-item')(bot.registry)
|
||||
|
||||
const matchWindowType = window => /minecraft:(?:chipped_|damaged_)?anvil/.test(window.type)
|
||||
|
||||
async function openAnvil (anvilBlock) {
|
||||
const anvil = await bot.openBlock(anvilBlock)
|
||||
if (!matchWindowType(anvil)) {
|
||||
throw new Error('Not a anvil-like window: ' + JSON.stringify(anvil))
|
||||
}
|
||||
|
||||
function err (name) {
|
||||
anvil.close()
|
||||
throw new Error(name)
|
||||
}
|
||||
|
||||
function sendItemName (name) {
|
||||
if (bot.supportFeature('useMCItemName')) {
|
||||
bot._client.writeChannel('MC|ItemName', name)
|
||||
} else {
|
||||
bot._client.write('name_item', { name })
|
||||
}
|
||||
}
|
||||
|
||||
async function addCustomName (name) {
|
||||
if (!name) return
|
||||
for (let i = 1; i < name.length + 1; i++) {
|
||||
sendItemName(name.substring(0, i))
|
||||
await sleep(50)
|
||||
}
|
||||
}
|
||||
async function putInAnvil (itemOne, itemTwo) {
|
||||
await putSomething(0, itemOne.type, itemOne.metadata, itemOne.count, itemOne.nbt)
|
||||
sendItemName('') // sent like this by vnailla
|
||||
if (!bot.supportFeature('useMCItemName')) sendItemName('')
|
||||
await putSomething(1, itemTwo.type, itemTwo.metadata, itemTwo.count, itemTwo.nbt)
|
||||
}
|
||||
|
||||
async function combine (itemOne, itemTwo, name) {
|
||||
if (name?.length > 35) err('Name is too long.')
|
||||
if (bot.supportFeature('useMCItemName')) {
|
||||
bot._client.registerChannel('MC|ItemName', 'string')
|
||||
}
|
||||
|
||||
assert.ok(itemOne && itemTwo)
|
||||
const { xpCost: normalCost } = Item.anvil(itemOne, itemTwo, bot.game.gameMode === 'creative', name)
|
||||
const { xpCost: inverseCost } = Item.anvil(itemTwo, itemOne, bot.game.gameMode === 'creative', name)
|
||||
if (normalCost === 0 && inverseCost === 0) err('Not anvil-able (in either direction), cancelling.')
|
||||
|
||||
const smallest = (normalCost < inverseCost ? normalCost : inverseCost) === 0 ? inverseCost : 0
|
||||
if (bot.game.gameMode !== 'creative' && bot.experience.level < smallest) {
|
||||
err('Player does not have enough xp to do action, cancelling.')
|
||||
}
|
||||
|
||||
const xpPromise = bot.game.gameMode === 'creative' ? Promise.resolve() : once(bot, 'experience')
|
||||
if (normalCost === 0) await putInAnvil(itemTwo, itemOne)
|
||||
else if (inverseCost === 0) await putInAnvil(itemOne, itemTwo)
|
||||
else if (normalCost < inverseCost) await putInAnvil(itemOne, itemTwo)
|
||||
else await putInAnvil(itemTwo, itemOne)
|
||||
|
||||
await addCustomName(name)
|
||||
await bot.putAway(2)
|
||||
await xpPromise
|
||||
}
|
||||
|
||||
async function rename (item, name) {
|
||||
if (name?.length > 35) err('Name is too long.')
|
||||
if (bot.supportFeature('useMCItemName')) {
|
||||
bot._client.registerChannel('MC|ItemName', 'string')
|
||||
}
|
||||
assert.ok(item)
|
||||
const { xpCost: normalCost } = Item.anvil(item, null, bot.game.gameMode === 'creative', name)
|
||||
if (normalCost === 0) err('Not valid rename, cancelling.')
|
||||
|
||||
if (bot.game.gameMode !== 'creative' && bot.experience.level < normalCost) {
|
||||
err('Player does not have enough xp to do action, cancelling.')
|
||||
}
|
||||
const xpPromise = once(bot, 'experience')
|
||||
await putSomething(0, item.type, item.metadata, item.count, item.nbt)
|
||||
sendItemName('') // sent like this by vnailla
|
||||
if (!bot.supportFeature('useMCItemName')) sendItemName('')
|
||||
await addCustomName(name)
|
||||
await bot.putAway(2)
|
||||
await xpPromise
|
||||
}
|
||||
|
||||
async function putSomething (destSlot, itemId, metadata, count, nbt) {
|
||||
const options = {
|
||||
window: anvil,
|
||||
itemType: itemId,
|
||||
metadata,
|
||||
count,
|
||||
nbt,
|
||||
sourceStart: anvil.inventoryStart,
|
||||
sourceEnd: anvil.inventoryEnd,
|
||||
destStart: destSlot,
|
||||
destEnd: destSlot + 1
|
||||
}
|
||||
await bot.transfer(options)
|
||||
}
|
||||
|
||||
anvil.combine = combine
|
||||
anvil.rename = rename
|
||||
|
||||
return anvil
|
||||
}
|
||||
|
||||
bot.openAnvil = openAnvil
|
||||
}
|
||||
194
bridge/lib/mineflayer/lib/plugins/bed.js
Normal file
194
bridge/lib/mineflayer/lib/plugins/bed.js
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
const CARDINAL_DIRECTIONS = ['south', 'west', 'north', 'east']
|
||||
|
||||
function inject (bot) {
|
||||
bot.isSleeping = false
|
||||
|
||||
const beds = new Set(['white_bed', 'orange_bed', 'magenta_bed', 'light_blue_bed', 'yellow_bed', 'lime_bed', 'pink_bed', 'gray_bed',
|
||||
'light_gray_bed', 'cyan_bed', 'purple_bed', 'blue_bed', 'brown_bed', 'green_bed', 'red_bed', 'black_bed', 'bed'])
|
||||
|
||||
function isABed (block) {
|
||||
return beds.has(block.name)
|
||||
}
|
||||
|
||||
function parseBedMetadata (bedBlock) {
|
||||
const metadata = {
|
||||
part: false, // true: head, false: foot
|
||||
occupied: 0,
|
||||
facing: 0, // 0: south, 1: west, 2: north, 3 east
|
||||
headOffset: new Vec3(0, 0, 1)
|
||||
}
|
||||
|
||||
if (bot.supportFeature('blockStateId')) {
|
||||
const state = bedBlock.stateId - bot.registry.blocksByStateId[bedBlock.stateId].minStateId
|
||||
const bitMetadata = state.toString(2).padStart(4, '0') // FACING (first 2 bits), PART (3rd bit), OCCUPIED (4th bit)
|
||||
metadata.part = bitMetadata[3] === '0'
|
||||
metadata.occupied = bitMetadata[2] === '0'
|
||||
|
||||
switch (bitMetadata.slice(0, 2)) {
|
||||
case '00':
|
||||
metadata.facing = 2
|
||||
metadata.headOffset.set(0, 0, -1)
|
||||
break
|
||||
case '10':
|
||||
metadata.facing = 1
|
||||
metadata.headOffset.set(-1, 0, 0)
|
||||
break
|
||||
case '11':
|
||||
metadata.facing = 3
|
||||
metadata.headOffset.set(1, 0, 0)
|
||||
}
|
||||
} else if (bot.supportFeature('blockMetadata')) {
|
||||
const bitMetadata = bedBlock.metadata.toString(2).padStart(4, '0') // PART (1st bit), OCCUPIED (2nd bit), FACING (last 2 bits)
|
||||
metadata.part = bitMetadata[0] === '1'
|
||||
metadata.occupied = bitMetadata[1] === '1'
|
||||
|
||||
switch (bitMetadata.slice(2, 4)) {
|
||||
case '01':
|
||||
metadata.facing = 1
|
||||
metadata.headOffset.set(-1, 0, 0)
|
||||
break
|
||||
case '10':
|
||||
metadata.facing = 2
|
||||
metadata.headOffset.set(0, 0, -1)
|
||||
break
|
||||
case '11':
|
||||
metadata.facing = 3
|
||||
metadata.headOffset.set(1, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
async function wake () {
|
||||
if (!bot.isSleeping) {
|
||||
throw new Error('already awake')
|
||||
} else {
|
||||
bot._client.write('entity_action', {
|
||||
entityId: bot.entity.id,
|
||||
actionId: 2,
|
||||
jumpBoost: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep (bedBlock) {
|
||||
const thunderstorm = bot.isRaining && (bot.thunderState > 0)
|
||||
if (!thunderstorm && !(bot.time.timeOfDay >= 12541 && bot.time.timeOfDay <= 23458)) {
|
||||
throw new Error("it's not night and it's not a thunderstorm")
|
||||
} else if (bot.isSleeping) {
|
||||
throw new Error('already sleeping')
|
||||
} else if (!isABed(bedBlock)) {
|
||||
throw new Error('wrong block : not a bed block')
|
||||
} else {
|
||||
const botPos = bot.entity.position.floored()
|
||||
const metadata = parseBedMetadata(bedBlock)
|
||||
let headPoint = bedBlock.position
|
||||
|
||||
if (metadata.occupied) {
|
||||
throw new Error('the bed is occupied')
|
||||
}
|
||||
|
||||
if (!metadata.part) { // Is foot
|
||||
const upperBlock = bot.blockAt(bedBlock.position.plus(metadata.headOffset))
|
||||
|
||||
if (isABed(upperBlock)) {
|
||||
headPoint = upperBlock.position
|
||||
} else {
|
||||
const lowerBlock = bot.blockAt(bedBlock.position.plus(metadata.headOffset.scaled(-1)))
|
||||
|
||||
if (isABed(lowerBlock)) {
|
||||
// If there are 2 foot parts, minecraft only lets you sleep if you click on the lower one
|
||||
headPoint = bedBlock.position
|
||||
bedBlock = lowerBlock
|
||||
} else {
|
||||
throw new Error("there's only half bed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bot.canDigBlock(bedBlock)) {
|
||||
throw new Error('cant click the bed')
|
||||
}
|
||||
|
||||
const clickRange = [2, -3, -3, 2] // [south, west, north, east]
|
||||
const monsterRange = [7, -8, -8, 7]
|
||||
const oppositeCardinal = (metadata.facing + 2) % CARDINAL_DIRECTIONS.length
|
||||
|
||||
if (clickRange[oppositeCardinal] < 0) {
|
||||
clickRange[oppositeCardinal]--
|
||||
} else {
|
||||
clickRange[oppositeCardinal]++
|
||||
}
|
||||
|
||||
const nwClickCorner = headPoint.offset(clickRange[1], -2, clickRange[2]) // North-West lower corner
|
||||
const seClickCorner = headPoint.offset(clickRange[3], 2, clickRange[0]) // South-East upper corner
|
||||
if (botPos.x > seClickCorner.x || botPos.x < nwClickCorner.x || botPos.y > seClickCorner.y || botPos.y < nwClickCorner.y || botPos.z > seClickCorner.z || botPos.z < nwClickCorner.z) {
|
||||
throw new Error('the bed is too far')
|
||||
}
|
||||
|
||||
if (bot.game.gameMode !== 'creative' || bot.supportFeature('creativeSleepNearMobs')) { // If in creative mode the bot should be able to sleep even if there are monster nearby (starting in 1.13)
|
||||
const nwMonsterCorner = headPoint.offset(monsterRange[1], -6, monsterRange[2]) // North-West lower corner
|
||||
const seMonsterCorner = headPoint.offset(monsterRange[3], 4, monsterRange[0]) // South-East upper corner
|
||||
|
||||
for (const key of Object.keys(bot.entities)) {
|
||||
const entity = bot.entities[key]
|
||||
if (entity.kind === 'Hostile mobs') {
|
||||
const entityPos = entity.position.floored()
|
||||
if (entityPos.x <= seMonsterCorner.x && entityPos.x >= nwMonsterCorner.x && entityPos.y <= seMonsterCorner.y && entityPos.y >= nwMonsterCorner.y && entityPos.z <= seMonsterCorner.z && entityPos.z >= nwMonsterCorner.z) {
|
||||
throw new Error('there are monsters nearby')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to register the listener before actually going to sleep to avoid race conditions
|
||||
const waitingPromise = waitUntilSleep()
|
||||
bot.activateBlock(bedBlock)
|
||||
await waitingPromise
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilSleep () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutForSleep = setTimeout(() => {
|
||||
reject(new Error('bot is not sleeping'))
|
||||
}, 3000)
|
||||
|
||||
bot.once('sleep', () => {
|
||||
clearTimeout(timeoutForSleep)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
bot._client.on('game_state_change', (packet) => {
|
||||
if (packet.reason === 0 || packet.reason === 'no_respawn_block_available') {
|
||||
// occurs when you can't spawn in your bed and your spawn point gets reset
|
||||
bot.emit('spawnReset')
|
||||
}
|
||||
})
|
||||
|
||||
bot.on('entitySleep', (entity) => {
|
||||
if (entity === bot.entity) {
|
||||
bot.isSleeping = true
|
||||
bot.emit('sleep')
|
||||
}
|
||||
})
|
||||
|
||||
bot.on('entityWake', (entity) => {
|
||||
if (entity === bot.entity) {
|
||||
bot.isSleeping = false
|
||||
bot.emit('wake')
|
||||
}
|
||||
})
|
||||
|
||||
bot.parseBedMetadata = parseBedMetadata
|
||||
bot.wake = wake
|
||||
bot.sleep = sleep
|
||||
bot.isABed = isABed
|
||||
}
|
||||
113
bridge/lib/mineflayer/lib/plugins/block_actions.js
Normal file
113
bridge/lib/mineflayer/lib/plugins/block_actions.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
const CARDINALS = {
|
||||
north: new Vec3(0, 0, -1),
|
||||
south: new Vec3(0, 0, 1),
|
||||
west: new Vec3(-1, 0, 0),
|
||||
east: new Vec3(1, 0, 0)
|
||||
}
|
||||
|
||||
const FACING_MAP = {
|
||||
north: { west: 'right', east: 'left' },
|
||||
south: { west: 'left', east: 'right' },
|
||||
west: { north: 'left', south: 'right' },
|
||||
east: { north: 'right', south: 'left' }
|
||||
}
|
||||
|
||||
function inject (bot) {
|
||||
const { instruments, blocks } = bot.registry
|
||||
|
||||
// Stores how many players have currently open a container at a certain position
|
||||
const openCountByPos = {}
|
||||
|
||||
function parseChestMetadata (chestBlock) {
|
||||
const chestTypes = ['single', 'right', 'left']
|
||||
|
||||
return bot.supportFeature('doesntHaveChestType')
|
||||
? { facing: Object.keys(CARDINALS)[chestBlock.metadata - 2] }
|
||||
: {
|
||||
waterlogged: !(chestBlock.metadata & 1),
|
||||
type: chestTypes[(chestBlock.metadata >> 1) % 3],
|
||||
facing: Object.keys(CARDINALS)[Math.floor(chestBlock.metadata / 6)]
|
||||
}
|
||||
}
|
||||
|
||||
function getChestType (chestBlock) { // Returns 'single', 'right' or 'left'
|
||||
if (bot.supportFeature('doesntHaveChestType')) {
|
||||
const facing = parseChestMetadata(chestBlock).facing
|
||||
|
||||
if (!facing) return 'single'
|
||||
|
||||
// We have to check if the adjacent blocks in the perpendicular cardinals are the same type
|
||||
const perpendicularCardinals = Object.keys(FACING_MAP[facing])
|
||||
for (const cardinal of perpendicularCardinals) {
|
||||
const cardinalOffset = CARDINALS[cardinal]
|
||||
if (bot.blockAt(chestBlock.position.plus(cardinalOffset))?.type === chestBlock.type) {
|
||||
return FACING_MAP[cardinal][facing]
|
||||
}
|
||||
}
|
||||
|
||||
return 'single'
|
||||
} else {
|
||||
return parseChestMetadata(chestBlock).type
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.on('block_action', (packet) => {
|
||||
const pt = new Vec3(packet.location.x, packet.location.y, packet.location.z)
|
||||
const block = bot.blockAt(pt)
|
||||
|
||||
// Ignore on non-vanilla blocks
|
||||
if (block === null || !blocks[packet.blockId]) { return }
|
||||
|
||||
const blockName = blocks[packet.blockId].name
|
||||
|
||||
if (blockName === 'noteblock') { // Pre 1.13
|
||||
bot.emit('noteHeard', block, instruments[packet.byte1], packet.byte2)
|
||||
} else if (blockName === 'note_block') { // 1.13 onward
|
||||
bot.emit('noteHeard', block, instruments[Math.floor(block.metadata / 50)], Math.floor((block.metadata % 50) / 2))
|
||||
} else if (blockName === 'sticky_piston' || blockName === 'piston') {
|
||||
bot.emit('pistonMove', block, packet.byte1, packet.byte2)
|
||||
} else {
|
||||
let block2 = null
|
||||
|
||||
if (blockName === 'chest' || blockName === 'trapped_chest') {
|
||||
const chestType = getChestType(block)
|
||||
if (chestType === 'right') {
|
||||
const index = Object.values(FACING_MAP[parseChestMetadata(block).facing]).indexOf('left')
|
||||
const cardinalBlock2 = Object.keys(FACING_MAP[parseChestMetadata(block).facing])[index]
|
||||
const block2Position = block.position.plus(CARDINALS[cardinalBlock2])
|
||||
block2 = bot.blockAt(block2Position)
|
||||
} else if (chestType === 'left') return // Omit left part of the chest so 'chestLidMove' doesn't emit twice when it's a double chest
|
||||
}
|
||||
|
||||
// Emit 'chestLidMove' only if the number of players with the lid open changes
|
||||
if (openCountByPos[block.position] !== packet.byte2) {
|
||||
bot.emit('chestLidMove', block, packet.byte2, block2)
|
||||
|
||||
if (packet.byte2 > 0) {
|
||||
openCountByPos[block.position] = packet.byte2
|
||||
} else {
|
||||
delete openCountByPos[block.position]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('block_break_animation', (packet) => {
|
||||
const destroyStage = packet.destroyStage
|
||||
const pt = new Vec3(packet.location.x, packet.location.y, packet.location.z)
|
||||
const block = bot.blockAt(pt)
|
||||
const entity = bot.entities[packet.entityId]
|
||||
|
||||
if (destroyStage < 0 || destroyStage > 9) {
|
||||
// http://minecraft.wiki/w/Protocol#Block_Break_Progress
|
||||
// "0-9 to set it, any other value to remove it"
|
||||
bot.emit('blockBreakProgressEnd', block, entity)
|
||||
} else {
|
||||
bot.emit('blockBreakProgressObserved', block, destroyStage, entity)
|
||||
}
|
||||
})
|
||||
}
|
||||
608
bridge/lib/mineflayer/lib/plugins/blocks.js
Normal file
608
bridge/lib/mineflayer/lib/plugins/blocks.js
Normal file
|
|
@ -0,0 +1,608 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
const assert = require('assert')
|
||||
const Painting = require('../painting')
|
||||
const { onceWithCleanup } = require('../promise_utils')
|
||||
|
||||
const { OctahedronIterator } = require('prismarine-world').iterators
|
||||
|
||||
module.exports = inject
|
||||
|
||||
const paintingFaceToVec = [
|
||||
new Vec3(0, 0, -1),
|
||||
new Vec3(-1, 0, 0),
|
||||
new Vec3(0, 0, 1),
|
||||
new Vec3(1, 0, 0)
|
||||
]
|
||||
|
||||
const dimensionNames = {
|
||||
'-1': 'minecraft:nether',
|
||||
0: 'minecraft:overworld',
|
||||
1: 'minecraft:end'
|
||||
}
|
||||
|
||||
function inject (bot, { version, storageBuilder, hideErrors }) {
|
||||
const Block = require('prismarine-block')(bot.registry)
|
||||
const Chunk = require('prismarine-chunk')(bot.registry)
|
||||
const World = require('prismarine-world')(bot.registry)
|
||||
const paintingsByPos = {}
|
||||
const paintingsById = {}
|
||||
|
||||
function addPainting (painting) {
|
||||
paintingsById[painting.id] = painting
|
||||
paintingsByPos[painting.position] = painting
|
||||
}
|
||||
|
||||
function deletePainting (painting) {
|
||||
delete paintingsById[painting.id]
|
||||
delete paintingsByPos[painting.position]
|
||||
}
|
||||
|
||||
function delColumn (chunkX, chunkZ) {
|
||||
bot.world.unloadColumn(chunkX, chunkZ)
|
||||
}
|
||||
|
||||
function addColumn (args) {
|
||||
if (!args.bitMap && args.groundUp) {
|
||||
// stop storing the chunk column
|
||||
delColumn(args.x, args.z)
|
||||
return
|
||||
}
|
||||
let column = bot.world.getColumn(args.x, args.z)
|
||||
if (!column) {
|
||||
// Allocates new chunk object while taking world's custom min/max height into account
|
||||
column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
|
||||
}
|
||||
|
||||
try {
|
||||
column.load(args.data, args.bitMap, args.skyLightSent, args.groundUp)
|
||||
if (args.biomes !== undefined) {
|
||||
column.loadBiomes(args.biomes)
|
||||
}
|
||||
if (args.skyLight !== undefined) {
|
||||
column.loadParsedLight(args.skyLight, args.blockLight, args.skyLightMask, args.blockLightMask, args.emptySkyLightMask, args.emptyBlockLightMask)
|
||||
}
|
||||
bot.world.setColumn(args.x, args.z, column)
|
||||
} catch (e) {
|
||||
bot.emit('error', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForChunksToLoad () {
|
||||
const dist = 2
|
||||
// This makes sure that the bot's real position has been already sent
|
||||
if (!bot.entity.height) await onceWithCleanup(bot, 'chunkColumnLoad')
|
||||
const pos = bot.entity.position
|
||||
const center = new Vec3(pos.x >> 4 << 4, 0, pos.z >> 4 << 4)
|
||||
// get corner coords of 5x5 chunks around us
|
||||
const chunkPosToCheck = new Set()
|
||||
for (let x = -dist; x <= dist; x++) {
|
||||
for (let y = -dist; y <= dist; y++) {
|
||||
// ignore any chunks which are already loaded
|
||||
const pos = center.plus(new Vec3(x, 0, y).scaled(16))
|
||||
if (!bot.world.getColumnAt(pos)) chunkPosToCheck.add(pos.toString())
|
||||
}
|
||||
}
|
||||
|
||||
if (chunkPosToCheck.size) {
|
||||
return new Promise((resolve) => {
|
||||
function waitForLoadEvents (columnCorner) {
|
||||
chunkPosToCheck.delete(columnCorner.toString())
|
||||
if (chunkPosToCheck.size === 0) { // no chunks left to find
|
||||
bot.world.off('chunkColumnLoad', waitForLoadEvents) // remove this listener instance
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// begin listening for remaining chunks to load
|
||||
bot.world.on('chunkColumnLoad', waitForLoadEvents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getMatchingFunction (matching) {
|
||||
if (typeof (matching) !== 'function') {
|
||||
if (!Array.isArray(matching)) {
|
||||
matching = [matching]
|
||||
}
|
||||
return isMatchingType
|
||||
}
|
||||
return matching
|
||||
|
||||
function isMatchingType (block) {
|
||||
return block === null ? false : matching.indexOf(block.type) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
function isBlockInSection (section, matcher) {
|
||||
if (!section) return false // section is empty, skip it (yay!)
|
||||
// If the chunk use a palette we can speed up the search by first
|
||||
// checking the palette which usually contains less than 20 ids
|
||||
// vs checking the 4096 block of the section. If we don't have a
|
||||
// match in the palette, we can skip this section.
|
||||
if (section.palette) {
|
||||
for (const stateId of section.palette) {
|
||||
if (matcher(Block.fromStateId(stateId, 0))) {
|
||||
return true // the block is in the palette
|
||||
}
|
||||
}
|
||||
return false // skip
|
||||
}
|
||||
return true // global palette, the block might be in there
|
||||
}
|
||||
|
||||
function getFullMatchingFunction (matcher, useExtraInfo) {
|
||||
if (typeof (useExtraInfo) === 'boolean') {
|
||||
return fullSearchMatcher
|
||||
}
|
||||
|
||||
return nonFullSearchMatcher
|
||||
|
||||
function nonFullSearchMatcher (point) {
|
||||
const block = blockAt(point, true)
|
||||
return matcher(block) && useExtraInfo(block)
|
||||
}
|
||||
|
||||
function fullSearchMatcher (point) {
|
||||
return matcher(bot.blockAt(point, useExtraInfo))
|
||||
}
|
||||
}
|
||||
|
||||
bot.findBlocks = (options) => {
|
||||
const matcher = getMatchingFunction(options.matching)
|
||||
const point = (options.point || bot.entity.position).floored()
|
||||
const maxDistance = options.maxDistance || 16
|
||||
const count = options.count || 1
|
||||
const useExtraInfo = options.useExtraInfo || false
|
||||
const fullMatcher = getFullMatchingFunction(matcher, useExtraInfo)
|
||||
const start = new Vec3(Math.floor(point.x / 16), Math.floor(point.y / 16), Math.floor(point.z / 16))
|
||||
const it = new OctahedronIterator(start, Math.ceil((maxDistance + 8) / 16))
|
||||
// the octahedron iterator can sometime go through the same section again
|
||||
// we use a set to keep track of visited sections
|
||||
const visitedSections = new Set()
|
||||
|
||||
let blocks = []
|
||||
let startedLayer = 0
|
||||
let next = start
|
||||
while (next) {
|
||||
const column = bot.world.getColumn(next.x, next.z)
|
||||
const sectionY = next.y + Math.abs(bot.game.minY >> 4)
|
||||
const totalSections = bot.game.height >> 4
|
||||
if (sectionY >= 0 && sectionY < totalSections && column && !visitedSections.has(next.toString())) {
|
||||
const section = column.sections[sectionY]
|
||||
if (useExtraInfo === true || isBlockInSection(section, matcher)) {
|
||||
const begin = new Vec3(next.x * 16, sectionY * 16 + bot.game.minY, next.z * 16)
|
||||
const cursor = begin.clone()
|
||||
const end = cursor.offset(16, 16, 16)
|
||||
for (cursor.x = begin.x; cursor.x < end.x; cursor.x++) {
|
||||
for (cursor.y = begin.y; cursor.y < end.y; cursor.y++) {
|
||||
for (cursor.z = begin.z; cursor.z < end.z; cursor.z++) {
|
||||
if (fullMatcher(cursor) && cursor.distanceTo(point) <= maxDistance) blocks.push(cursor.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visitedSections.add(next.toString())
|
||||
}
|
||||
// If we started a layer, we have to finish it otherwise we might miss closer blocks
|
||||
if (startedLayer !== it.apothem && blocks.length >= count) {
|
||||
break
|
||||
}
|
||||
startedLayer = it.apothem
|
||||
next = it.next()
|
||||
}
|
||||
blocks.sort((a, b) => {
|
||||
return a.distanceTo(point) - b.distanceTo(point)
|
||||
})
|
||||
// We found more blocks than needed, shorten the array to not confuse people
|
||||
if (blocks.length > count) {
|
||||
blocks = blocks.slice(0, count)
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
function findBlock (options) {
|
||||
const blocks = bot.findBlocks(options)
|
||||
if (blocks.length === 0) return null
|
||||
return bot.blockAt(blocks[0])
|
||||
}
|
||||
|
||||
function blockAt (absolutePoint, extraInfos = true) {
|
||||
const block = bot.world.getBlock(absolutePoint)
|
||||
// null block means chunk not loaded
|
||||
if (!block) return null
|
||||
|
||||
if (extraInfos) {
|
||||
block.painting = paintingsByPos[block.position]
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
// if passed in block is within line of sight to the bot, returns true
|
||||
// also works on anything with a position value
|
||||
function canSeeBlock (block) {
|
||||
const headPos = bot.entity.position.offset(0, bot.entity.eyeHeight, 0)
|
||||
const range = headPos.distanceTo(block.position)
|
||||
const dir = block.position.offset(0.5, 0.5, 0.5).minus(headPos)
|
||||
const match = (inputBlock, iter) => {
|
||||
const intersect = iter.intersect(inputBlock.shapes, inputBlock.position)
|
||||
if (intersect) { return true }
|
||||
return block.position.equals(inputBlock.position)
|
||||
}
|
||||
const blockAtCursor = bot.world.raycast(headPos, dir.normalize(), range, match)
|
||||
return blockAtCursor && blockAtCursor.position.equals(block.position)
|
||||
}
|
||||
|
||||
bot._client.on('unload_chunk', (packet) => {
|
||||
delColumn(packet.chunkX, packet.chunkZ)
|
||||
})
|
||||
|
||||
function updateBlockState (point, stateId) {
|
||||
const oldBlock = blockAt(point)
|
||||
bot.world.setBlockStateId(point, stateId)
|
||||
|
||||
const newBlock = blockAt(point)
|
||||
// sometimes minecraft server sends us block updates before it sends
|
||||
// us the column that the block is in. ignore this.
|
||||
if (newBlock === null) {
|
||||
return
|
||||
}
|
||||
if (oldBlock.type !== newBlock.type) {
|
||||
const pos = point.floored()
|
||||
const painting = paintingsByPos[pos]
|
||||
if (painting) deletePainting(painting)
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.on('update_light', (packet) => {
|
||||
let column = bot.world.getColumn(packet.chunkX, packet.chunkZ)
|
||||
if (!column) {
|
||||
column = new Chunk({ minY: bot.game.minY, worldHeight: bot.game.height })
|
||||
bot.world.setColumn(packet.chunkX, packet.chunkZ, column)
|
||||
}
|
||||
|
||||
if (bot.supportFeature('newLightingDataFormat')) {
|
||||
column.loadParsedLight(packet.skyLight, packet.blockLight, packet.skyLightMask, packet.blockLightMask, packet.emptySkyLightMask, packet.emptyBlockLightMask)
|
||||
} else {
|
||||
column.loadLight(packet.data, packet.skyLightMask, packet.blockLightMask, packet.emptySkyLightMask, packet.emptyBlockLightMask)
|
||||
}
|
||||
})
|
||||
|
||||
// Chunk batches are used by the server to throttle the chunks per tick for players based on their connection speed.
|
||||
let chunkBatchStartTime = 0
|
||||
// The Vanilla client uses nano seconds with its weighted average starting at 2000000 converted to milliseconds that is 2
|
||||
let weightedAverage = 2
|
||||
// This is used for keeping track of the weight of the old average when updating it.
|
||||
let oldSampleWeight = 1
|
||||
|
||||
bot._client.on('chunk_batch_start', (packet) => {
|
||||
// Get the time the chunk batch is starting.
|
||||
chunkBatchStartTime = Date.now()
|
||||
})
|
||||
|
||||
bot._client.on('chunk_batch_finished', (packet) => {
|
||||
const milliPerChunk = (Date.now() - chunkBatchStartTime) / packet.batchSize
|
||||
// Prevents the MilliPerChunk from being hugely different then the average, Vanilla uses 3 as a constant here.
|
||||
const clampedMilliPerChunk = Math.min(Math.max(milliPerChunk, weightedAverage / 3.0), weightedAverage * 3.0)
|
||||
weightedAverage = ((weightedAverage * oldSampleWeight) + clampedMilliPerChunk) / (oldSampleWeight + 1)
|
||||
// 49 is used in Vanilla client to limit it to 50 samples
|
||||
oldSampleWeight = Math.min(49, oldSampleWeight + 1)
|
||||
bot._client.write('chunk_batch_received', {
|
||||
// Vanilla uses 7000000 as a constant here, since we are using milliseconds that is now 7. Not sure why they pick this constant to convert from nano seconds per chunk to chunks per tick.
|
||||
chunksPerTick: 7 / weightedAverage
|
||||
})
|
||||
})
|
||||
bot._client.on('map_chunk', (packet) => {
|
||||
addColumn({
|
||||
x: packet.x,
|
||||
z: packet.z,
|
||||
bitMap: packet.bitMap,
|
||||
heightmaps: packet.heightmaps,
|
||||
biomes: packet.biomes,
|
||||
skyLightSent: bot.game.dimension === 'overworld',
|
||||
groundUp: packet.groundUp,
|
||||
data: packet.chunkData,
|
||||
trustEdges: packet.trustEdges,
|
||||
skyLightMask: packet.skyLightMask,
|
||||
blockLightMask: packet.blockLightMask,
|
||||
emptySkyLightMask: packet.emptySkyLightMask,
|
||||
emptyBlockLightMask: packet.emptyBlockLightMask,
|
||||
skyLight: packet.skyLight,
|
||||
blockLight: packet.blockLight
|
||||
})
|
||||
|
||||
if (typeof packet.blockEntities !== 'undefined') {
|
||||
const column = bot.world.getColumn(packet.x, packet.z)
|
||||
if (!column) {
|
||||
if (!hideErrors) console.warn('Ignoring block entities as chunk failed to load at', packet.x, packet.z)
|
||||
return
|
||||
}
|
||||
for (const blockEntity of packet.blockEntities) {
|
||||
if (blockEntity.x !== undefined) { // 1.17+
|
||||
column.setBlockEntity(blockEntity, blockEntity.nbtData)
|
||||
} else {
|
||||
const pos = new Vec3(blockEntity.value.x.value & 0xf, blockEntity.value.y.value, blockEntity.value.z.value & 0xf)
|
||||
column.setBlockEntity(pos, blockEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('map_chunk_bulk', (packet) => {
|
||||
let offset = 0
|
||||
let meta
|
||||
let i
|
||||
let size
|
||||
for (i = 0; i < packet.meta.length; ++i) {
|
||||
meta = packet.meta[i]
|
||||
size = (8192 + (packet.skyLightSent ? 2048 : 0)) *
|
||||
onesInShort(meta.bitMap) + // block ids
|
||||
2048 * onesInShort(meta.bitMap) + // (two bytes per block id)
|
||||
256 // biomes
|
||||
addColumn({
|
||||
x: meta.x,
|
||||
z: meta.z,
|
||||
bitMap: meta.bitMap,
|
||||
heightmaps: packet.heightmaps,
|
||||
skyLightSent: packet.skyLightSent,
|
||||
groundUp: true,
|
||||
data: packet.data.slice(offset, offset + size)
|
||||
})
|
||||
offset += size
|
||||
}
|
||||
|
||||
assert.strictEqual(offset, packet.data.length)
|
||||
})
|
||||
|
||||
bot._client.on('multi_block_change', (packet) => {
|
||||
// multi block change
|
||||
for (let i = 0; i < packet.records.length; ++i) {
|
||||
const record = packet.records[i]
|
||||
|
||||
let blockX, blockY, blockZ
|
||||
if (bot.supportFeature('usesMultiblockSingleLong')) {
|
||||
blockZ = (record >> 4) & 0x0f
|
||||
blockX = (record >> 8) & 0x0f
|
||||
blockY = record & 0x0f
|
||||
} else {
|
||||
blockZ = record.horizontalPos & 0x0f
|
||||
blockX = (record.horizontalPos >> 4) & 0x0f
|
||||
blockY = record.y
|
||||
}
|
||||
|
||||
let pt
|
||||
if (bot.supportFeature('usesMultiblock3DChunkCoords')) {
|
||||
pt = new Vec3(packet.chunkCoordinates.x, packet.chunkCoordinates.y, packet.chunkCoordinates.z)
|
||||
} else {
|
||||
pt = new Vec3(packet.chunkX, 0, packet.chunkZ)
|
||||
}
|
||||
|
||||
pt = pt.scale(16).offset(blockX, blockY, blockZ)
|
||||
|
||||
if (bot.supportFeature('usesMultiblockSingleLong')) {
|
||||
updateBlockState(pt, record >> 12)
|
||||
} else {
|
||||
updateBlockState(pt, record.blockId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('block_change', (packet) => {
|
||||
const pt = new Vec3(packet.location.x, packet.location.y, packet.location.z)
|
||||
updateBlockState(pt, packet.type)
|
||||
})
|
||||
|
||||
bot._client.on('explosion', (packet) => {
|
||||
// explosion
|
||||
const p = new Vec3(packet.x, packet.y, packet.z)
|
||||
if (packet.affectedBlockOffsets) {
|
||||
// TODO: server no longer sends in 1.21.3. Is client supposed to compute this or is it sent via normal block updates?
|
||||
packet.affectedBlockOffsets.forEach((offset) => {
|
||||
const pt = p.offset(offset.x, offset.y, offset.z)
|
||||
updateBlockState(pt, 0)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('spawn_entity_painting', (packet) => {
|
||||
const pos = new Vec3(packet.location.x, packet.location.y, packet.location.z)
|
||||
const painting = new Painting(packet.entityId,
|
||||
pos, packet.title, paintingFaceToVec[packet.direction])
|
||||
addPainting(painting)
|
||||
})
|
||||
|
||||
bot._client.on('entity_destroy', (packet) => {
|
||||
// destroy entity
|
||||
packet.entityIds.forEach((id) => {
|
||||
const painting = paintingsById[id]
|
||||
if (painting) deletePainting(painting)
|
||||
})
|
||||
})
|
||||
|
||||
bot._client.on('update_sign', (packet) => {
|
||||
const pos = new Vec3(packet.location.x & 0xf, packet.location.y, packet.location.z & 0xf)
|
||||
|
||||
// TODO: warn if out of loaded world?
|
||||
const column = bot.world.getColumn(packet.location.x >> 4, packet.location.z >> 4)
|
||||
if (!column) {
|
||||
return
|
||||
}
|
||||
|
||||
const blockAt = column.getBlock(pos)
|
||||
|
||||
blockAt.signText = [packet.text1, packet.text2, packet.text3, packet.text4].map(text => {
|
||||
if (text === 'null' || text === '') return ''
|
||||
return JSON.parse(text)
|
||||
})
|
||||
column.setBlock(pos, blockAt)
|
||||
})
|
||||
|
||||
bot._client.on('tile_entity_data', (packet) => {
|
||||
if (packet.location !== undefined) {
|
||||
const column = bot.world.getColumn(packet.location.x >> 4, packet.location.z >> 4)
|
||||
if (!column) return
|
||||
const pos = new Vec3(packet.location.x & 0xf, packet.location.y, packet.location.z & 0xf)
|
||||
column.setBlockEntity(pos, packet.nbtData)
|
||||
} else {
|
||||
const tag = packet.nbtData
|
||||
const column = bot.world.getColumn(tag.value.x.value >> 4, tag.value.z.value >> 4)
|
||||
if (!column) return
|
||||
const pos = new Vec3(tag.value.x.value & 0xf, tag.value.y.value, tag.value.z.value & 0xf)
|
||||
column.setBlockEntity(pos, tag)
|
||||
}
|
||||
})
|
||||
|
||||
bot.updateSign = (block, text, back = false) => {
|
||||
const lines = text.split('\n')
|
||||
if (lines.length > 4) {
|
||||
bot.emit('error', new Error('too many lines for sign text'))
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; ++i) {
|
||||
if (lines[i].length > 45) {
|
||||
bot.emit('error', new Error('Signs have a maximum of 45 characters per line'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let signData
|
||||
if (bot.supportFeature('sendStringifiedSignText')) {
|
||||
signData = {
|
||||
text1: lines[0] ? JSON.stringify(lines[0]) : '""',
|
||||
text2: lines[1] ? JSON.stringify(lines[1]) : '""',
|
||||
text3: lines[2] ? JSON.stringify(lines[2]) : '""',
|
||||
text4: lines[3] ? JSON.stringify(lines[3]) : '""'
|
||||
}
|
||||
} else {
|
||||
signData = {
|
||||
text1: lines[0] ?? '',
|
||||
text2: lines[1] ?? '',
|
||||
text3: lines[2] ?? '',
|
||||
text4: lines[3] ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.write('update_sign', {
|
||||
location: block.position,
|
||||
isFrontText: !back,
|
||||
...signData
|
||||
})
|
||||
}
|
||||
|
||||
// if we get a respawn packet and the dimension is changed,
|
||||
// unload all chunks from memory.
|
||||
let dimension
|
||||
let worldName
|
||||
function dimensionToFolderName (dimension) {
|
||||
if (bot.supportFeature('dimensionIsAnInt')) {
|
||||
return dimensionNames[dimension]
|
||||
} else if (bot.supportFeature('dimensionIsAString') || bot.supportFeature('dimensionIsAWorld')) {
|
||||
return worldName
|
||||
}
|
||||
}
|
||||
// only exposed for testing
|
||||
bot._getDimensionName = () => worldName
|
||||
|
||||
async function switchWorld () {
|
||||
if (bot.world) {
|
||||
if (storageBuilder) {
|
||||
await bot.world.async.waitSaving()
|
||||
}
|
||||
|
||||
for (const [name, listener] of Object.entries(bot._events)) {
|
||||
if (name.startsWith('blockUpdate:') && typeof listener === 'function') {
|
||||
bot.emit(name, null, null)
|
||||
bot.off(name, listener)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [x, z] of Object.keys(bot.world.async.columns).map(key => key.split(',').map(x => parseInt(x, 10)))) {
|
||||
bot.world.unloadColumn(x, z)
|
||||
}
|
||||
|
||||
if (storageBuilder) {
|
||||
bot.world.async.storageProvider = storageBuilder({ version: bot.version, worldName: dimensionToFolderName(dimension) })
|
||||
}
|
||||
} else {
|
||||
bot.world = new World(null, storageBuilder ? storageBuilder({ version: bot.version, worldName: dimensionToFolderName(dimension) }) : null).sync
|
||||
startListenerProxy()
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.on('login', (packet) => {
|
||||
if (bot.supportFeature('dimensionIsAnInt')) {
|
||||
dimension = packet.dimension
|
||||
worldName = dimensionToFolderName(dimension)
|
||||
} else if (bot.supportFeature('spawnRespawnWorldDataField')) { // 1.20.5+
|
||||
dimension = packet.worldState.dimension
|
||||
worldName = packet.worldState.name
|
||||
} else {
|
||||
dimension = packet.dimension
|
||||
worldName = /^minecraft:.+/.test(packet.worldName) ? packet.worldName : `minecraft:${packet.worldName}`
|
||||
}
|
||||
switchWorld()
|
||||
})
|
||||
|
||||
bot._client.on('respawn', (packet) => {
|
||||
if (bot.supportFeature('dimensionIsAnInt')) { // <=1.15.2
|
||||
if (dimension === packet.dimension) return
|
||||
dimension = packet.dimension
|
||||
} else if (bot.supportFeature('spawnRespawnWorldDataField')) { // 1.20.5+
|
||||
if (dimension === packet.worldState.dimension) return
|
||||
if (worldName === packet.worldState.name && packet.copyMetadata === true) return // don't unload chunks if in same world and metaData is true
|
||||
dimension = packet.worldState.dimension
|
||||
worldName = packet.worldState.name
|
||||
} else { // >= 1.15.2
|
||||
if (dimension === packet.dimension) return
|
||||
if (worldName === packet.worldName && packet.copyMetadata === true) return // don't unload chunks if in same world and metaData is true
|
||||
// Metadata is true when switching dimensions however, then the world name is different
|
||||
dimension = packet.dimension
|
||||
worldName = packet.worldName
|
||||
}
|
||||
switchWorld()
|
||||
})
|
||||
|
||||
let listener
|
||||
let listenerRemove
|
||||
function startListenerProxy () {
|
||||
if (listener) {
|
||||
// custom forwarder for custom events
|
||||
bot.off('newListener', listener)
|
||||
bot.off('removeListener', listenerRemove)
|
||||
}
|
||||
// standardized forwarding
|
||||
const forwardedEvents = ['blockUpdate', 'chunkColumnLoad', 'chunkColumnUnload']
|
||||
for (const event of forwardedEvents) {
|
||||
bot.world.on(event, (...args) => bot.emit(event, ...args))
|
||||
}
|
||||
const blockUpdateRegex = /blockUpdate:\(-?\d+, -?\d+, -?\d+\)/
|
||||
listener = (event, listener) => {
|
||||
if (blockUpdateRegex.test(event)) {
|
||||
bot.world.on(event, listener)
|
||||
}
|
||||
}
|
||||
listenerRemove = (event, listener) => {
|
||||
if (blockUpdateRegex.test(event)) {
|
||||
bot.world.off(event, listener)
|
||||
}
|
||||
}
|
||||
bot.on('newListener', listener)
|
||||
bot.on('removeListener', listenerRemove)
|
||||
}
|
||||
|
||||
bot.findBlock = findBlock
|
||||
bot.canSeeBlock = canSeeBlock
|
||||
bot.blockAt = blockAt
|
||||
bot._updateBlockState = updateBlockState
|
||||
bot.waitForChunksToLoad = waitForChunksToLoad
|
||||
}
|
||||
|
||||
function onesInShort (n) {
|
||||
n = n & 0xffff
|
||||
let count = 0
|
||||
for (let i = 0; i < 16; ++i) {
|
||||
count = ((1 << i) & n) ? count + 1 : count
|
||||
}
|
||||
return count
|
||||
}
|
||||
101
bridge/lib/mineflayer/lib/plugins/book.js
Normal file
101
bridge/lib/mineflayer/lib/plugins/book.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
const assert = require('assert')
|
||||
const { once } = require('../promise_utils')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
const Item = require('prismarine-item')(bot.registry)
|
||||
|
||||
let editBook
|
||||
if (bot.supportFeature('editBookIsPluginChannel')) {
|
||||
bot._client.registerChannel('MC|BEdit', 'slot')
|
||||
bot._client.registerChannel('MC|BSign', 'slot')
|
||||
editBook = (book, pages, title, slot, signing = false) => {
|
||||
if (signing) bot._client.writeChannel('MC|BSign', Item.toNotch(book))
|
||||
else bot._client.writeChannel('MC|BEdit', Item.toNotch(book))
|
||||
}
|
||||
} else if (bot.supportFeature('hasEditBookPacket')) {
|
||||
if (bot.supportFeature('editBookPacketUsesNbt')) { // 1.13 - 1.17
|
||||
editBook = (book, pages, title, slot, signing = false, hand = 0) => {
|
||||
bot._client.write('edit_book', {
|
||||
hand: slot,
|
||||
pages,
|
||||
title
|
||||
})
|
||||
}
|
||||
} else { // 1.18+
|
||||
editBook = (book, pages, title, slot, signing = false, hand = 0) => {
|
||||
bot._client.write('edit_book', {
|
||||
new_book: Item.toNotch(book),
|
||||
signing,
|
||||
hand
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function write (slot, pages, author, title, signing) {
|
||||
assert.ok(slot >= 0 && slot <= 44, 'slot out of inventory range')
|
||||
const book = bot.inventory.slots[slot]
|
||||
assert.ok(book && book.type === bot.registry.itemsByName.writable_book.id, `no book found in slot ${slot}`)
|
||||
const quickBarSlot = bot.quickBarSlot
|
||||
const moveToQuickBar = slot < 36
|
||||
|
||||
if (moveToQuickBar) {
|
||||
await bot.moveSlotItem(slot, 36)
|
||||
}
|
||||
|
||||
bot.setQuickBarSlot(moveToQuickBar ? 0 : slot - 36)
|
||||
|
||||
const modifiedBook = await modifyBook(moveToQuickBar ? 36 : slot, pages, author, title, signing)
|
||||
editBook(modifiedBook, pages, title, moveToQuickBar ? 0 : slot - 36, signing)
|
||||
await once(bot.inventory, `updateSlot:${moveToQuickBar ? 36 : slot}`)
|
||||
|
||||
bot.setQuickBarSlot(quickBarSlot)
|
||||
|
||||
if (moveToQuickBar) {
|
||||
await bot.moveSlotItem(36, slot)
|
||||
}
|
||||
}
|
||||
|
||||
function modifyBook (slot, pages, author, title, signing) {
|
||||
const book = Object.assign({}, bot.inventory.slots[slot])
|
||||
if (!book.nbt || book.nbt.type !== 'compound') {
|
||||
book.nbt = {
|
||||
type: 'compound',
|
||||
name: '',
|
||||
value: {}
|
||||
}
|
||||
}
|
||||
if (signing) {
|
||||
if (bot.supportFeature('clientUpdateBookIdWhenSign')) {
|
||||
book.type = bot.registry.itemsByName.written_book.id
|
||||
}
|
||||
book.nbt.value.author = {
|
||||
type: 'string',
|
||||
value: author
|
||||
}
|
||||
book.nbt.value.title = {
|
||||
type: 'string',
|
||||
value: title
|
||||
}
|
||||
}
|
||||
book.nbt.value.pages = {
|
||||
type: 'list',
|
||||
value: {
|
||||
type: 'string',
|
||||
value: pages
|
||||
}
|
||||
}
|
||||
bot.inventory.updateSlot(slot, book)
|
||||
return book
|
||||
}
|
||||
|
||||
bot.writeBook = async (slot, pages) => {
|
||||
await write(slot, pages, null, null, false)
|
||||
}
|
||||
|
||||
bot.signBook = async (slot, pages, author, title) => {
|
||||
await write(slot, pages, author, title, true)
|
||||
}
|
||||
}
|
||||
63
bridge/lib/mineflayer/lib/plugins/boss_bar.js
Normal file
63
bridge/lib/mineflayer/lib/plugins/boss_bar.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
module.exports = inject
|
||||
|
||||
function inject (bot, { version }) {
|
||||
const BossBar = require('../bossbar')(bot.registry)
|
||||
const bars = {}
|
||||
|
||||
function extractTitle (title) {
|
||||
if (!title) return ''
|
||||
if (typeof title === 'string') return title
|
||||
// Return the original object for BossBar to handle
|
||||
return title
|
||||
}
|
||||
|
||||
function handleBossBarPacket (packet) {
|
||||
if (packet.action === 0) {
|
||||
bars[packet.entityUUID] = new BossBar(
|
||||
packet.entityUUID,
|
||||
extractTitle(packet.title),
|
||||
packet.health,
|
||||
packet.dividers,
|
||||
packet.color,
|
||||
packet.flags
|
||||
)
|
||||
bot.emit('bossBarCreated', bars[packet.entityUUID])
|
||||
} else if (packet.action === 1) {
|
||||
bot.emit('bossBarDeleted', bars[packet.entityUUID])
|
||||
delete bars[packet.entityUUID]
|
||||
} else {
|
||||
if (!(packet.entityUUID in bars)) {
|
||||
return
|
||||
}
|
||||
if (packet.action === 2 && packet.health !== undefined) {
|
||||
bars[packet.entityUUID].health = packet.health
|
||||
}
|
||||
if (packet.action === 3 && packet.title !== undefined) {
|
||||
bars[packet.entityUUID].title = extractTitle(packet.title)
|
||||
}
|
||||
if (packet.action === 4) {
|
||||
if (packet.dividers !== undefined) {
|
||||
bars[packet.entityUUID].dividers = packet.dividers
|
||||
}
|
||||
if (packet.color !== undefined) {
|
||||
bars[packet.entityUUID].color = packet.color
|
||||
}
|
||||
}
|
||||
if (packet.action === 5 && packet.flags !== undefined) {
|
||||
bars[packet.entityUUID].flags = packet.flags
|
||||
}
|
||||
bot.emit('bossBarUpdated', bars[packet.entityUUID])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle all possible packet names
|
||||
bot._client.on('boss_bar', handleBossBarPacket)
|
||||
bot._client.on('bossbar', handleBossBarPacket)
|
||||
bot._client.on('boss_bar_update', handleBossBarPacket)
|
||||
|
||||
Object.defineProperty(bot, 'bossBars', {
|
||||
get () {
|
||||
return Object.values(bars)
|
||||
}
|
||||
})
|
||||
}
|
||||
19
bridge/lib/mineflayer/lib/plugins/breath.js
Normal file
19
bridge/lib/mineflayer/lib/plugins/breath.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
if (bot.supportFeature('mcDataHasEntityMetadata')) {
|
||||
// this is handled inside entities.js. We don't yet have entity metadataKeys for all versions but once we do
|
||||
// we can delete the numerical checks here and in entities.js https://github.com/extremeheat/mineflayer/blob/eb9982aa04973b0086aac68a2847005d77f01a3d/lib/plugins/entities.js#L469
|
||||
return
|
||||
}
|
||||
bot._client.on('entity_metadata', (packet) => {
|
||||
if (!bot.entity) return
|
||||
if (bot.entity.id !== packet.entityId) return
|
||||
for (const metadata of packet.metadata) {
|
||||
if (metadata.key === 1) {
|
||||
bot.oxygenLevel = Math.round(metadata.value / 15)
|
||||
bot.emit('breath')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
221
bridge/lib/mineflayer/lib/plugins/chat.js
Normal file
221
bridge/lib/mineflayer/lib/plugins/chat.js
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
const { onceWithCleanup } = require('../promise_utils')
|
||||
|
||||
const USERNAME_REGEX = '(?:\\(.{1,15}\\)|\\[.{1,15}\\]|.){0,5}?(\\w+)'
|
||||
const LEGACY_VANILLA_CHAT_REGEX = new RegExp(`^${USERNAME_REGEX}\\s?[>:\\-»\\]\\)~]+\\s(.*)$`)
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot, options) {
|
||||
const CHAT_LENGTH_LIMIT = options.chatLengthLimit ?? (bot.supportFeature('lessCharsInChat') ? 100 : 256)
|
||||
const defaultChatPatterns = options.defaultChatPatterns ?? true
|
||||
|
||||
const ChatMessage = require('prismarine-chat')(bot.registry)
|
||||
// chat.pattern.type will emit an event for bot.on() of the same type, eg chatType = whisper will trigger bot.on('whisper')
|
||||
const _patterns = {}
|
||||
let _length = 0
|
||||
// deprecated
|
||||
bot.chatAddPattern = (patternValue, typeValue) => {
|
||||
return bot.addChatPattern(typeValue, patternValue, { deprecated: true })
|
||||
}
|
||||
|
||||
bot.addChatPatternSet = (name, patterns, opts = {}) => {
|
||||
if (!patterns.every(p => p instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp')
|
||||
const { repeat = true, parse = false } = opts
|
||||
_patterns[_length++] = {
|
||||
name,
|
||||
patterns,
|
||||
position: 0,
|
||||
matches: [],
|
||||
messages: [],
|
||||
repeat,
|
||||
parse
|
||||
}
|
||||
return _length
|
||||
}
|
||||
|
||||
bot.addChatPattern = (name, pattern, opts = {}) => {
|
||||
if (!(pattern instanceof RegExp)) throw new Error('Pattern parameter should be of type RegExp')
|
||||
const { repeat = true, deprecated = false, parse = false } = opts
|
||||
_patterns[_length] = {
|
||||
name,
|
||||
patterns: [pattern],
|
||||
position: 0,
|
||||
matches: [],
|
||||
messages: [],
|
||||
deprecated,
|
||||
repeat,
|
||||
parse
|
||||
}
|
||||
return _length++ // increment length after we give it back to the user
|
||||
}
|
||||
|
||||
bot.removeChatPattern = name => {
|
||||
if (typeof name === 'number') {
|
||||
_patterns[name] = undefined
|
||||
} else {
|
||||
const matchingPatterns = Object.entries(_patterns).filter(pattern => pattern[1]?.name === name)
|
||||
matchingPatterns.forEach(([indexString]) => {
|
||||
_patterns[+indexString] = undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function findMatchingPatterns (msg) {
|
||||
const found = []
|
||||
for (const [indexString, pattern] of Object.entries(_patterns)) {
|
||||
if (!pattern) continue
|
||||
const { position, patterns } = pattern
|
||||
if (patterns[position].test(msg)) {
|
||||
found.push(+indexString)
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
bot.on('messagestr', (msg, _, originalMsg) => {
|
||||
const foundPatterns = findMatchingPatterns(msg)
|
||||
|
||||
for (const ix of foundPatterns) {
|
||||
_patterns[ix].matches.push(msg)
|
||||
_patterns[ix].messages.push(originalMsg)
|
||||
_patterns[ix].position++
|
||||
|
||||
if (_patterns[ix].deprecated) {
|
||||
const [, ...matches] = _patterns[ix].matches[0].match(_patterns[ix].patterns[0])
|
||||
bot.emit(_patterns[ix].name, ...matches, _patterns[ix].messages[0].translate, ..._patterns[ix].messages)
|
||||
_patterns[ix].messages = [] // clear out old messages
|
||||
} else { // regular parsing
|
||||
if (_patterns[ix].patterns.length > _patterns[ix].matches.length) return // we have all the matches, so we can emit the done event
|
||||
if (_patterns[ix].parse) {
|
||||
const matches = _patterns[ix].patterns.map((pattern, i) => {
|
||||
const [, ...matches] = _patterns[ix].matches[i].match(pattern) // delete full message match
|
||||
return matches
|
||||
})
|
||||
bot.emit(`chat:${_patterns[ix].name}`, matches)
|
||||
} else {
|
||||
bot.emit(`chat:${_patterns[ix].name}`, _patterns[ix].matches)
|
||||
}
|
||||
// these are possibly null-ish if the user deletes them as soon as the event for the match is emitted
|
||||
}
|
||||
if (_patterns[ix]?.repeat) {
|
||||
_patterns[ix].position = 0
|
||||
_patterns[ix].matches = []
|
||||
} else {
|
||||
_patterns[ix] = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addDefaultPatterns()
|
||||
|
||||
bot._client.on('playerChat', (data) => {
|
||||
const message = data.formattedMessage
|
||||
const verified = data.verified
|
||||
let msg
|
||||
if (bot.supportFeature('clientsideChatFormatting')) {
|
||||
const parameters = {
|
||||
sender: data.senderName ? JSON.parse(data.senderName) : undefined,
|
||||
target: data.targetName ? JSON.parse(data.targetName) : undefined,
|
||||
content: message ? JSON.parse(message) : { text: data.plainMessage }
|
||||
}
|
||||
const registryIndex = data.type.chatType != null ? data.type.chatType : data.type
|
||||
msg = ChatMessage.fromNetwork(registryIndex, parameters)
|
||||
|
||||
if (data.unsignedContent) {
|
||||
msg.unsigned = ChatMessage.fromNetwork(registryIndex, { sender: parameters.sender, target: parameters.target, content: JSON.parse(data.unsignedContent) })
|
||||
}
|
||||
} else {
|
||||
msg = ChatMessage.fromNotch(message)
|
||||
}
|
||||
bot.emit('message', msg, 'chat', data.sender, verified)
|
||||
bot.emit('messagestr', msg.toString(), 'chat', msg, data.sender, verified)
|
||||
})
|
||||
|
||||
bot._client.on('systemChat', (data) => {
|
||||
const msg = ChatMessage.fromNotch(data.formattedMessage)
|
||||
const chatPositions = {
|
||||
1: 'system',
|
||||
2: 'game_info'
|
||||
}
|
||||
bot.emit('message', msg, chatPositions[data.positionId], null)
|
||||
bot.emit('messagestr', msg.toString(), chatPositions[data.positionId], msg, null)
|
||||
if (data.positionId === 2) bot.emit('actionBar', msg, null)
|
||||
})
|
||||
|
||||
function chatWithHeader (header, message) {
|
||||
if (typeof message === 'number') message = message.toString()
|
||||
if (typeof message !== 'string') {
|
||||
throw new Error('Chat message type must be a string or number: ' + typeof message)
|
||||
}
|
||||
|
||||
if (!header && message.startsWith('/')) {
|
||||
// Do not try and split a command without a header
|
||||
bot._client.chat(message)
|
||||
return
|
||||
}
|
||||
|
||||
const lengthLimit = CHAT_LENGTH_LIMIT - header.length
|
||||
message.split('\n').forEach((subMessage) => {
|
||||
if (!subMessage) return
|
||||
let i
|
||||
let smallMsg
|
||||
for (i = 0; i < subMessage.length; i += lengthLimit) {
|
||||
smallMsg = header + subMessage.substring(i, i + lengthLimit)
|
||||
bot._client.chat(smallMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function tabComplete (text, assumeCommand = false, sendBlockInSight = true, timeout = 5000) {
|
||||
let position
|
||||
|
||||
if (sendBlockInSight) {
|
||||
const block = bot.blockAtCursor()
|
||||
|
||||
if (block) {
|
||||
position = block.position
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.write('tab_complete', {
|
||||
text,
|
||||
assumeCommand,
|
||||
lookedAtBlock: position
|
||||
})
|
||||
|
||||
const [packet] = await onceWithCleanup(bot._client, 'tab_complete', { timeout })
|
||||
return packet.matches
|
||||
}
|
||||
|
||||
bot.whisper = (username, message) => {
|
||||
chatWithHeader(`/tell ${username} `, message)
|
||||
}
|
||||
bot.chat = (message) => {
|
||||
chatWithHeader('', message)
|
||||
}
|
||||
|
||||
bot.tabComplete = tabComplete
|
||||
|
||||
function addDefaultPatterns () {
|
||||
// 1.19 changes the chat format to move <sender> prefix from message contents to a separate field.
|
||||
// TODO: new chat lister to handle this
|
||||
if (!defaultChatPatterns) return
|
||||
bot.addChatPattern('whisper', new RegExp(`^${USERNAME_REGEX} whispers(?: to you)?:? (.*)$`), { deprecated: true })
|
||||
bot.addChatPattern('whisper', new RegExp(`^\\[${USERNAME_REGEX} -> \\w+\\s?\\] (.*)$`), { deprecated: true })
|
||||
bot.addChatPattern('chat', LEGACY_VANILLA_CHAT_REGEX, { deprecated: true })
|
||||
}
|
||||
|
||||
function awaitMessage (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const resolveMessages = args.flatMap(x => x)
|
||||
function messageListener (msg) {
|
||||
if (resolveMessages.some(x => x instanceof RegExp ? x.test(msg) : msg === x)) {
|
||||
resolve(msg)
|
||||
bot.off('messagestr', messageListener)
|
||||
}
|
||||
}
|
||||
bot.on('messagestr', messageListener)
|
||||
})
|
||||
}
|
||||
bot.awaitMessage = awaitMessage
|
||||
}
|
||||
33
bridge/lib/mineflayer/lib/plugins/chest.js
Normal file
33
bridge/lib/mineflayer/lib/plugins/chest.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
const allowedWindowTypes = ['minecraft:generic', 'minecraft:chest', 'minecraft:dispenser', 'minecraft:ender_chest', 'minecraft:shulker_box', 'minecraft:hopper', 'minecraft:container', 'minecraft:dropper', 'minecraft:trapped_chest', 'minecraft:barrel', 'minecraft:white_shulker_box', 'minecraft:orange_shulker_box', 'minecraft:magenta_shulker_box', 'minecraft:light_blue_shulker_box', 'minecraft:yellow_shulker_box', 'minecraft:lime_shulker_box', 'minecraft:pink_shulker_box', 'minecraft:gray_shulker_box', 'minecraft:light_gray_shulker_box', 'minecraft:cyan_shulker_box', 'minecraft:purple_shulker_box', 'minecraft:blue_shulker_box', 'minecraft:brown_shulker_box', 'minecraft:green_shulker_box', 'minecraft:red_shulker_box', 'minecraft:black_shulker_box']
|
||||
function matchWindowType (window) {
|
||||
for (const type of allowedWindowTypes) {
|
||||
if (window.type.startsWith(type)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function openContainer (containerToOpen, direction, cursorPos) {
|
||||
direction = direction ?? new Vec3(0, 1, 0)
|
||||
cursorPos = cursorPos ?? new Vec3(0.5, 0.5, 0.5)
|
||||
let chest
|
||||
if (containerToOpen.constructor.name === 'Block' && allowedWindowTypes.map(name => name.replace('minecraft:', '')).includes(containerToOpen.name)) {
|
||||
chest = await bot.openBlock(containerToOpen, direction, cursorPos)
|
||||
} else if (containerToOpen.constructor.name === 'Entity') {
|
||||
chest = await bot.openEntity(containerToOpen)
|
||||
} else {
|
||||
throw new Error('containerToOpen is neither a block nor an entity')
|
||||
}
|
||||
|
||||
if (!matchWindowType(chest)) { throw new Error('Non-container window used as a container: ' + JSON.stringify(chest)) }
|
||||
return chest
|
||||
}
|
||||
|
||||
bot.openContainer = openContainer
|
||||
bot.openChest = openContainer
|
||||
bot.openDispenser = openContainer
|
||||
}
|
||||
128
bridge/lib/mineflayer/lib/plugins/command_block.js
Normal file
128
bridge/lib/mineflayer/lib/plugins/command_block.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
const assert = require('assert')
|
||||
const { ProtoDef } = require('protodef')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
function setCommandBlock (pos, command, options = {}) {
|
||||
assert.strictEqual(bot.player.gamemode, 1, new Error('The bot has to be in creative mode to open the command block window'))
|
||||
assert.notStrictEqual(pos, null)
|
||||
assert.notStrictEqual(command, null)
|
||||
assert.strictEqual(bot.blockAt(pos).name.includes('command_block'), true, new Error("The block isn't a command block"))
|
||||
|
||||
// Default values when a command block is placed in vanilla minecraft
|
||||
options.trackOutput = options.trackOutput ?? false
|
||||
options.conditional = options.conditional ?? false
|
||||
options.alwaysActive = options.alwaysActive ?? false
|
||||
options.mode = options.mode ?? 2 // Possible values: 0: SEQUENCE, 1: AUTO and 2: REDSTONE
|
||||
|
||||
let flags = 0
|
||||
flags |= +options.trackOutput << 0 // 0x01
|
||||
flags |= +options.conditional << 1 // 0x02
|
||||
flags |= +options.alwaysActive << 2 // 0x04
|
||||
|
||||
if (bot.supportFeature('usesAdvCmd') || bot.supportFeature('usesAdvCdm')) {
|
||||
const pluginChannelName = bot.supportFeature('usesAdvCdm') ? 'MC|AdvCdm' : 'MC|AdvCmd'
|
||||
|
||||
const proto = new ProtoDef()
|
||||
|
||||
proto.addType('string', [
|
||||
'pstring',
|
||||
{
|
||||
countType: 'varint'
|
||||
}])
|
||||
|
||||
proto.addType(pluginChannelName, [
|
||||
'container',
|
||||
[
|
||||
{
|
||||
name: 'mode',
|
||||
type: 'i8'
|
||||
},
|
||||
{
|
||||
name: 'x',
|
||||
type: [
|
||||
'switch',
|
||||
{
|
||||
compareTo: 'mode',
|
||||
fields: {
|
||||
0: 'i32'
|
||||
},
|
||||
default: 'void'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'y',
|
||||
type: [
|
||||
'switch',
|
||||
{
|
||||
compareTo: 'mode',
|
||||
fields: {
|
||||
0: 'i32'
|
||||
},
|
||||
default: 'void'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'z',
|
||||
type: [
|
||||
'switch',
|
||||
{
|
||||
compareTo: 'mode',
|
||||
fields: {
|
||||
0: 'i32'
|
||||
},
|
||||
default: 'void'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'eid',
|
||||
type: [
|
||||
'switch',
|
||||
{
|
||||
compareTo: 'mode',
|
||||
fields: {
|
||||
1: 'i32'
|
||||
},
|
||||
default: 'void'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'command',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
name: 'trackOutput',
|
||||
type: 'bool'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const buffer = proto.createPacketBuffer(pluginChannelName, {
|
||||
mode: 0,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
z: pos.z,
|
||||
command,
|
||||
trackOutput: options.trackOutput
|
||||
})
|
||||
bot._client.write('custom_payload', {
|
||||
channel: pluginChannelName,
|
||||
data: buffer
|
||||
})
|
||||
} else {
|
||||
bot._client.write('update_command_block', {
|
||||
location: pos,
|
||||
command,
|
||||
mode: options.mode,
|
||||
flags
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bot.setCommandBlock = setCommandBlock
|
||||
}
|
||||
243
bridge/lib/mineflayer/lib/plugins/craft.js
Normal file
243
bridge/lib/mineflayer/lib/plugins/craft.js
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
const assert = require('assert')
|
||||
const { once } = require('../promise_utils')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
const Item = require('prismarine-item')(bot.registry)
|
||||
const Recipe = require('prismarine-recipe')(bot.registry).Recipe
|
||||
let windowCraftingTable
|
||||
|
||||
async function craft (recipe, count, craftingTable) {
|
||||
assert.ok(recipe)
|
||||
count = parseInt(count ?? 1, 10)
|
||||
if (recipe.requiresTable && !craftingTable) {
|
||||
throw new Error('Recipe requires craftingTable, but one was not supplied: ' + JSON.stringify(recipe))
|
||||
}
|
||||
|
||||
try {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await craftOnce(recipe, craftingTable)
|
||||
}
|
||||
|
||||
if (windowCraftingTable) {
|
||||
bot.closeWindow(windowCraftingTable)
|
||||
windowCraftingTable = undefined
|
||||
}
|
||||
} catch (err) {
|
||||
if (windowCraftingTable) {
|
||||
bot.closeWindow(windowCraftingTable)
|
||||
windowCraftingTable = undefined
|
||||
}
|
||||
throw new Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function craftOnce (recipe, craftingTable) {
|
||||
if (craftingTable) {
|
||||
if (!windowCraftingTable) {
|
||||
bot.activateBlock(craftingTable)
|
||||
const [window] = await once(bot, 'windowOpen')
|
||||
windowCraftingTable = window
|
||||
}
|
||||
if (!windowCraftingTable.type.startsWith('minecraft:crafting')) {
|
||||
throw new Error('crafting: non craftingTable used as craftingTable: ' + windowCraftingTable.type)
|
||||
}
|
||||
await startClicking(windowCraftingTable, 3, 3)
|
||||
} else {
|
||||
await startClicking(bot.inventory, 2, 2)
|
||||
}
|
||||
|
||||
async function startClicking (window, w, h) {
|
||||
const extraSlots = unusedRecipeSlots()
|
||||
let ingredientIndex = 0
|
||||
let originalSourceSlot = null
|
||||
let it
|
||||
if (recipe.inShape) {
|
||||
it = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
row: recipe.inShape[0]
|
||||
}
|
||||
await clickShape()
|
||||
} else {
|
||||
await nextIngredientsClick()
|
||||
}
|
||||
|
||||
function incrementShapeIterator () {
|
||||
it.x += 1
|
||||
if (it.x >= it.row.length) {
|
||||
it.y += 1
|
||||
if (it.y >= recipe.inShape.length) return null
|
||||
it.x = 0
|
||||
it.row = recipe.inShape[it.y]
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
async function nextShapeClick () {
|
||||
if (incrementShapeIterator()) {
|
||||
await clickShape()
|
||||
} else if (!recipe.ingredients) {
|
||||
await putMaterialsAway()
|
||||
} else {
|
||||
await nextIngredientsClick()
|
||||
}
|
||||
}
|
||||
|
||||
async function clickShape () {
|
||||
const destSlot = slot(it.x, it.y)
|
||||
const ingredient = it.row[it.x]
|
||||
if (ingredient.id === -1) return nextShapeClick()
|
||||
if (!window.selectedItem || window.selectedItem.type !== ingredient.id ||
|
||||
(ingredient.metadata != null &&
|
||||
window.selectedItem.metadata !== ingredient.metadata)) {
|
||||
// we are not holding the item we need. click it.
|
||||
const sourceItem = window.findInventoryItem(ingredient.id, ingredient.metadata)
|
||||
if (!sourceItem) throw new Error('missing ingredient')
|
||||
if (originalSourceSlot == null) originalSourceSlot = sourceItem.slot
|
||||
await bot.clickWindow(sourceItem.slot, 0, 0)
|
||||
}
|
||||
await bot.clickWindow(destSlot, 1, 0)
|
||||
await nextShapeClick()
|
||||
}
|
||||
|
||||
async function nextIngredientsClick () {
|
||||
const ingredient = recipe.ingredients[ingredientIndex]
|
||||
const destSlot = extraSlots.pop()
|
||||
if (!window.selectedItem || window.selectedItem.type !== ingredient.id ||
|
||||
(ingredient.metadata != null &&
|
||||
window.selectedItem.metadata !== ingredient.metadata)) {
|
||||
// we are not holding the item we need. click it.
|
||||
const sourceItem = window.findInventoryItem(ingredient.id, ingredient.metadata)
|
||||
if (!sourceItem) throw new Error('missing ingredient')
|
||||
if (originalSourceSlot == null) originalSourceSlot = sourceItem.slot
|
||||
await bot.clickWindow(sourceItem.slot, 0, 0)
|
||||
}
|
||||
await bot.clickWindow(destSlot, 1, 0)
|
||||
if (++ingredientIndex < recipe.ingredients.length) {
|
||||
await nextIngredientsClick()
|
||||
} else {
|
||||
await putMaterialsAway()
|
||||
}
|
||||
}
|
||||
|
||||
async function putMaterialsAway () {
|
||||
const start = window.inventoryStart
|
||||
const end = window.inventoryEnd
|
||||
await bot.putSelectedItemRange(start, end, window, originalSourceSlot)
|
||||
await grabResult()
|
||||
}
|
||||
|
||||
async function grabResult () {
|
||||
assert.strictEqual(window.selectedItem, null)
|
||||
// Causes a double-emit on 1.12+ --nickelpro
|
||||
// put the recipe result in the output
|
||||
const item = new Item(recipe.result.id, recipe.result.count, recipe.result.metadata)
|
||||
window.updateSlot(0, item)
|
||||
await bot.putAway(0)
|
||||
await updateOutShape()
|
||||
}
|
||||
|
||||
async function updateOutShape () {
|
||||
if (!recipe.outShape) {
|
||||
for (let i = 1; i <= w * h; i++) {
|
||||
window.updateSlot(i, null)
|
||||
}
|
||||
return
|
||||
}
|
||||
const slotsToClick = []
|
||||
for (let y = 0; y < recipe.outShape.length; ++y) {
|
||||
const row = recipe.outShape[y]
|
||||
for (let x = 0; x < row.length; ++x) {
|
||||
const _slot = slot(x, y)
|
||||
let item = null
|
||||
if (row[x].id !== -1) {
|
||||
item = new Item(row[x].id, row[x].count, row[x].metadata || null)
|
||||
slotsToClick.push(_slot)
|
||||
}
|
||||
window.updateSlot(_slot, item)
|
||||
}
|
||||
}
|
||||
for (const _slot of slotsToClick) {
|
||||
await bot.putAway(_slot)
|
||||
}
|
||||
}
|
||||
|
||||
function slot (x, y) {
|
||||
return 1 + x + w * y
|
||||
}
|
||||
|
||||
function unusedRecipeSlots () {
|
||||
const result = []
|
||||
let x
|
||||
let y
|
||||
let row
|
||||
if (recipe.inShape) {
|
||||
for (y = 0; y < recipe.inShape.length; ++y) {
|
||||
row = recipe.inShape[y]
|
||||
for (x = 0; x < row.length; ++x) {
|
||||
if (row[x].id === -1) result.push(slot(x, y))
|
||||
}
|
||||
for (; x < w; ++x) {
|
||||
result.push(slot(x, y))
|
||||
}
|
||||
}
|
||||
for (; y < h; ++y) {
|
||||
for (x = 0; x < w; ++x) {
|
||||
result.push(slot(x, y))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (y = 0; y < h; ++y) {
|
||||
for (x = 0; x < w; ++x) {
|
||||
result.push(slot(x, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recipesFor (itemType, metadata, minResultCount, craftingTable) {
|
||||
minResultCount = minResultCount ?? 1
|
||||
const results = []
|
||||
Recipe.find(itemType, metadata).forEach((recipe) => {
|
||||
if (requirementsMetForRecipe(recipe, minResultCount, craftingTable)) {
|
||||
results.push(recipe)
|
||||
}
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
function recipesAll (itemType, metadata, craftingTable) {
|
||||
const results = []
|
||||
Recipe.find(itemType, metadata).forEach((recipe) => {
|
||||
if (!recipe.requiresTable || craftingTable) {
|
||||
results.push(recipe)
|
||||
}
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
function requirementsMetForRecipe (recipe, minResultCount, craftingTable) {
|
||||
if (recipe.requiresTable && !craftingTable) return false
|
||||
|
||||
// how many times we have to perform the craft to achieve minResultCount
|
||||
const craftCount = Math.ceil(minResultCount / recipe.result.count)
|
||||
|
||||
// false if not enough inventory to make all the ones that we want
|
||||
for (let i = 0; i < recipe.delta.length; ++i) {
|
||||
const d = recipe.delta[i]
|
||||
if (bot.inventory.count(d.id, d.metadata) + d.count * craftCount < 0) return false
|
||||
}
|
||||
|
||||
// otherwise true
|
||||
return true
|
||||
}
|
||||
|
||||
bot.craft = craft
|
||||
bot.recipesFor = recipesFor
|
||||
bot.recipesAll = recipesAll
|
||||
}
|
||||
112
bridge/lib/mineflayer/lib/plugins/creative.js
Normal file
112
bridge/lib/mineflayer/lib/plugins/creative.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
const assert = require('assert')
|
||||
const { Vec3 } = require('vec3')
|
||||
const { sleep, onceWithCleanup } = require('../promise_utils')
|
||||
const { once } = require('../promise_utils')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
const Item = require('prismarine-item')(bot.registry)
|
||||
|
||||
// these features only work when you are in creative mode.
|
||||
bot.creative = {
|
||||
setInventorySlot,
|
||||
flyTo,
|
||||
startFlying,
|
||||
stopFlying,
|
||||
clearSlot: slotNum => setInventorySlot(slotNum, null),
|
||||
clearInventory
|
||||
}
|
||||
|
||||
const creativeSlotsUpdates = []
|
||||
|
||||
// WARN: This method should not be called twice on the same slot before first promise succeeds
|
||||
async function setInventorySlot (slot, item, waitTimeout = 400) {
|
||||
assert(slot >= 0 && slot <= 44)
|
||||
|
||||
if (Item.equal(bot.inventory.slots[slot], item, true)) return
|
||||
if (creativeSlotsUpdates[slot]) {
|
||||
throw new Error(`Setting slot ${slot} cancelled due to calling bot.creative.setInventorySlot(${slot}, ...) again`)
|
||||
}
|
||||
creativeSlotsUpdates[slot] = true
|
||||
bot._client.write('set_creative_slot', {
|
||||
slot,
|
||||
item: Item.toNotch(item)
|
||||
})
|
||||
|
||||
if (bot.supportFeature('noAckOnCreateSetSlotPacket')) {
|
||||
// No ack
|
||||
bot._setSlot(slot, item)
|
||||
if (waitTimeout === 0) return // no wait
|
||||
// allow some time to see if server rejects
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateSlot (oldItem, newItem) {
|
||||
if (newItem.itemId !== item.itemId) {
|
||||
creativeSlotsUpdates[slot] = false
|
||||
reject(Error('Server rejected'))
|
||||
}
|
||||
}
|
||||
bot.inventory.once(`updateSlot:${slot}`, updateSlot)
|
||||
setTimeout(() => {
|
||||
bot.inventory.off(`updateSlot:${slot}`, updateSlot)
|
||||
creativeSlotsUpdates[slot] = false
|
||||
resolve()
|
||||
}, waitTimeout)
|
||||
})
|
||||
}
|
||||
|
||||
await onceWithCleanup(bot.inventory, `updateSlot:${slot}`, {
|
||||
timeout: 5000,
|
||||
checkCondition: (oldItem, newItem) => item === null ? newItem === null : newItem?.name === item.name && newItem?.count === item.count && newItem?.metadata === item.metadata
|
||||
})
|
||||
creativeSlotsUpdates[slot] = false
|
||||
}
|
||||
|
||||
async function clearInventory () {
|
||||
return Promise.all(bot.inventory.slots.filter(item => item).map(item => setInventorySlot(item.slot, null)))
|
||||
}
|
||||
|
||||
let normalGravity = null
|
||||
const flyingSpeedPerUpdate = 0.5
|
||||
|
||||
// straight line, so make sure there's a clear path.
|
||||
async function flyTo (destination) {
|
||||
// TODO: consider sending 0x13
|
||||
startFlying()
|
||||
|
||||
let vector = destination.minus(bot.entity.position)
|
||||
let magnitude = vecMagnitude(vector)
|
||||
|
||||
while (magnitude > flyingSpeedPerUpdate) {
|
||||
bot.physics.gravity = 0
|
||||
bot.entity.velocity = new Vec3(0, 0, 0)
|
||||
|
||||
// small steps
|
||||
const normalizedVector = vector.scaled(1 / magnitude)
|
||||
bot.entity.position.add(normalizedVector.scaled(flyingSpeedPerUpdate))
|
||||
|
||||
await sleep(50)
|
||||
|
||||
vector = destination.minus(bot.entity.position)
|
||||
magnitude = vecMagnitude(vector)
|
||||
}
|
||||
|
||||
// last step
|
||||
bot.entity.position = destination
|
||||
await once(bot, 'move', /* no timeout */ 0)
|
||||
}
|
||||
|
||||
function startFlying () {
|
||||
if (normalGravity == null) normalGravity = bot.physics.gravity
|
||||
bot.physics.gravity = 0
|
||||
}
|
||||
|
||||
function stopFlying () {
|
||||
bot.physics.gravity = normalGravity
|
||||
}
|
||||
}
|
||||
|
||||
// this should be in the vector library
|
||||
function vecMagnitude (vec) {
|
||||
return Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z)
|
||||
}
|
||||
264
bridge/lib/mineflayer/lib/plugins/digging.js
Normal file
264
bridge/lib/mineflayer/lib/plugins/digging.js
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
const { performance } = require('perf_hooks')
|
||||
const { createDoneTask, createTask } = require('../promise_utils')
|
||||
const BlockFaces = require('prismarine-world').iterators.BlockFace
|
||||
const { Vec3 } = require('vec3')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
let swingInterval = null
|
||||
let waitTimeout = null
|
||||
|
||||
let diggingTask = createDoneTask()
|
||||
|
||||
bot.targetDigBlock = null
|
||||
bot.targetDigFace = null
|
||||
bot.lastDigTime = null
|
||||
|
||||
async function dig (block, forceLook, digFace) {
|
||||
if (block === null || block === undefined) {
|
||||
throw new Error('dig was called with an undefined or null block')
|
||||
}
|
||||
|
||||
if (!digFace || typeof digFace === 'function') {
|
||||
digFace = 'auto'
|
||||
}
|
||||
|
||||
const waitTime = bot.digTime(block)
|
||||
if (waitTime === Infinity) {
|
||||
throw new Error(`dig time for ${block?.name ?? block} is Infinity`)
|
||||
}
|
||||
|
||||
bot.targetDigFace = 1 // Default (top)
|
||||
|
||||
if (forceLook !== 'ignore') {
|
||||
if (digFace?.x || digFace?.y || digFace?.z) {
|
||||
// Determine the block face the bot should mine
|
||||
if (digFace.x) {
|
||||
bot.targetDigFace = digFace.x > 0 ? BlockFaces.EAST : BlockFaces.WEST
|
||||
} else if (digFace.y) {
|
||||
bot.targetDigFace = digFace.y > 0 ? BlockFaces.TOP : BlockFaces.BOTTOM
|
||||
} else if (digFace.z) {
|
||||
bot.targetDigFace = digFace.z > 0 ? BlockFaces.SOUTH : BlockFaces.NORTH
|
||||
}
|
||||
await bot.lookAt(
|
||||
block.position.offset(0.5, 0.5, 0.5).offset(digFace.x * 0.5, digFace.y * 0.5, digFace.z * 0.5),
|
||||
forceLook
|
||||
)
|
||||
} else if (digFace === 'raycast') {
|
||||
// Check faces that could be seen from the current position. If the delta is smaller then 0.5 that means the
|
||||
// bot can most likely not see the face as the block is 1 block thick
|
||||
// this could be false for blocks that have a smaller bounding box than 1x1x1
|
||||
const dx = bot.entity.position.x - (block.position.x + 0.5)
|
||||
const dy = bot.entity.position.y + bot.entity.eyeHeight - (block.position.y + 0.5)
|
||||
const dz = bot.entity.position.z - (block.position.z + 0.5)
|
||||
// Check y first then x and z
|
||||
const visibleFaces = {
|
||||
y: Math.sign(Math.abs(dy) > 0.5 ? dy : 0),
|
||||
x: Math.sign(Math.abs(dx) > 0.5 ? dx : 0),
|
||||
z: Math.sign(Math.abs(dz) > 0.5 ? dz : 0)
|
||||
}
|
||||
const validFaces = []
|
||||
const closerBlocks = []
|
||||
for (const i in visibleFaces) {
|
||||
if (!visibleFaces[i]) continue // skip as this face is not visible
|
||||
// target position on the target block face. -> 0.5 + (current face) * 0.5
|
||||
const targetPos = block.position.offset(
|
||||
0.5 + (i === 'x' ? visibleFaces[i] * 0.5 : 0),
|
||||
0.5 + (i === 'y' ? visibleFaces[i] * 0.5 : 0),
|
||||
0.5 + (i === 'z' ? visibleFaces[i] * 0.5 : 0)
|
||||
)
|
||||
const startPos = bot.entity.position.offset(0, bot.entity.eyeHeight, 0)
|
||||
const rayBlock = bot.world.raycast(startPos, targetPos.clone().subtract(startPos).normalize(), 5)
|
||||
if (rayBlock) {
|
||||
if (startPos.distanceTo(rayBlock.intersect) < startPos.distanceTo(targetPos)) {
|
||||
// Block is closer then the raycasted block
|
||||
closerBlocks.push(rayBlock)
|
||||
// continue since if distance is ever less, then we did not intersect the block we wanted,
|
||||
// meaning that the position of the intersected block is not what we want.
|
||||
continue
|
||||
}
|
||||
const rayPos = rayBlock.position
|
||||
if (
|
||||
rayPos.x === block.position.x &&
|
||||
rayPos.y === block.position.y &&
|
||||
rayPos.z === block.position.z
|
||||
) {
|
||||
validFaces.push({
|
||||
face: rayBlock.face,
|
||||
targetPos: rayBlock.intersect
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validFaces.length > 0) {
|
||||
// Chose closest valid face
|
||||
let closest
|
||||
let distSqrt = 999
|
||||
for (const i in validFaces) {
|
||||
const tPos = validFaces[i].targetPos
|
||||
const cDist = new Vec3(tPos.x, tPos.y, tPos.z).distanceSquared(
|
||||
bot.entity.position.offset(0, bot.entity.eyeHeight, 0)
|
||||
)
|
||||
if (distSqrt > cDist) {
|
||||
closest = validFaces[i]
|
||||
distSqrt = cDist
|
||||
}
|
||||
}
|
||||
await bot.lookAt(closest.targetPos, forceLook)
|
||||
bot.targetDigFace = closest.face
|
||||
} else if (closerBlocks.length === 0 && block.shapes.length === 0) {
|
||||
// no other blocks were detected and the block has no shapes.
|
||||
// The block in question is replaceable (like tall grass) so we can just dig it
|
||||
// TODO: do AABB + ray intercept check to this position for digFace.
|
||||
await bot.lookAt(block.position.offset(0.5, 0.5, 0.5), forceLook)
|
||||
} else {
|
||||
// Block is obstructed return error?
|
||||
throw new Error('Block not in view')
|
||||
}
|
||||
} else {
|
||||
await bot.lookAt(block.position.offset(0.5, 0.5, 0.5), forceLook)
|
||||
}
|
||||
}
|
||||
|
||||
// In vanilla the client will cancel digging the current block once the other block is at the crosshair.
|
||||
// Todo: don't wait until lookAt is at middle of the block, but at the edge of it.
|
||||
if (bot.targetDigBlock) bot.stopDigging()
|
||||
|
||||
diggingTask = createTask()
|
||||
bot._client.write('block_dig', {
|
||||
status: 0, // start digging
|
||||
location: block.position,
|
||||
face: bot.targetDigFace // default face is 1 (top)
|
||||
})
|
||||
waitTimeout = setTimeout(finishDigging, waitTime)
|
||||
bot.targetDigBlock = block
|
||||
bot.swingArm()
|
||||
|
||||
swingInterval = setInterval(() => {
|
||||
bot.swingArm()
|
||||
}, 350)
|
||||
|
||||
function finishDigging () {
|
||||
clearInterval(swingInterval)
|
||||
clearTimeout(waitTimeout)
|
||||
swingInterval = null
|
||||
waitTimeout = null
|
||||
if (bot.targetDigBlock) {
|
||||
bot._client.write('block_dig', {
|
||||
status: 2, // finish digging
|
||||
location: bot.targetDigBlock.position,
|
||||
face: bot.targetDigFace // always the same as the start face
|
||||
})
|
||||
}
|
||||
bot.targetDigBlock = null
|
||||
bot.targetDigFace = null
|
||||
bot.lastDigTime = performance.now()
|
||||
bot._updateBlockState(block.position, 0)
|
||||
}
|
||||
|
||||
const eventName = `blockUpdate:${block.position}`
|
||||
bot.on(eventName, onBlockUpdate)
|
||||
|
||||
const currentBlock = block
|
||||
bot.stopDigging = () => {
|
||||
if (!bot.targetDigBlock) return
|
||||
|
||||
// Replicate the odd vanilla cancellation face value.
|
||||
// When the cancellation is because of a new dig request on another block it's the same as the new dig start face. In all other cases it's 0.
|
||||
const stoppedBecauseOfNewDigRequest = !currentBlock.position.equals(bot.targetDigBlock.position)
|
||||
const cancellationDiggingFace = !stoppedBecauseOfNewDigRequest ? bot.targetDigFace : 0
|
||||
|
||||
bot.removeListener(eventName, onBlockUpdate)
|
||||
clearInterval(swingInterval)
|
||||
clearTimeout(waitTimeout)
|
||||
swingInterval = null
|
||||
waitTimeout = null
|
||||
bot._client.write('block_dig', {
|
||||
status: 1, // cancel digging
|
||||
location: bot.targetDigBlock.position,
|
||||
face: cancellationDiggingFace
|
||||
})
|
||||
const block = bot.targetDigBlock
|
||||
bot.targetDigBlock = null
|
||||
bot.targetDigFace = null
|
||||
bot.lastDigTime = performance.now()
|
||||
bot.emit('diggingAborted', block)
|
||||
bot.stopDigging = noop
|
||||
diggingTask.cancel(new Error('Digging aborted'))
|
||||
}
|
||||
|
||||
function onBlockUpdate (oldBlock, newBlock) {
|
||||
// vanilla server never actually interrupt digging, but some server send block update when you start digging
|
||||
// so ignore block update if not air
|
||||
// All block update listeners receive (null, null) when the world is unloaded. So newBlock can be null.
|
||||
if (newBlock?.type !== 0) return
|
||||
bot.removeListener(eventName, onBlockUpdate)
|
||||
clearInterval(swingInterval)
|
||||
clearTimeout(waitTimeout)
|
||||
swingInterval = null
|
||||
waitTimeout = null
|
||||
bot.targetDigBlock = null
|
||||
bot.targetDigFace = null
|
||||
bot.lastDigTime = performance.now()
|
||||
bot.emit('diggingCompleted', newBlock)
|
||||
diggingTask.finish()
|
||||
}
|
||||
|
||||
await diggingTask.promise
|
||||
}
|
||||
|
||||
bot.on('death', () => {
|
||||
bot.removeAllListeners('diggingAborted')
|
||||
bot.removeAllListeners('diggingCompleted')
|
||||
bot.stopDigging()
|
||||
})
|
||||
|
||||
function canDigBlock (block) {
|
||||
return (
|
||||
block &&
|
||||
block.diggable &&
|
||||
block.position.offset(0.5, 0.5, 0.5).distanceTo(bot.entity.position.offset(0, 1.65, 0)) <= 5.1
|
||||
)
|
||||
}
|
||||
|
||||
function digTime (block) {
|
||||
let type = null
|
||||
let enchantments = []
|
||||
|
||||
// Retrieve currently held item ID and active enchantments from heldItem
|
||||
const currentlyHeldItem = bot.heldItem
|
||||
if (currentlyHeldItem) {
|
||||
type = currentlyHeldItem.type
|
||||
enchantments = currentlyHeldItem.enchants
|
||||
}
|
||||
|
||||
// Append helmet enchantments (because Aqua Affinity actually affects dig speed)
|
||||
const headEquipmentSlot = bot.getEquipmentDestSlot('head')
|
||||
const headEquippedItem = bot.inventory.slots[headEquipmentSlot]
|
||||
if (headEquippedItem) {
|
||||
const helmetEnchantments = headEquippedItem.enchants
|
||||
enchantments = enchantments.concat(helmetEnchantments)
|
||||
}
|
||||
|
||||
const creative = bot.game.gameMode === 'creative'
|
||||
return block.digTime(
|
||||
type,
|
||||
creative,
|
||||
bot.entity.isInWater,
|
||||
!bot.entity.onGround,
|
||||
enchantments,
|
||||
bot.entity.effects
|
||||
)
|
||||
}
|
||||
|
||||
bot.dig = dig
|
||||
bot.stopDigging = noop
|
||||
bot.canDigBlock = canDigBlock
|
||||
bot.digTime = digTime
|
||||
}
|
||||
|
||||
function noop (err) {
|
||||
if (err) throw err
|
||||
}
|
||||
103
bridge/lib/mineflayer/lib/plugins/enchantment_table.js
Normal file
103
bridge/lib/mineflayer/lib/plugins/enchantment_table.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
const assert = require('assert')
|
||||
const { once } = require('../promise_utils')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
async function openEnchantmentTable (enchantmentTableBlock) {
|
||||
assert.strictEqual(enchantmentTableBlock.name, 'enchanting_table')
|
||||
let ready = false
|
||||
const enchantmentTable = await bot.openBlock(enchantmentTableBlock)
|
||||
if (!enchantmentTable.type.startsWith('minecraft:enchant')) {
|
||||
throw new Error('Expected minecraft:enchant when opening table but got ' + enchantmentTable.type)
|
||||
}
|
||||
|
||||
resetEnchantmentOptions()
|
||||
|
||||
enchantmentTable.enchant = enchant
|
||||
enchantmentTable.takeTargetItem = takeTargetItem
|
||||
enchantmentTable.putTargetItem = putTargetItem
|
||||
enchantmentTable.putLapis = putLapis
|
||||
enchantmentTable.targetItem = function () { return this.slots[0] }
|
||||
|
||||
bot._client.on('craft_progress_bar', onUpdateWindowProperty)
|
||||
enchantmentTable.once('close', () => {
|
||||
bot._client.removeListener('craft_progress_bar', onUpdateWindowProperty)
|
||||
})
|
||||
|
||||
return enchantmentTable
|
||||
|
||||
function onUpdateWindowProperty (packet) {
|
||||
if (packet.windowId !== enchantmentTable.id) return
|
||||
assert.ok(packet.property >= 0)
|
||||
|
||||
const slots = enchantmentTable.enchantments
|
||||
|
||||
if (packet.property < 3) {
|
||||
const slot = slots[packet.property]
|
||||
slot.level = packet.value
|
||||
} else if (packet.property === 3) {
|
||||
enchantmentTable.xpseed = packet.value
|
||||
} else if (packet.property < 7) {
|
||||
const slot = slots[packet.property - 4]
|
||||
slot.expected.enchant = packet.value
|
||||
} else if (packet.property < 10) {
|
||||
const slot = slots[packet.property - 7]
|
||||
slot.expected.level = packet.value
|
||||
}
|
||||
|
||||
if (slots[0].level >= 0 && slots[1].level >= 0 && slots[2].level >= 0) {
|
||||
if (!ready) {
|
||||
ready = true
|
||||
enchantmentTable.emit('ready')
|
||||
}
|
||||
} else {
|
||||
ready = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetEnchantmentOptions () {
|
||||
enchantmentTable.xpseed = -1
|
||||
enchantmentTable.enchantments = []
|
||||
for (let slot = 0; slot < 3; slot++) {
|
||||
enchantmentTable.enchantments.push({
|
||||
level: -1,
|
||||
expected: {
|
||||
enchant: -1,
|
||||
level: -1
|
||||
}
|
||||
})
|
||||
}
|
||||
ready = false
|
||||
}
|
||||
|
||||
async function enchant (choice) {
|
||||
if (!ready) await once(enchantmentTable, 'ready')
|
||||
choice = parseInt(choice, 10) // allow string argument
|
||||
assert.notStrictEqual(enchantmentTable.enchantments[choice].level, -1)
|
||||
bot._client.write('enchant_item', {
|
||||
windowId: enchantmentTable.id,
|
||||
enchantment: choice
|
||||
})
|
||||
const [, newItem] = await once(enchantmentTable, 'updateSlot:0')
|
||||
return newItem
|
||||
}
|
||||
|
||||
async function takeTargetItem () {
|
||||
const item = enchantmentTable.targetItem()
|
||||
assert.ok(item)
|
||||
await bot.putAway(item.slot)
|
||||
return item
|
||||
}
|
||||
|
||||
async function putTargetItem (item) {
|
||||
await bot.moveSlotItem(item.slot, 0)
|
||||
}
|
||||
|
||||
async function putLapis (item) {
|
||||
await bot.moveSlotItem(item.slot, 1)
|
||||
}
|
||||
}
|
||||
|
||||
bot.openEnchantmentTable = openEnchantmentTable
|
||||
}
|
||||
956
bridge/lib/mineflayer/lib/plugins/entities.js
Normal file
956
bridge/lib/mineflayer/lib/plugins/entities.js
Normal file
|
|
@ -0,0 +1,956 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
const conv = require('../conversions')
|
||||
// These values are only accurate for versions 1.14 and above (crouch hitbox changes)
|
||||
// Todo: hitbox sizes for sleeping, swimming/crawling, and flying with elytra
|
||||
const PLAYER_HEIGHT = 1.8
|
||||
const CROUCH_HEIGHT = 1.5
|
||||
const PLAYER_WIDTH = 0.6
|
||||
const PLAYER_EYEHEIGHT = 1.62
|
||||
const CROUCH_EYEHEIGHT = 1.27
|
||||
|
||||
module.exports = inject
|
||||
|
||||
const animationEvents = {
|
||||
0: 'entitySwingArm',
|
||||
1: 'entityHurt',
|
||||
2: 'entityWake',
|
||||
3: 'entityEat',
|
||||
4: 'entityCriticalEffect',
|
||||
5: 'entityMagicCriticalEffect'
|
||||
}
|
||||
|
||||
const entityStatusEvents = {
|
||||
2: 'entityHurt',
|
||||
3: 'entityDead',
|
||||
6: 'entityTaming',
|
||||
7: 'entityTamed',
|
||||
8: 'entityShakingOffWater',
|
||||
10: 'entityEatingGrass',
|
||||
55: 'entityHandSwap'
|
||||
}
|
||||
|
||||
function inject (bot) {
|
||||
const { mobs } = bot.registry
|
||||
const Entity = require('prismarine-entity')(bot.version)
|
||||
const Item = require('prismarine-item')(bot.version)
|
||||
const ChatMessage = require('prismarine-chat')(bot.registry)
|
||||
|
||||
// ONLY 1.17 has this destroy_entity packet which is the same thing as entity_destroy packet except the entity is singular
|
||||
// 1.17.1 reverted this change so this is just a simpler fix
|
||||
bot._client.on('destroy_entity', (packet) => {
|
||||
bot._client.emit('entity_destroy', { entityIds: [packet.entityId] })
|
||||
})
|
||||
|
||||
bot.findPlayer = bot.findPlayers = (filter) => {
|
||||
const filterFn = (entity) => {
|
||||
if (entity.type !== 'player') return false
|
||||
if (filter === null) return true
|
||||
if (typeof filter === 'object' && filter instanceof RegExp) {
|
||||
return entity.username.search(filter) !== -1
|
||||
} else if (typeof filter === 'function') {
|
||||
return filter(entity)
|
||||
} else if (typeof filter === 'string') {
|
||||
return entity.username.toLowerCase() === filter.toLowerCase()
|
||||
}
|
||||
return false
|
||||
}
|
||||
const resultSet = Object.values(bot.entities)
|
||||
.filter(filterFn)
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
switch (resultSet.length) {
|
||||
case 0:
|
||||
return null
|
||||
case 1:
|
||||
return resultSet[0]
|
||||
default:
|
||||
return resultSet
|
||||
}
|
||||
}
|
||||
return resultSet
|
||||
}
|
||||
|
||||
bot.players = {}
|
||||
bot.uuidToUsername = {}
|
||||
bot.entities = {}
|
||||
|
||||
bot._playerFromUUID = (uuid) => Object.values(bot.players).find(player => player.uuid === uuid)
|
||||
|
||||
bot.nearestEntity = (match = (entity) => { return true }) => {
|
||||
let best = null
|
||||
let bestDistance = Number.MAX_VALUE
|
||||
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
if (entity === bot.entity || !match(entity)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const dist = bot.entity.position.distanceSquared(entity.position)
|
||||
if (dist < bestDistance) {
|
||||
best = entity
|
||||
bestDistance = dist
|
||||
}
|
||||
}
|
||||
|
||||
return best
|
||||
}
|
||||
|
||||
// Reset list of players and entities on login
|
||||
bot._client.on('login', (packet) => {
|
||||
bot.players = {}
|
||||
bot.uuidToUsername = {}
|
||||
bot.entities = {}
|
||||
// login
|
||||
bot.entity = fetchEntity(packet.entityId)
|
||||
bot.username = bot._client.username
|
||||
bot.entity.username = bot._client.username
|
||||
bot.entity.type = 'player'
|
||||
bot.entity.name = 'player'
|
||||
bot.entity.height = PLAYER_HEIGHT
|
||||
bot.entity.width = PLAYER_WIDTH
|
||||
bot.entity.eyeHeight = PLAYER_EYEHEIGHT
|
||||
})
|
||||
|
||||
bot._client.on('entity_equipment', (packet) => {
|
||||
// entity equipment
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
if (packet.equipments !== undefined) {
|
||||
packet.equipments.forEach(equipment => entity.setEquipment(equipment.slot, equipment.item ? Item.fromNotch(equipment.item) : null))
|
||||
} else {
|
||||
entity.setEquipment(packet.slot, packet.item ? Item.fromNotch(packet.item) : null)
|
||||
}
|
||||
bot.emit('entityEquip', entity)
|
||||
})
|
||||
|
||||
bot._client.on('bed', (packet) => {
|
||||
// use bed
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
entity.position.set(packet.location.x, packet.location.y, packet.location.z)
|
||||
bot.emit('entitySleep', entity)
|
||||
})
|
||||
|
||||
bot._client.on('animation', (packet) => {
|
||||
// animation
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
const eventName = animationEvents[packet.animation]
|
||||
if (eventName) bot.emit(eventName, entity)
|
||||
})
|
||||
|
||||
bot.on('entityCrouch', (entity) => {
|
||||
entity.eyeHeight = CROUCH_EYEHEIGHT
|
||||
entity.height = CROUCH_HEIGHT
|
||||
})
|
||||
|
||||
bot.on('entityUncrouch', (entity) => {
|
||||
entity.eyeHeight = PLAYER_EYEHEIGHT
|
||||
entity.height = PLAYER_HEIGHT
|
||||
})
|
||||
|
||||
bot._client.on('collect', (packet) => {
|
||||
// collect item
|
||||
const collector = fetchEntity(packet.collectorEntityId)
|
||||
const collected = fetchEntity(packet.collectedEntityId)
|
||||
bot.emit('playerCollect', collector, collected)
|
||||
})
|
||||
|
||||
// What is internalId?
|
||||
const entityDataByInternalId = Object.fromEntries(bot.registry.entitiesArray.map((e) => [e.internalId, e]))
|
||||
|
||||
function setEntityData (entity, type, entityData) {
|
||||
entityData ??= entityDataByInternalId[type]
|
||||
if (entityData) {
|
||||
entity.type = entityData.type || 'object'
|
||||
entity.displayName = entityData.displayName
|
||||
entity.entityType = entityData.id
|
||||
entity.name = entityData.name
|
||||
entity.kind = entityData.category
|
||||
entity.height = entityData.height
|
||||
entity.width = entityData.width
|
||||
} else {
|
||||
// unknown entity
|
||||
entity.type = 'other'
|
||||
entity.entityType = type
|
||||
entity.displayName = 'unknown'
|
||||
entity.name = 'unknown'
|
||||
entity.kind = 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
function updateEntityPos (entity, pos) {
|
||||
if (bot.supportFeature('fixedPointPosition')) {
|
||||
entity.position.set(pos.x / 32, pos.y / 32, pos.z / 32)
|
||||
} else if (bot.supportFeature('doublePosition')) {
|
||||
entity.position.set(pos.x, pos.y, pos.z)
|
||||
}
|
||||
entity.yaw = conv.fromNotchianYawByte(pos.yaw)
|
||||
entity.pitch = conv.fromNotchianPitchByte(pos.pitch)
|
||||
}
|
||||
|
||||
function addNewPlayer (entityId, uuid, pos) {
|
||||
const entity = fetchEntity(entityId)
|
||||
entity.type = 'player'
|
||||
entity.name = 'player'
|
||||
entity.username = bot.uuidToUsername[uuid]
|
||||
entity.uuid = uuid
|
||||
updateEntityPos(entity, pos)
|
||||
entity.eyeHeight = PLAYER_EYEHEIGHT
|
||||
entity.height = PLAYER_HEIGHT
|
||||
entity.width = PLAYER_WIDTH
|
||||
if (bot.players[entity.username] !== undefined && !bot.players[entity.username].entity) {
|
||||
bot.players[entity.username].entity = entity
|
||||
}
|
||||
return entity
|
||||
}
|
||||
|
||||
function addNewNonPlayer (entityId, uuid, entityType, pos) {
|
||||
const entity = fetchEntity(entityId)
|
||||
const entityData = bot.registry.entities[entityType]
|
||||
setEntityData(entity, entityType, entityData)
|
||||
updateEntityPos(entity, pos)
|
||||
entity.uuid = uuid
|
||||
return entity
|
||||
}
|
||||
|
||||
bot._client.on('named_entity_spawn', (packet) => {
|
||||
// in case player_info packet was not sent before named_entity_spawn : ignore named_entity_spawn (see #213)
|
||||
if (packet.playerUUID in bot.uuidToUsername) {
|
||||
// spawn named entity
|
||||
const entity = addNewPlayer(packet.entityId, packet.playerUUID, packet, packet.metadata)
|
||||
entity.dataBlobs = packet.data // this field doesn't appear to be listed on any version
|
||||
entity.metadata = parseMetadata(packet.metadata, entity.metadata) // 1.8
|
||||
bot.emit('entitySpawn', entity)
|
||||
}
|
||||
})
|
||||
|
||||
// spawn object/vehicle on versions < 1.19, on versions > 1.19 handles all non-player entities
|
||||
// on versions >= 1.20.2, this also handles player entities
|
||||
bot._client.on('spawn_entity', (packet) => {
|
||||
const entityData = entityDataByInternalId[packet.type]
|
||||
const entity = entityData?.type === 'player'
|
||||
? addNewPlayer(packet.entityId, packet.objectUUID, packet)
|
||||
: addNewNonPlayer(packet.entityId, packet.objectUUID, packet.type, packet)
|
||||
bot.emit('entitySpawn', entity)
|
||||
})
|
||||
|
||||
// spawn_entity_experience_orb packet was removed in 1.21.5+
|
||||
// XP orbs are now handled through the general spawn_entity packet
|
||||
bot._client.on('spawn_entity_experience_orb', (packet) => {
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
entity.type = 'orb'
|
||||
entity.name = 'experience_orb'
|
||||
entity.width = 0.5
|
||||
entity.height = 0.5
|
||||
|
||||
if (bot.supportFeature('fixedPointPosition')) {
|
||||
entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32)
|
||||
} else if (bot.supportFeature('doublePosition')) {
|
||||
entity.position.set(packet.x, packet.y, packet.z)
|
||||
}
|
||||
|
||||
entity.count = packet.count
|
||||
bot.emit('entitySpawn', entity)
|
||||
})
|
||||
|
||||
// This packet is removed since 1.19 and merged into spawn_entity
|
||||
bot._client.on('spawn_entity_living', (packet) => {
|
||||
// spawn mob
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
entity.type = 'mob'
|
||||
entity.uuid = packet.entityUUID
|
||||
const entityData = mobs[packet.type]
|
||||
|
||||
setEntityData(entity, packet.type, entityData)
|
||||
|
||||
if (bot.supportFeature('fixedPointPosition')) {
|
||||
entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32)
|
||||
} else if (bot.supportFeature('doublePosition')) {
|
||||
entity.position.set(packet.x, packet.y, packet.z)
|
||||
}
|
||||
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw)
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch)
|
||||
entity.headPitch = conv.fromNotchianPitchByte(packet.headPitch)
|
||||
|
||||
const notchVel = new Vec3(packet.velocityX, packet.velocityY, packet.velocityZ)
|
||||
entity.velocity.update(conv.fromNotchVelocity(notchVel))
|
||||
entity.metadata = parseMetadata(packet.metadata, entity.metadata)
|
||||
|
||||
bot.emit('entitySpawn', entity)
|
||||
})
|
||||
|
||||
bot._client.on('entity_velocity', (packet) => {
|
||||
// entity velocity
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
const notchVel = new Vec3(packet.velocityX, packet.velocityY, packet.velocityZ)
|
||||
entity.velocity.update(conv.fromNotchVelocity(notchVel))
|
||||
})
|
||||
|
||||
bot._client.on('entity_destroy', (packet) => {
|
||||
// destroy entity
|
||||
packet.entityIds.forEach((id) => {
|
||||
const entity = fetchEntity(id)
|
||||
bot.emit('entityGone', entity)
|
||||
entity.isValid = false
|
||||
if (entity.username && bot.players[entity.username]) {
|
||||
bot.players[entity.username].entity = null
|
||||
}
|
||||
delete bot.entities[id]
|
||||
})
|
||||
})
|
||||
|
||||
bot._client.on('rel_entity_move', (packet) => {
|
||||
// entity relative move
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
if (bot.supportFeature('fixedPointDelta')) {
|
||||
entity.position.translate(packet.dX / 32, packet.dY / 32, packet.dZ / 32)
|
||||
} else if (bot.supportFeature('fixedPointDelta128')) {
|
||||
entity.position.translate(packet.dX / (128 * 32), packet.dY / (128 * 32), packet.dZ / (128 * 32))
|
||||
}
|
||||
bot.emit('entityMoved', entity)
|
||||
})
|
||||
|
||||
bot._client.on('entity_look', (packet) => {
|
||||
// entity look
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw)
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch)
|
||||
bot.emit('entityMoved', entity)
|
||||
})
|
||||
|
||||
bot._client.on('entity_move_look', (packet) => {
|
||||
// entity look and relative move
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
if (bot.supportFeature('fixedPointDelta')) {
|
||||
entity.position.translate(packet.dX / 32, packet.dY / 32, packet.dZ / 32)
|
||||
} else if (bot.supportFeature('fixedPointDelta128')) {
|
||||
entity.position.translate(packet.dX / (128 * 32), packet.dY / (128 * 32), packet.dZ / (128 * 32))
|
||||
}
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw)
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch)
|
||||
bot.emit('entityMoved', entity)
|
||||
})
|
||||
|
||||
bot._client.on('entity_teleport', (packet) => {
|
||||
// entity teleport
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
if (bot.supportFeature('fixedPointPosition')) {
|
||||
entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32)
|
||||
}
|
||||
if (bot.supportFeature('doublePosition')) {
|
||||
entity.position.set(packet.x, packet.y, packet.z)
|
||||
}
|
||||
entity.yaw = conv.fromNotchianYawByte(packet.yaw)
|
||||
entity.pitch = conv.fromNotchianPitchByte(packet.pitch)
|
||||
bot.emit('entityMoved', entity)
|
||||
})
|
||||
|
||||
// 1.21.3 - merges the packets above
|
||||
bot._client.on('sync_entity_position', (packet) => {
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
entity.position.set(packet.x, packet.y, packet.z)
|
||||
entity.velocity.set(packet.dx, packet.dy, packet.dz)
|
||||
entity.yaw = packet.yaw
|
||||
entity.pitch = packet.pitch
|
||||
bot.emit('entityMoved', entity)
|
||||
})
|
||||
|
||||
bot._client.on('entity_head_rotation', (packet) => {
|
||||
// entity head look
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
entity.headYaw = conv.fromNotchianYawByte(packet.headYaw)
|
||||
bot.emit('entityMoved', entity)
|
||||
})
|
||||
|
||||
bot._client.on('entity_status', (packet) => {
|
||||
// entity status
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
const eventName = entityStatusEvents[packet.entityStatus]
|
||||
|
||||
if (eventName === 'entityHandSwap' && entity.equipment) {
|
||||
[entity.equipment[0], entity.equipment[1]] = [entity.equipment[1], entity.equipment[0]]
|
||||
entity.heldItem = entity.equipment[0] // Update held item like prismarine-entity does upon equipment updates
|
||||
}
|
||||
|
||||
if (eventName) bot.emit(eventName, entity)
|
||||
})
|
||||
|
||||
bot._client.on('damage_event', (packet) => { // 1.20+
|
||||
const entity = bot.entities[packet.entityId]
|
||||
const source = bot.entities[packet.sourceCauseId - 1] // damage_event : SourceCauseId : The ID + 1 of the entity responsible for the damage, if present. If not present, the value is 0
|
||||
bot.emit('entityHurt', entity, source)
|
||||
})
|
||||
|
||||
bot._client.on('attach_entity', (packet) => {
|
||||
// attach entity
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
if (packet.vehicleId === -1) {
|
||||
const vehicle = entity.vehicle
|
||||
delete entity.vehicle
|
||||
bot.emit('entityDetach', entity, vehicle)
|
||||
} else {
|
||||
entity.vehicle = fetchEntity(packet.vehicleId)
|
||||
bot.emit('entityAttach', entity, entity.vehicle)
|
||||
}
|
||||
})
|
||||
|
||||
bot.fireworkRocketDuration = 0
|
||||
function setElytraFlyingState (entity, elytraFlying) {
|
||||
let startedFlying = false
|
||||
if (elytraFlying) {
|
||||
startedFlying = !entity.elytraFlying
|
||||
entity.elytraFlying = true
|
||||
} else if (entity.elytraFlying) {
|
||||
entity.elytraFlying = false
|
||||
}
|
||||
if (bot.fireworkRocketDuration !== 0 && entity.id === bot.entity?.id && !elytraFlying) {
|
||||
bot.fireworkRocketDuration = 0
|
||||
knownFireworks.clear()
|
||||
}
|
||||
|
||||
if (startedFlying) {
|
||||
bot.emit('entityElytraFlew', entity)
|
||||
}
|
||||
}
|
||||
|
||||
const knownFireworks = new Set()
|
||||
function handleBotUsedFireworkRocket (fireworkEntityId, fireworkInfo) {
|
||||
if (knownFireworks.has(fireworkEntityId)) return
|
||||
knownFireworks.add(fireworkEntityId)
|
||||
let flightDur = fireworkInfo?.nbtData?.value?.Fireworks?.value?.Flight.value ?? 1
|
||||
if (typeof flightDur !== 'number') { flightDur = 1 }
|
||||
const baseDuration = 10 * (flightDur + 1)
|
||||
const randomDuration = Math.floor(Math.random() * 6) + Math.floor(Math.random() * 7)
|
||||
bot.fireworkRocketDuration = baseDuration + randomDuration
|
||||
|
||||
bot.emit('usedFirework', fireworkEntityId)
|
||||
}
|
||||
|
||||
let fireworkEntityName
|
||||
if (bot.supportFeature('fireworkNamePlural')) {
|
||||
fireworkEntityName = 'fireworks_rocket'
|
||||
} else if (bot.supportFeature('fireworkNameSingular')) {
|
||||
fireworkEntityName = 'firework_rocket'
|
||||
}
|
||||
|
||||
let fireworkMetadataIdx
|
||||
let fireworkMetadataIsOpt
|
||||
if (bot.supportFeature('fireworkMetadataVarInt7')) {
|
||||
fireworkMetadataIdx = 7
|
||||
fireworkMetadataIsOpt = false
|
||||
} else if (bot.supportFeature('fireworkMetadataOptVarInt8')) {
|
||||
fireworkMetadataIdx = 8
|
||||
fireworkMetadataIsOpt = true
|
||||
} else if (bot.supportFeature('fireworkMetadataOptVarInt9')) {
|
||||
fireworkMetadataIdx = 9
|
||||
fireworkMetadataIsOpt = true
|
||||
}
|
||||
const hasFireworkSupport = fireworkEntityName !== undefined && fireworkMetadataIdx !== undefined && fireworkMetadataIsOpt !== undefined
|
||||
|
||||
bot._client.on('entity_metadata', (packet) => {
|
||||
// entity metadata
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
const metadata = parseMetadata(packet.metadata, entity.metadata)
|
||||
entity.metadata = metadata
|
||||
bot.emit('entityUpdate', entity)
|
||||
|
||||
if (bot.supportFeature('mcDataHasEntityMetadata')) {
|
||||
const metadataKeys = bot.registry.entitiesByName[entity.name]?.metadataKeys
|
||||
const metas = metadataKeys ? Object.fromEntries(packet.metadata.map(e => [metadataKeys[e.key], e.value])) : {}
|
||||
if (packet.metadata.some(m => m.type === 'item_stack')) {
|
||||
bot.emit('itemDrop', entity)
|
||||
}
|
||||
if (metas.sleeping_pos || metas.pose === 2) {
|
||||
bot.emit('entitySleep', entity)
|
||||
}
|
||||
|
||||
if (hasFireworkSupport && fireworkEntityName === entity.name && metas.attached_to_target !== undefined) {
|
||||
// fireworkMetadataOptVarInt9 and later is implied by
|
||||
// mcDataHasEntityMetadata, so no need to check metadata index and type
|
||||
// (eg fireworkMetadataOptVarInt8)
|
||||
if (metas.attached_to_target !== 0) {
|
||||
const entityId = metas.attached_to_target - 1
|
||||
if (entityId === bot.entity?.id) {
|
||||
handleBotUsedFireworkRocket(entity.id, metas.fireworks_item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (metas.shared_flags != null) {
|
||||
if (bot.supportFeature('hasElytraFlying')) {
|
||||
const elytraFlying = metas.shared_flags & 0x80
|
||||
setElytraFlyingState(entity, Boolean(elytraFlying))
|
||||
}
|
||||
|
||||
if (metas.shared_flags & 2) {
|
||||
entity.crouching = true
|
||||
bot.emit('entityCrouch', entity)
|
||||
} else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event
|
||||
entity.crouching = false
|
||||
bot.emit('entityUncrouch', entity)
|
||||
}
|
||||
}
|
||||
|
||||
// Breathing (formerly in breath.js)
|
||||
if (metas.air_supply != null) {
|
||||
bot.oxygenLevel = Math.round(metas.air_supply / 15)
|
||||
bot.emit('breath')
|
||||
}
|
||||
} else {
|
||||
const typeSlot = (bot.supportFeature('itemsAreAlsoBlocks') ? 5 : 6) + (bot.supportFeature('entityMetadataHasLong') ? 1 : 0)
|
||||
const slot = packet.metadata.find(e => e.type === typeSlot)
|
||||
if (entity.name && (entity.name.toLowerCase() === 'item' || entity.name === 'item_stack') && slot) {
|
||||
bot.emit('itemDrop', entity)
|
||||
}
|
||||
|
||||
const typePose = bot.supportFeature('entityMetadataHasLong') ? 19 : 18
|
||||
const pose = packet.metadata.find(e => e.type === typePose)
|
||||
if (pose && pose.value === 2) {
|
||||
bot.emit('entitySleep', entity)
|
||||
}
|
||||
|
||||
if (hasFireworkSupport && fireworkEntityName === entity.name) {
|
||||
const attachedToTarget = packet.metadata.find(e => e.key === fireworkMetadataIdx)
|
||||
if (attachedToTarget !== undefined) {
|
||||
let entityId
|
||||
if (fireworkMetadataIsOpt) {
|
||||
if (attachedToTarget.value !== 0) {
|
||||
entityId = attachedToTarget.value - 1
|
||||
} // else, not attached to an entity
|
||||
} else {
|
||||
entityId = attachedToTarget.value
|
||||
}
|
||||
if (entityId !== undefined && entityId === bot.entity?.id) {
|
||||
const fireworksItem = packet.metadata.find(e => e.key === (fireworkMetadataIdx - 1))
|
||||
handleBotUsedFireworkRocket(entity.id, fireworksItem?.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bitField = packet.metadata.find(p => p.key === 0)
|
||||
if (bitField !== undefined) {
|
||||
if (bot.supportFeature('hasElytraFlying')) {
|
||||
const elytraFlying = bitField.value & 0x80
|
||||
setElytraFlyingState(entity, Boolean(elytraFlying))
|
||||
}
|
||||
|
||||
if ((bitField.value & 2) !== 0) {
|
||||
entity.crouching = true
|
||||
bot.emit('entityCrouch', entity)
|
||||
} else if (entity.crouching) { // prevent the initial entity_metadata packet from firing off an uncrouch event
|
||||
entity.crouching = false
|
||||
bot.emit('entityUncrouch', entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('entity_effect', (packet) => {
|
||||
// entity effect
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
const effect = {
|
||||
id: packet.effectId,
|
||||
amplifier: packet.amplifier,
|
||||
duration: packet.duration
|
||||
}
|
||||
entity.effects[effect.id] = effect
|
||||
bot.emit('entityEffect', entity, effect)
|
||||
})
|
||||
|
||||
bot._client.on('remove_entity_effect', (packet) => {
|
||||
// remove entity effect
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
let effect = entity.effects[packet.effectId]
|
||||
if (effect) {
|
||||
delete entity.effects[effect.id]
|
||||
} else {
|
||||
// unknown effect
|
||||
effect = {
|
||||
id: packet.effectId,
|
||||
amplifier: -1,
|
||||
duration: -1
|
||||
}
|
||||
}
|
||||
bot.emit('entityEffectEnd', entity, effect)
|
||||
})
|
||||
|
||||
const updateAttributes = (packet) => {
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
if (!entity.attributes) entity.attributes = {}
|
||||
for (const prop of packet.properties) {
|
||||
entity.attributes[prop.key] = {
|
||||
value: prop.value,
|
||||
modifiers: prop.modifiers
|
||||
}
|
||||
}
|
||||
bot.emit('entityAttributes', entity)
|
||||
}
|
||||
bot._client.on('update_attributes', updateAttributes) // 1.8
|
||||
bot._client.on('entity_update_attributes', updateAttributes) // others
|
||||
|
||||
bot._client.on('spawn_entity_weather', (packet) => {
|
||||
// spawn global entity
|
||||
const entity = fetchEntity(packet.entityId)
|
||||
entity.type = 'global'
|
||||
entity.globalType = 'thunderbolt'
|
||||
entity.uuid = packet.entityUUID
|
||||
entity.position.set(packet.x / 32, packet.y / 32, packet.z / 32)
|
||||
bot.emit('entitySpawn', entity)
|
||||
})
|
||||
|
||||
bot.on('spawn', () => {
|
||||
bot.emit('entitySpawn', bot.entity)
|
||||
})
|
||||
|
||||
function handlePlayerInfoBitfield (packet) {
|
||||
for (const item of packet.data) {
|
||||
let player = bot._playerFromUUID(item.uuid)
|
||||
const newPlayer = !player
|
||||
|
||||
if (newPlayer) {
|
||||
player = { uuid: item.uuid }
|
||||
}
|
||||
|
||||
if (packet.action.add_player) {
|
||||
player.username = item.player.name
|
||||
player.displayName = new ChatMessage({ text: '', extra: [{ text: item.player.name }] })
|
||||
player.skinData = extractSkinInformation(item.player.properties)
|
||||
}
|
||||
if (packet.action.initialize_chat && item.chatSession) {
|
||||
player.chatSession = {
|
||||
publicKey: item.chatSession.publicKey,
|
||||
sessionUuid: item.chatSession.uuid
|
||||
}
|
||||
}
|
||||
if (packet.action.update_game_mode) {
|
||||
player.gamemode = item.gamemode
|
||||
}
|
||||
if (packet.action.update_listed) {
|
||||
player.listed = item.listed
|
||||
}
|
||||
if (packet.action.update_latency) {
|
||||
player.ping = item.latency
|
||||
}
|
||||
if (packet.action.update_display_name) {
|
||||
player.displayName = item.displayName ? ChatMessage.fromNotch(item.displayName) : new ChatMessage({ text: '', extra: [{ text: player.username }] })
|
||||
}
|
||||
|
||||
if (newPlayer) {
|
||||
if (!player.username) continue // Should be unreachable if add_player is always sent for new players
|
||||
bot.players[player.username] = player
|
||||
bot.uuidToUsername[player.uuid] = player.username
|
||||
}
|
||||
|
||||
const playerEntity = Object.values(bot.entities).find(e => e.type === 'player' && e.username === player.username)
|
||||
player.entity = playerEntity
|
||||
|
||||
if (playerEntity === bot.entity) {
|
||||
bot.player = player
|
||||
}
|
||||
|
||||
if (newPlayer) {
|
||||
bot.emit('playerJoined', player)
|
||||
} else {
|
||||
bot.emit('playerUpdated', player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePlayerInfoLegacy (packet) {
|
||||
for (const item of packet.data) {
|
||||
let player = bot._playerFromUUID(item.uuid)
|
||||
|
||||
switch (packet.action) {
|
||||
case 'add_player': {
|
||||
const newPlayer = !player
|
||||
if (newPlayer) {
|
||||
player = bot.players[item.name] = {
|
||||
username: item.name,
|
||||
uuid: item.uuid
|
||||
}
|
||||
bot.uuidToUsername[item.uuid] = item.name
|
||||
}
|
||||
|
||||
player.ping = item.ping
|
||||
player.gamemode = item.gamemode
|
||||
player.displayName = item.displayName ? ChatMessage.fromNotch(item.displayName) : new ChatMessage({ text: '', extra: [{ text: item.name }] })
|
||||
if (item.properties) {
|
||||
player.skinData = extractSkinInformation(item.properties)
|
||||
}
|
||||
if (item.crypto) {
|
||||
player.profileKeys = {
|
||||
publicKey: item.crypto.publicKey,
|
||||
signature: item.crypto.signature
|
||||
}
|
||||
}
|
||||
|
||||
const playerEntity = Object.values(bot.entities).find(e => e.type === 'player' && e.username === item.name)
|
||||
player.entity = playerEntity
|
||||
if (playerEntity === bot.entity) {
|
||||
bot.player = player
|
||||
}
|
||||
|
||||
if (newPlayer) bot.emit('playerJoined', player)
|
||||
else bot.emit('playerUpdated', player)
|
||||
break
|
||||
}
|
||||
case 'update_gamemode': {
|
||||
if (player) {
|
||||
player.gamemode = item.gamemode
|
||||
bot.emit('playerUpdated', player)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'update_latency': {
|
||||
if (player) {
|
||||
player.ping = item.ping
|
||||
bot.emit('playerUpdated', player)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'update_display_name': {
|
||||
if (player) {
|
||||
player.displayName = item.displayName ? ChatMessage.fromNotch(item.displayName) : new ChatMessage({ text: '', extra: [{ text: player.username }] })
|
||||
bot.emit('playerUpdated', player)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'remove_player': {
|
||||
if (player) {
|
||||
if (player.entity === bot.entity) continue
|
||||
player.entity = null
|
||||
delete bot.players[player.username]
|
||||
delete bot.uuidToUsername[item.uuid]
|
||||
bot.emit('playerLeft', player)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bot._client.on('player_info', bot.supportFeature('playerInfoActionIsBitfield') ? handlePlayerInfoBitfield : handlePlayerInfoLegacy)
|
||||
|
||||
// 1.19.3+ - player(s) leave the game
|
||||
bot._client.on('player_remove', (packet) => {
|
||||
for (const uuid of packet.players) {
|
||||
const player = bot._playerFromUUID(uuid)
|
||||
if (!player || player.entity === bot.entity) continue
|
||||
|
||||
player.entity = null
|
||||
delete bot.players[player.username]
|
||||
delete bot.uuidToUsername[uuid]
|
||||
bot.emit('playerLeft', player)
|
||||
}
|
||||
})
|
||||
|
||||
// attaching to a vehicle
|
||||
bot._client.on('attach_entity', (packet) => {
|
||||
const passenger = fetchEntity(packet.entityId)
|
||||
const vehicle = packet.vehicleId === -1 ? null : fetchEntity(packet.vehicleId)
|
||||
|
||||
const originalVehicle = passenger.vehicle
|
||||
if (originalVehicle) {
|
||||
const index = originalVehicle.passengers.indexOf(passenger)
|
||||
originalVehicle.passengers.splice(index, 1)
|
||||
}
|
||||
passenger.vehicle = vehicle
|
||||
if (vehicle) {
|
||||
vehicle.passengers.push(passenger)
|
||||
}
|
||||
|
||||
if (packet.entityId === bot.entity.id) {
|
||||
const vehicle = bot.vehicle
|
||||
if (packet.vehicleId === -1) {
|
||||
bot.vehicle = null
|
||||
bot.emit('dismount', vehicle)
|
||||
} else {
|
||||
bot.vehicle = bot.entities[packet.vehicleId]
|
||||
bot.emit('mount')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('set_passengers', ({ entityId, passengers }) => {
|
||||
const passengerEntities = passengers.map((passengerId) => fetchEntity(passengerId))
|
||||
const vehicle = entityId === -1 ? null : bot.entities[entityId]
|
||||
|
||||
for (const passengerEntity of passengerEntities) {
|
||||
const originalVehicle = passengerEntity.vehicle
|
||||
if (originalVehicle) {
|
||||
const index = originalVehicle.passengers.indexOf(passengerEntity)
|
||||
originalVehicle.passengers.splice(index, 1)
|
||||
}
|
||||
passengerEntity.vehicle = vehicle
|
||||
if (vehicle) {
|
||||
vehicle.passengers.push(passengerEntity)
|
||||
}
|
||||
}
|
||||
|
||||
if (passengers.includes(bot.entity.id)) {
|
||||
const originalVehicle = bot.vehicle
|
||||
if (entityId === -1) {
|
||||
bot.vehicle = null
|
||||
bot.emit('dismount', originalVehicle)
|
||||
} else {
|
||||
bot.vehicle = bot.entities[entityId]
|
||||
bot.emit('mount')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// dismounting when the vehicle is gone
|
||||
bot._client.on('entityGone', (entity) => {
|
||||
if (bot.vehicle === entity) {
|
||||
bot.vehicle = null
|
||||
bot.emit('dismount', (entity))
|
||||
}
|
||||
if (entity.passengers) {
|
||||
for (const passenger of entity.passengers) {
|
||||
passenger.vehicle = null
|
||||
}
|
||||
}
|
||||
if (entity.vehicle) {
|
||||
const index = entity.vehicle.passengers.indexOf(entity)
|
||||
if (index !== -1) {
|
||||
entity.vehicle.passengers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
bot.swingArm = swingArm
|
||||
bot.attack = attack
|
||||
bot.mount = mount
|
||||
bot.dismount = dismount
|
||||
bot.useOn = useOn
|
||||
bot.moveVehicle = moveVehicle
|
||||
|
||||
function swingArm (arm = 'right', showHand = true) {
|
||||
const hand = arm === 'right' ? 0 : 1
|
||||
const packet = {}
|
||||
if (showHand) packet.hand = hand
|
||||
bot._client.write('arm_animation', packet)
|
||||
}
|
||||
|
||||
function useOn (target) {
|
||||
// TODO: check if not crouching will make make this action always use the item
|
||||
useEntity(target, 0)
|
||||
}
|
||||
|
||||
function attack (target, swing = true) {
|
||||
// arm animation comes before the use_entity packet on 1.8
|
||||
if (bot.supportFeature('armAnimationBeforeUse')) {
|
||||
if (swing) {
|
||||
swingArm()
|
||||
}
|
||||
useEntity(target, 1)
|
||||
} else {
|
||||
useEntity(target, 1)
|
||||
if (swing) {
|
||||
swingArm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mount (target) {
|
||||
// TODO: check if crouching will make make this action always mount
|
||||
useEntity(target, 0)
|
||||
}
|
||||
|
||||
function moveVehicle (left, forward) {
|
||||
if (bot.supportFeature('newPlayerInputPacket')) {
|
||||
// docs:
|
||||
// * left can take -1 or 1 : -1 means right, 1 means left
|
||||
// * forward can take -1 or 1 : -1 means backward, 1 means forward
|
||||
bot._client.write('player_input', {
|
||||
inputs: {
|
||||
forward: forward > 0,
|
||||
backward: forward < 0,
|
||||
left: left > 0,
|
||||
right: left < 0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
bot._client.write('steer_vehicle', {
|
||||
sideways: left,
|
||||
forward,
|
||||
jump: 0x01
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function dismount () {
|
||||
if (bot.vehicle) {
|
||||
if (bot.supportFeature('newPlayerInputPacket')) {
|
||||
bot._client.write('player_input', {
|
||||
inputs: {
|
||||
jump: true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
bot._client.write('steer_vehicle', {
|
||||
sideways: 0.0,
|
||||
forward: 0.0,
|
||||
jump: 0x02
|
||||
})
|
||||
}
|
||||
} else {
|
||||
bot.emit('error', new Error('dismount: not mounted'))
|
||||
}
|
||||
}
|
||||
|
||||
function useEntity (target, leftClick, x, y, z) {
|
||||
const sneaking = bot.getControlState('sneak')
|
||||
if (x && y && z) {
|
||||
bot._client.write('use_entity', {
|
||||
target: target.id,
|
||||
mouse: leftClick,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
sneaking
|
||||
})
|
||||
} else {
|
||||
bot._client.write('use_entity', {
|
||||
target: target.id,
|
||||
mouse: leftClick,
|
||||
sneaking
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function fetchEntity (id) {
|
||||
return bot.entities[id] || (bot.entities[id] = new Entity(id))
|
||||
}
|
||||
}
|
||||
|
||||
function parseMetadata (metadata, entityMetadata = {}) {
|
||||
if (metadata !== undefined) {
|
||||
for (const { key, value } of metadata) {
|
||||
entityMetadata[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return entityMetadata
|
||||
}
|
||||
|
||||
function extractSkinInformation (properties) {
|
||||
if (!properties) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const props = Object.fromEntries(properties.map((e) => [e.name, e]))
|
||||
if (!props.textures || !props.textures.value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const skinTexture = JSON.parse(Buffer.from(props.textures.value, 'base64').toString('utf8'))
|
||||
|
||||
const skinTextureUrl = skinTexture?.textures?.SKIN?.url ?? undefined
|
||||
const skinTextureModel = skinTexture?.textures?.SKIN?.metadata?.model ?? undefined
|
||||
|
||||
if (!skinTextureUrl) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return { url: skinTextureUrl, model: skinTextureModel }
|
||||
}
|
||||
15
bridge/lib/mineflayer/lib/plugins/experience.js
Normal file
15
bridge/lib/mineflayer/lib/plugins/experience.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
bot.experience = {
|
||||
level: null,
|
||||
points: null,
|
||||
progress: null
|
||||
}
|
||||
bot._client.on('experience', (packet) => {
|
||||
bot.experience.level = packet.level
|
||||
bot.experience.points = packet.totalExperience
|
||||
bot.experience.progress = packet.experienceBar
|
||||
bot.emit('experience')
|
||||
})
|
||||
}
|
||||
93
bridge/lib/mineflayer/lib/plugins/explosion.js
Normal file
93
bridge/lib/mineflayer/lib/plugins/explosion.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
// https://minecraft.wiki/w/Explosion
|
||||
function calcExposure (playerPos, explosionPos, world) {
|
||||
const dx = 1 / (0.6 * 2 + 1)
|
||||
const dy = 1 / (1.8 * 2 + 1)
|
||||
const dz = 1 / (0.6 * 2 + 1)
|
||||
|
||||
const d3 = (1 - Math.floor(1 / dx) * dx) / 2
|
||||
const d4 = (1 - Math.floor(1 / dz) * dz) / 2
|
||||
|
||||
let sampled = 0
|
||||
let exposed = 0
|
||||
const pos = new Vec3(0, 0, 0)
|
||||
for (pos.y = playerPos.y; pos.y <= playerPos.y + 1.8; pos.y += 1.8 * dy) {
|
||||
for (pos.x = playerPos.x - 0.3 + d3; pos.x <= playerPos.x + 0.3; pos.x += 0.6 * dx) {
|
||||
for (pos.z = playerPos.z - 0.3 + d4; pos.z <= playerPos.z + 0.3; pos.z += 0.6 * dz) {
|
||||
const dir = pos.minus(explosionPos)
|
||||
const range = dir.norm()
|
||||
if (world.raycast(explosionPos, dir.normalize(), range) === null) {
|
||||
exposed++
|
||||
}
|
||||
sampled++
|
||||
}
|
||||
}
|
||||
}
|
||||
return exposed / sampled
|
||||
}
|
||||
|
||||
// https://minecraft.wiki/w/Armor#Damage_protection
|
||||
function getDamageAfterAbsorb (damages, armorValue, toughness) {
|
||||
const var3 = 2 + toughness / 4
|
||||
const var4 = Math.min(Math.max(armorValue - damages / var3, armorValue * 0.2), 20)
|
||||
return damages * (1 - var4 / 25)
|
||||
}
|
||||
|
||||
// https://minecraft.wiki/w/Attribute#Operations
|
||||
function getAttributeValue (prop) {
|
||||
let X = prop.value
|
||||
for (const mod of prop.modifiers) {
|
||||
if (mod.operation !== 0) continue
|
||||
X += mod.amount
|
||||
}
|
||||
let Y = X
|
||||
for (const mod of prop.modifiers) {
|
||||
if (mod.operation !== 1) continue
|
||||
Y += X * mod.amount
|
||||
}
|
||||
for (const mod of prop.modifiers) {
|
||||
if (mod.operation !== 2) continue
|
||||
Y += Y * mod.amount
|
||||
}
|
||||
return Y
|
||||
}
|
||||
|
||||
function inject (bot) {
|
||||
const damageMultiplier = 7 // for 1.12+ 8 for 1.8 TODO check when the change occur (likely 1.9)
|
||||
const armorThoughnessKey = 'generic.armorToughness' // was renamed in 1.16
|
||||
|
||||
const difficultyValues = {
|
||||
peaceful: 0,
|
||||
easy: 1,
|
||||
normal: 2,
|
||||
hard: 3
|
||||
}
|
||||
|
||||
bot.getExplosionDamages = (targetEntity, sourcePos, power, rawDamages = false) => {
|
||||
const distance = targetEntity.position.distanceTo(sourcePos)
|
||||
const radius = 2 * power
|
||||
if (distance >= radius) return 0
|
||||
const exposure = calcExposure(targetEntity.position, sourcePos, bot.world)
|
||||
const impact = (1 - distance / radius) * exposure
|
||||
let damages = Math.floor((impact * impact + impact) * damageMultiplier * power + 1)
|
||||
|
||||
// The following modifiers are constant for the input targetEntity and doesnt depend
|
||||
// on the source position, so if the goal is to compare between positions they can be
|
||||
// ignored to save computations
|
||||
if (!rawDamages && targetEntity.attributes['generic.armor']) {
|
||||
const armor = getAttributeValue(targetEntity.attributes['generic.armor'])
|
||||
const armorToughness = getAttributeValue(targetEntity.attributes[armorThoughnessKey])
|
||||
damages = getDamageAfterAbsorb(damages, armor, armorToughness)
|
||||
|
||||
// TODO: protection enchantment and resistance effects
|
||||
|
||||
if (targetEntity.type === 'player') damages *= difficultyValues[bot.game.difficulty] * 0.5
|
||||
} else if (!rawDamages && !targetEntity.attributes['generic.armor']) {
|
||||
return null
|
||||
}
|
||||
return Math.floor(damages)
|
||||
}
|
||||
}
|
||||
61
bridge/lib/mineflayer/lib/plugins/fishing.js
Normal file
61
bridge/lib/mineflayer/lib/plugins/fishing.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
const { Vec3 } = require('vec3')
|
||||
const { createDoneTask, createTask } = require('../promise_utils')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
let bobberId = 90
|
||||
// Before 1.14 the bobber entity keep changing name at each version (but the id stays 90)
|
||||
// 1.14 changes the id, but hopefully we can stick with the name: fishing_bobber
|
||||
// the alternative would be to rename it in all version of mcData
|
||||
if (bot.supportFeature('fishingBobberCorrectlyNamed')) {
|
||||
bobberId = bot.registry.entitiesByName.fishing_bobber.id
|
||||
}
|
||||
|
||||
let fishingTask = createDoneTask()
|
||||
let lastBobber = null
|
||||
|
||||
bot._client.on('spawn_entity', (packet) => {
|
||||
if (packet.type === bobberId && !fishingTask.done && !lastBobber) {
|
||||
lastBobber = bot.entities[packet.entityId]
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('world_particles', (packet) => {
|
||||
if (!lastBobber || fishingTask.done) return
|
||||
|
||||
const pos = lastBobber.position
|
||||
|
||||
const bobberCondition = bot.registry.supportFeature('updatedParticlesPacket')
|
||||
? ((packet.particle.type === 'fishing' || packet.particle.type === 'bubble') && packet.amount === 6 && pos.distanceTo(new Vec3(packet.x, pos.y, packet.z)) <= 1.23)
|
||||
// This "(particles.fishing ?? particles.bubble).id" condition doesn't make sense (these are both valid types)
|
||||
: (packet.particleId === (bot.registry.particlesByName.fishing ?? bot.registry.particlesByName.bubble).id && packet.particles === 6 && pos.distanceTo(new Vec3(packet.x, pos.y, packet.z)) <= 1.23)
|
||||
|
||||
if (bobberCondition) {
|
||||
bot.activateItem()
|
||||
lastBobber = undefined
|
||||
fishingTask.finish()
|
||||
}
|
||||
})
|
||||
bot._client.on('entity_destroy', (packet) => {
|
||||
if (!lastBobber) return
|
||||
if (packet.entityIds.some(id => id === lastBobber.id)) {
|
||||
lastBobber = undefined
|
||||
fishingTask.cancel(new Error('Fishing cancelled'))
|
||||
}
|
||||
})
|
||||
|
||||
async function fish () {
|
||||
if (!fishingTask.done) {
|
||||
fishingTask.cancel(new Error('Fishing cancelled due to calling bot.fish() again'))
|
||||
}
|
||||
|
||||
fishingTask = createTask()
|
||||
|
||||
bot.activateItem()
|
||||
|
||||
await fishingTask.promise
|
||||
}
|
||||
|
||||
bot.fish = fish
|
||||
}
|
||||
121
bridge/lib/mineflayer/lib/plugins/furnace.js
Normal file
121
bridge/lib/mineflayer/lib/plugins/furnace.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
const assert = require('assert')
|
||||
|
||||
module.exports = inject
|
||||
|
||||
function inject (bot) {
|
||||
const allowedWindowTypes = ['minecraft:furnace', 'minecraft:blast_furnace', 'minecraft:smoker']
|
||||
|
||||
function matchWindowType (window) {
|
||||
for (const type of allowedWindowTypes) {
|
||||
if (window.type.startsWith(type)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function openFurnace (furnaceBlock) {
|
||||
const furnace = await bot.openBlock(furnaceBlock)
|
||||
if (!matchWindowType(furnace)) {
|
||||
throw new Error('This is not a furnace-like window')
|
||||
}
|
||||
|
||||
furnace.totalFuel = null
|
||||
furnace.fuel = null
|
||||
furnace.fuelSeconds = null
|
||||
furnace.totalProgress = null
|
||||
furnace.progress = null
|
||||
furnace.progressSeconds = null
|
||||
furnace.takeInput = takeInput
|
||||
furnace.takeFuel = takeFuel
|
||||
furnace.takeOutput = takeOutput
|
||||
furnace.putInput = putInput
|
||||
furnace.putFuel = putFuel
|
||||
furnace.inputItem = function () { return this.slots[0] }
|
||||
furnace.fuelItem = function () { return this.slots[1] }
|
||||
furnace.outputItem = function () { return this.slots[2] }
|
||||
|
||||
bot._client.on('craft_progress_bar', onUpdateWindowProperty)
|
||||
furnace.once('close', () => {
|
||||
bot._client.removeListener('craft_progress_bar', onUpdateWindowProperty)
|
||||
})
|
||||
|
||||
return furnace
|
||||
|
||||
function onUpdateWindowProperty (packet) {
|
||||
if (packet.windowId !== furnace.id) return
|
||||
|
||||
switch (packet.property) {
|
||||
case 0: // Current fuel
|
||||
furnace.fuel = 0
|
||||
furnace.fuelSeconds = 0
|
||||
if (furnace.totalFuel) {
|
||||
furnace.fuel = packet.value / furnace.totalFuel
|
||||
furnace.fuelSeconds = furnace.fuel * furnace.totalFuelSeconds
|
||||
}
|
||||
break
|
||||
case 1: // Total fuel
|
||||
furnace.totalFuel = packet.value
|
||||
furnace.totalFuelSeconds = ticksToSeconds(furnace.totalFuel)
|
||||
break
|
||||
case 2: // Current progress
|
||||
furnace.progress = 0
|
||||
furnace.progressSeconds = 0
|
||||
if (furnace.totalProgress) {
|
||||
furnace.progress = packet.value / furnace.totalProgress
|
||||
furnace.progressSeconds = furnace.totalProgressSeconds - (furnace.progress * furnace.totalProgressSeconds)
|
||||
}
|
||||
break
|
||||
case 3: // Total progress
|
||||
furnace.totalProgress = packet.value
|
||||
furnace.totalProgressSeconds = ticksToSeconds(furnace.totalProgress)
|
||||
}
|
||||
|
||||
furnace.emit('update')
|
||||
}
|
||||
|
||||
async function takeSomething (item) {
|
||||
assert.ok(item)
|
||||
await bot.putAway(item.slot)
|
||||
return item
|
||||
}
|
||||
|
||||
async function takeInput () {
|
||||
return takeSomething(furnace.inputItem())
|
||||
}
|
||||
|
||||
async function takeFuel () {
|
||||
return takeSomething(furnace.fuelItem())
|
||||
}
|
||||
|
||||
async function takeOutput () {
|
||||
return takeSomething(furnace.outputItem())
|
||||
}
|
||||
|
||||
async function putSomething (destSlot, itemType, metadata, count) {
|
||||
const options = {
|
||||
window: furnace,
|
||||
itemType,
|
||||
metadata,
|
||||
count,
|
||||
sourceStart: furnace.inventoryStart,
|
||||
sourceEnd: furnace.inventoryEnd,
|
||||
destStart: destSlot,
|
||||
destEnd: destSlot + 1
|
||||
}
|
||||
await bot.transfer(options)
|
||||
}
|
||||
|
||||
async function putInput (itemType, metadata, count) {
|
||||
await putSomething(0, itemType, metadata, count)
|
||||
}
|
||||
|
||||
async function putFuel (itemType, metadata, count) {
|
||||
await putSomething(1, itemType, metadata, count)
|
||||
}
|
||||
}
|
||||
|
||||
function ticksToSeconds (ticks) {
|
||||
return ticks * 0.05
|
||||
}
|
||||
|
||||
bot.openFurnace = openFurnace
|
||||
}
|
||||
142
bridge/lib/mineflayer/lib/plugins/game.js
Normal file
142
bridge/lib/mineflayer/lib/plugins/game.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
const nbt = require('prismarine-nbt')
|
||||
module.exports = inject
|
||||
|
||||
const difficultyNames = ['peaceful', 'easy', 'normal', 'hard']
|
||||
const gameModes = ['survival', 'creative', 'adventure', 'spectator']
|
||||
|
||||
const dimensionNames = {
|
||||
'-1': 'the_nether',
|
||||
0: 'overworld',
|
||||
1: 'the_end'
|
||||
}
|
||||
|
||||
const parseGameMode = gameModeBits => {
|
||||
if (gameModeBits < 0 || gameModeBits > 0b11) {
|
||||
return 'survival'
|
||||
}
|
||||
return gameModes[(gameModeBits & 0b11)] // lower two bits
|
||||
}
|
||||
|
||||
function inject (bot, options) {
|
||||
function getBrandCustomChannelName () {
|
||||
if (bot.supportFeature('customChannelMCPrefixed')) {
|
||||
return 'MC|Brand'
|
||||
} else if (bot.supportFeature('customChannelIdentifier')) {
|
||||
return 'minecraft:brand'
|
||||
}
|
||||
throw new Error('Unsupported brand channel name')
|
||||
}
|
||||
|
||||
function handleRespawnPacketData (packet) {
|
||||
bot.game.levelType = packet.levelType ?? (packet.isFlat ? 'flat' : 'default')
|
||||
bot.game.hardcore = packet.isHardcore ?? Boolean(packet.gameMode & 0b100)
|
||||
// Either a respawn packet or a login packet. Depending on the packet it can be "gamemode" or "gameMode"
|
||||
if (bot.supportFeature('spawnRespawnWorldDataField')) { // 1.20.5
|
||||
bot.game.gameMode = packet.gamemode
|
||||
} else {
|
||||
bot.game.gameMode = parseGameMode(packet.gamemode ?? packet.gameMode)
|
||||
}
|
||||
if (bot.supportFeature('segmentedRegistryCodecData')) { // 1.20.5
|
||||
if (typeof packet.dimension === 'number') {
|
||||
bot.game.dimension = bot.registry.dimensionsArray[packet.dimension]?.name?.replace('minecraft:', '')
|
||||
} else if (typeof packet.dimension === 'string') { // iirc, in 1.21 it's back to a string
|
||||
bot.game.dimension = packet.dimension.replace('minecraft:', '')
|
||||
}
|
||||
} else if (bot.supportFeature('dimensionIsAnInt')) {
|
||||
bot.game.dimension = dimensionNames[packet.dimension]
|
||||
} else if (bot.supportFeature('dimensionIsAString')) {
|
||||
bot.game.dimension = packet.dimension.replace('minecraft:', '')
|
||||
} else if (bot.supportFeature('dimensionIsAWorld')) {
|
||||
bot.game.dimension = packet.worldName.replace('minecraft:', '')
|
||||
} else {
|
||||
throw new Error('Unsupported dimension type in login packet')
|
||||
}
|
||||
|
||||
if (packet.dimensionCodec) {
|
||||
bot.registry.loadDimensionCodec(packet.dimensionCodec)
|
||||
}
|
||||
|
||||
bot.game.minY = 0
|
||||
bot.game.height = 256
|
||||
|
||||
if (bot.supportFeature('dimensionDataInCodec')) { // 1.19+
|
||||
// pre 1.20.5 before we consolidated login and respawn's SpawnInfo structure into one type,
|
||||
// "dimension" was called "worldType" in login_packet's payload but not respawn.
|
||||
if (packet.worldType && !bot.game.dimension) {
|
||||
bot.game.dimension = packet.worldType.replace('minecraft:', '')
|
||||
}
|
||||
const dimData = bot.registry.dimensionsByName[bot.game.dimension]
|
||||
if (dimData) {
|
||||
bot.game.minY = dimData.minY
|
||||
bot.game.height = dimData.height
|
||||
}
|
||||
} else if (bot.supportFeature('dimensionDataIsAvailable')) { // 1.16.2+
|
||||
const dimensionData = nbt.simplify(packet.dimension)
|
||||
bot.game.minY = dimensionData.min_y
|
||||
bot.game.height = dimensionData.height
|
||||
}
|
||||
|
||||
if (packet.difficulty) {
|
||||
bot.game.difficulty = difficultyNames[packet.difficulty]
|
||||
}
|
||||
}
|
||||
|
||||
bot.game = {}
|
||||
|
||||
const brandChannel = getBrandCustomChannelName()
|
||||
bot._client.registerChannel(brandChannel, ['string', []])
|
||||
|
||||
// 1.20.2
|
||||
bot._client.on('registry_data', (packet) => {
|
||||
bot.registry.loadDimensionCodec(packet.codec || packet)
|
||||
})
|
||||
|
||||
bot._client.on('login', (packet) => {
|
||||
handleRespawnPacketData(packet.worldState || packet)
|
||||
|
||||
bot.game.maxPlayers = packet.maxPlayers
|
||||
if (packet.enableRespawnScreen) {
|
||||
bot.game.enableRespawnScreen = packet.enableRespawnScreen
|
||||
}
|
||||
if (packet.viewDistance) {
|
||||
bot.game.serverViewDistance = packet.viewDistance
|
||||
}
|
||||
|
||||
bot.emit('login')
|
||||
bot.emit('game')
|
||||
|
||||
// varint length-prefixed string as data
|
||||
bot._client.writeChannel(brandChannel, options.brand)
|
||||
})
|
||||
|
||||
bot._client.on('respawn', (packet) => {
|
||||
// in 1.20.5+ protocol we move the shared spawn data into one SpawnInfo type under .worldState
|
||||
handleRespawnPacketData(packet.worldState || packet)
|
||||
bot.emit('game')
|
||||
})
|
||||
|
||||
bot._client.on('game_state_change', (packet) => {
|
||||
if ((packet.reason === 4 || packet.reason === 'win_game') && packet.gameMode === 1) {
|
||||
bot._client.write('client_command', { action: 0 })
|
||||
}
|
||||
if ((packet.reason === 3) || (packet.reason === 'change_game_mode')) {
|
||||
bot.game.gameMode = parseGameMode(packet.gameMode)
|
||||
bot.emit('game')
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('difficulty', (packet) => {
|
||||
bot.game.difficulty = difficultyNames[packet.difficulty]
|
||||
})
|
||||
|
||||
bot._client.on(brandChannel, (serverBrand) => {
|
||||
bot.game.serverBrand = serverBrand
|
||||
})
|
||||
|
||||
// mimic the vanilla 1.17 client to prevent anticheat kicks
|
||||
bot._client.on('ping', (data) => {
|
||||
bot._client.write('pong', {
|
||||
id: data.id
|
||||
})
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue