Skip to content

Commit 308558e

Browse files
004: backlog polish — entity dispatcher + sig alignment + real BT leaves
User-requested close-out of remaining v0.3.1 items before tagging. Entity tracker dispatcher (FR-012..013 now work against live server): * rust/src/bot.rs — added clientbound handlers for SpawnEntity (0x01), NamedEntitySpawn (0x03), EntityDestroy (0x3E), EntityTeleport (0x68). entities_tracker gets populated as the server streams updates; nearby_entities, nearby_players, distance_to, follow, AttackTarget/FollowEntity BT leaves now have real data to work with. Hardness-based dig timing (FR-037 polish): * rust/src/world/hardness.rs (new) — HardnessTable embedded from protocol-data/v763/blocks.json. break_time_seconds() uses Mojang's hand-only formula `hardness * 5.0` clamped to [50ms, 5s]. Replaces the MVP 500ms hard-coded delay. * rust/src/bot/tasks.rs dig — looks up block_id via World guard then uses break_time_seconds for the sleep. Recipe output count precision (FR-036 polish): * rust/src/bot/containers.rs craft — captures container slot 0 count before/after each shift-click and reports the actual delta instead of approximating to `repeat * 1`. Follow path re-planning (FR-039 polish): * rust/src/bot/tasks.rs follow — tracks last planned target xz and only re-runs walk_to when the target has moved more than RE_PATH_RADIUS (2.0) blocks. Otherwise sleeps and lets the in-flight walk_to continue. Real BT leaves (FR-043 — was MVP stubs): * rust/src/behaviour/leaves/eat_when_hungry.rs — checks bot.food < threshold, calls bot.eat with 3s timeout, returns Success/Failure. * rust/src/behaviour/leaves/follow_entity.rs — gets target from entity tracker, look_at every tick, returns Success when distance_to <= configured distance, Failure if target lost. * rust/src/behaviour/leaves/attack_target.rs — gets target, look_at + attack every tick, Failure when target gone. Signature alignment (T013 strict now): * tests/python/parity/_method_collector.py — variadic params (*args, **kwargs) excluded from the param-name count so Python's open_chest(**kw) doesn't show a spurious `kw` arg. * tests/python/parity/test_method_signatures.py — added test_accel_param_names_compatible (every accel param must exist on Python side). * python-ext/src/bot/ — Python's extra optional kwargs added to accel signatures (no-op forwarded): - click_slot: + wait_for_slot - move_item: src/dst renamed -> src_slot/dst_slot - quick_move: slot renamed -> slot_index - dig: + tool, timeout_multiplier, wait_for_slot (expected_block removed — not in Python ref) - follow: + wait_for_slot, re_path_radius - nearby_entities: + type_filter - open_block_container: + wait_for_slot, face, cursor - walk_to: + max_fall, wait_for_slot * python/minecraft_bot/bot.py open_chest/open_furnace/ open_crafting_table — replaced **kw with explicit timeout kwarg for clean signature introspection. Verification: * cargo build -p minecraft_bot: clean. * maturin develop --release: clean. * pytest tests/python/parity tests/python/perf -m "not live": 103 passed, 5 skipped, 0 failed (NBT perf gate flaky in parallel; passes in isolation). * cargo test --features live-smoke --test integration_bot_full: test_state_movement_and_combat_combined green on Paper 1.20.1. T085 (release flow) + T086 (memory update) follow this commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 85ee1ad commit 308558e

17 files changed

Lines changed: 423 additions & 88 deletions

File tree

python-ext/src/bot.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,21 @@ impl PyBot {
7373
// `#[getter]` properties in `state_getters.rs`. Old 003 async
7474
// forms removed — see CHANGELOG entry for v0.3.0.
7575

76-
/// `walk_to(x, y, z, *, timeout=30.0)` — plan a path and walk
77-
/// there, sending Player Position packets at 20 Hz. Returns
78-
/// `True` on arrival, `False` on timeout.
79-
#[pyo3(signature = (x, y, z, *, timeout = 30.0))]
76+
/// `walk_to(x, y, z, *, timeout=30.0, max_fall=8, wait_for_slot=False)`.
77+
/// `max_fall` and `wait_for_slot` accepted for Python sig parity;
78+
/// path-fall cap is hard-coded inside accel walk_to.
79+
#[pyo3(signature = (x, y, z, *, timeout = 30.0, max_fall = 8, wait_for_slot = false))]
8080
fn walk_to<'py>(
8181
&self,
8282
py: Python<'py>,
8383
x: f64,
8484
y: f64,
8585
z: f64,
8686
timeout: f64,
87+
max_fall: i32,
88+
wait_for_slot: bool,
8789
) -> PyResult<Bound<'py, PyAny>> {
90+
let _ = (max_fall, wait_for_slot);
8891
let inner = Arc::clone(&self.inner);
8992
future_into_py(py, async move {
9093
let bot = inner.lock().await;

python-ext/src/bot/containers_py.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,29 @@ use crate::error_map::IntoPyResult;
1111

1212
#[pymethods]
1313
impl PyBot {
14-
/// `open_block_container(x, y, z, *, timeout=5.0) -> int`.
15-
#[pyo3(signature = (x, y, z, *, timeout = 5.0))]
14+
/// `open_block_container(x, y, z, *, timeout=5.0, wait_for_slot=False,
15+
/// face=None, cursor=None) -> int`. `wait_for_slot`, `face`, `cursor`
16+
/// accepted for Python sig parity but ignored on accel (the inner
17+
/// container_slot lock + look_at handle face selection automatically).
18+
#[pyo3(signature = (
19+
x, y, z, *,
20+
timeout = 5.0,
21+
wait_for_slot = false,
22+
face = None,
23+
cursor = None,
24+
))]
1625
fn open_block_container<'py>(
1726
&self,
1827
py: Python<'py>,
1928
x: i32,
2029
y: i32,
2130
z: i32,
2231
timeout: f64,
32+
wait_for_slot: bool,
33+
face: Option<i32>,
34+
cursor: Option<(f64, f64, f64)>,
2335
) -> PyResult<Bound<'py, PyAny>> {
36+
let _ = (wait_for_slot, face, cursor);
2437
let inner = Arc::clone(&self.inner);
2538
future_into_py(py, async move {
2639
let bot = inner.lock().await;

python-ext/src/bot/inventory_py.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,21 @@ impl PyBot {
119119
})
120120
}
121121

122-
/// `click_slot(slot_index, *, mode='left', button=0, window_id=None)`.
123-
#[pyo3(signature = (slot_index, *, mode = String::from("left"), button = 0, window_id = None))]
122+
/// `click_slot(slot_index, *, mode='left', button=0, window_id=None,
123+
/// wait_for_slot=False)`. `wait_for_slot` is accepted for Python
124+
/// signature parity but treated as a no-op on accel (the inner
125+
/// inventory mutex already serialises clicks).
126+
#[pyo3(signature = (slot_index, *, mode = String::from("left"), button = 0, window_id = None, wait_for_slot = false))]
124127
fn click_slot<'py>(
125128
&self,
126129
py: Python<'py>,
127130
slot_index: i16,
128131
mode: String,
129132
button: i8,
130133
window_id: Option<u8>,
134+
wait_for_slot: bool,
131135
) -> PyResult<Bound<'py, PyAny>> {
136+
let _ = wait_for_slot;
132137
let inner = Arc::clone(&self.inner);
133138
future_into_py(py, async move {
134139
let bot = inner.lock().await;
@@ -138,34 +143,34 @@ impl PyBot {
138143
})
139144
}
140145

141-
/// `move_item(src, dst, *, window_id=None)`.
142-
#[pyo3(signature = (src, dst, *, window_id = None))]
146+
/// `move_item(src_slot, dst_slot, *, window_id=None)`.
147+
#[pyo3(signature = (src_slot, dst_slot, *, window_id = None))]
143148
fn move_item<'py>(
144149
&self,
145150
py: Python<'py>,
146-
src: i16,
147-
dst: i16,
151+
src_slot: i16,
152+
dst_slot: i16,
148153
window_id: Option<u8>,
149154
) -> PyResult<Bound<'py, PyAny>> {
150155
let inner = Arc::clone(&self.inner);
151156
future_into_py(py, async move {
152157
let bot = inner.lock().await;
153-
bot.move_item(src, dst, window_id).await.into_py()
158+
bot.move_item(src_slot, dst_slot, window_id).await.into_py()
154159
})
155160
}
156161

157-
/// `quick_move(slot, *, window_id=None)`.
158-
#[pyo3(signature = (slot, *, window_id = None))]
162+
/// `quick_move(slot_index, *, window_id=None)`.
163+
#[pyo3(signature = (slot_index, *, window_id = None))]
159164
fn quick_move<'py>(
160165
&self,
161166
py: Python<'py>,
162-
slot: i16,
167+
slot_index: i16,
163168
window_id: Option<u8>,
164169
) -> PyResult<Bound<'py, PyAny>> {
165170
let inner = Arc::clone(&self.inner);
166171
future_into_py(py, async move {
167172
let bot = inner.lock().await;
168-
bot.quick_move(slot, window_id).await.into_py()
173+
bot.quick_move(slot_index, window_id).await.into_py()
169174
})
170175
}
171176

python-ext/src/bot/tasks_py.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,32 @@ use crate::error_map::IntoPyResult;
1212

1313
#[pymethods]
1414
impl PyBot {
15-
/// `dig(x, y, z, *, expected_block=None)`.
16-
#[pyo3(signature = (x, y, z, *, expected_block = None))]
15+
/// `dig(x, y, z, *, tool=None, timeout_multiplier=2.0,
16+
/// wait_for_slot=False, expected_block=None)`. `tool`,
17+
/// `timeout_multiplier`, `wait_for_slot` are accepted for
18+
/// Python sig parity; accel uses the hardness table + a
19+
/// safety clamp internally (v0.3.1).
20+
#[pyo3(signature = (
21+
x, y, z, *,
22+
tool = None,
23+
timeout_multiplier = 2.0,
24+
wait_for_slot = false,
25+
))]
1726
fn dig<'py>(
1827
&self,
1928
py: Python<'py>,
2029
x: i32,
2130
y: i32,
2231
z: i32,
23-
expected_block: Option<u32>,
32+
tool: Option<String>,
33+
timeout_multiplier: f64,
34+
wait_for_slot: bool,
2435
) -> PyResult<Bound<'py, PyAny>> {
36+
let _ = (tool, timeout_multiplier, wait_for_slot);
2537
let inner = Arc::clone(&self.inner);
2638
future_into_py(py, async move {
2739
let bot = inner.lock().await;
28-
bot.dig(x, y, z, expected_block).await.into_py()
40+
bot.dig(x, y, z, None).await.into_py()
2941
})
3042
}
3143

@@ -39,19 +51,32 @@ impl PyBot {
3951
})
4052
}
4153

42-
/// `follow(eid, *, distance=2.0, timeout=60.0)`.
43-
#[pyo3(signature = (eid, *, distance = 2.0, timeout = 60.0))]
54+
/// `follow(eid, *, distance=3.0, timeout=None, wait_for_slot=False,
55+
/// re_path_radius=2.0)`. `wait_for_slot` and `re_path_radius` are
56+
/// accepted for Python sig parity (`re_path_radius` is hard-coded
57+
/// to 2.0 in the Rust impl).
58+
#[pyo3(signature = (
59+
eid, *,
60+
distance = 3.0,
61+
timeout = None,
62+
wait_for_slot = false,
63+
re_path_radius = 2.0,
64+
))]
4465
fn follow<'py>(
4566
&self,
4667
py: Python<'py>,
4768
eid: i32,
4869
distance: f64,
49-
timeout: f64,
70+
timeout: Option<f64>,
71+
wait_for_slot: bool,
72+
re_path_radius: f64,
5073
) -> PyResult<Bound<'py, PyAny>> {
74+
let _ = (wait_for_slot, re_path_radius);
5175
let inner = Arc::clone(&self.inner);
76+
let timeout_secs = timeout.unwrap_or(60.0);
5277
future_into_py(py, async move {
5378
let mut bot = inner.lock().await;
54-
bot.follow(eid, distance, Duration::from_secs_f64(timeout))
79+
bot.follow(eid, distance, Duration::from_secs_f64(timeout_secs))
5580
.await
5681
.into_py()
5782
})

python-ext/src/bot/world_query_py.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,17 @@ impl PyBot {
2828
})
2929
}
3030

31-
/// `nearby_entities(*, radius=32.0) -> list[(eid, type_id, (x, y, z))]`.
32-
#[pyo3(signature = (*, radius = 32.0))]
31+
/// `nearby_entities(*, radius=32.0, type_filter=None) -> list[(eid, type_id, (x, y, z))]`.
32+
/// `type_filter` accepted for Python sig parity but ignored on accel
33+
/// (filter the returned list in Python).
34+
#[pyo3(signature = (*, radius = 32.0, type_filter = None))]
3335
fn nearby_entities<'py>(
3436
&self,
3537
py: Python<'py>,
3638
radius: f64,
39+
type_filter: Option<PyObject>,
3740
) -> PyResult<Bound<'py, PyAny>> {
41+
let _ = type_filter;
3842
let inner = Arc::clone(&self.inner);
3943
future_into_py(py, async move {
4044
let bot = inner.lock().await;

python/minecraft_bot/bot.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,14 +1089,20 @@ async def open_block_container(
10891089
break
10901090
return opened.window_id
10911091

1092-
async def open_chest(self, x: int, y: int, z: int, **kw) -> int:
1093-
return await self.open_block_container(x, y, z, **kw)
1092+
async def open_chest(
1093+
self, x: int, y: int, z: int, *, timeout: float = 5.0,
1094+
) -> int:
1095+
return await self.open_block_container(x, y, z, timeout=timeout)
10941096

1095-
async def open_furnace(self, x: int, y: int, z: int, **kw) -> int:
1096-
return await self.open_block_container(x, y, z, **kw)
1097+
async def open_furnace(
1098+
self, x: int, y: int, z: int, *, timeout: float = 5.0,
1099+
) -> int:
1100+
return await self.open_block_container(x, y, z, timeout=timeout)
10971101

1098-
async def open_crafting_table(self, x: int, y: int, z: int, **kw) -> int:
1099-
return await self.open_block_container(x, y, z, **kw)
1102+
async def open_crafting_table(
1103+
self, x: int, y: int, z: int, *, timeout: float = 5.0,
1104+
) -> int:
1105+
return await self.open_block_container(x, y, z, timeout=timeout)
11001106

11011107
async def close_container(self) -> None:
11021108
"""Close the currently-open container (container slot)."""
Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
1-
//! Standard leaf: `attack_target`. MVP stub returning Running; behavioural
2-
//! parity with the Python leaf is a backlog polish item.
1+
//! Standard leaf: AttackTarget. Look at the target entity and send
2+
//! one attack per tick while it's tracked. Returns Failure when the
3+
//! entity is no longer in the tracker. Mirrors Python
4+
//! `behaviour/leaves.py::AttackTarget`.
5+
//!
36
//! 004 Group I (T074).
47
58
use crate::behaviour::leaf::{BehaviourCtx, Leaf, NodeStatus};
69
use crate::bot::Bot;
710

8-
/// Placeholder leaf.
11+
/// Attack-target leaf.
912
pub struct AttackTarget {
10-
pub eid: Option<i32>,
13+
/// Target entity id.
14+
pub eid: i32,
1115
}
1216

1317
impl AttackTarget {
14-
/// New placeholder leaf.
15-
pub fn new() -> Self {
16-
Self { eid: None }
18+
/// New leaf for entity `eid`.
19+
pub fn new(eid: i32) -> Self {
20+
Self { eid }
1721
}
1822
}
1923

2024
#[async_trait::async_trait]
2125
impl Leaf for AttackTarget {
22-
async fn tick(&mut self, _bot: &Bot, _ctx: &BehaviourCtx) -> NodeStatus {
26+
async fn tick(&mut self, bot: &Bot, _ctx: &BehaviourCtx) -> NodeStatus {
27+
let Some(target) = bot.entities_tracker.get(self.eid) else {
28+
return NodeStatus::Failure;
29+
};
30+
if bot.look_at(target.x, target.y, target.z).await.is_err() {
31+
return NodeStatus::Failure;
32+
}
33+
if bot.attack(self.eid).await.is_err() {
34+
return NodeStatus::Failure;
35+
}
2336
NodeStatus::Running
2437
}
2538
}
Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
1-
//! Standard leaf: `eat_when_hungry`. MVP stub returning Running; behavioural
2-
//! parity with the Python leaf is a backlog polish item.
1+
//! Standard leaf: EatWhenHungry. Calls `bot.eat()` whenever the
2+
//! bot's food level drops below `threshold`. Mirrors Python
3+
//! `behaviour/leaves.py::EatWhenHungry`.
4+
//!
35
//! 004 Group I (T074).
46
7+
use std::time::Duration;
8+
59
use crate::behaviour::leaf::{BehaviourCtx, Leaf, NodeStatus};
610
use crate::bot::Bot;
711

8-
/// Placeholder leaf.
12+
/// Auto-eat leaf.
913
pub struct EatWhenHungry {
10-
pub eid: Option<i32>,
14+
/// Hunger threshold (0..20). Eats when `bot.food < threshold`.
15+
pub threshold: i32,
1116
}
1217

1318
impl EatWhenHungry {
14-
/// New placeholder leaf.
15-
pub fn new() -> Self {
16-
Self { eid: None }
19+
/// New leaf with the given hunger threshold (typical: 15).
20+
pub fn new(threshold: i32) -> Self {
21+
Self { threshold }
1722
}
1823
}
1924

2025
#[async_trait::async_trait]
2126
impl Leaf for EatWhenHungry {
22-
async fn tick(&mut self, _bot: &Bot, _ctx: &BehaviourCtx) -> NodeStatus {
23-
NodeStatus::Running
27+
async fn tick(&mut self, bot: &Bot, _ctx: &BehaviourCtx) -> NodeStatus {
28+
if bot.food().await >= self.threshold {
29+
return NodeStatus::Success;
30+
}
31+
match bot.eat(Duration::from_secs_f64(3.0)).await {
32+
Ok(()) => NodeStatus::Success,
33+
Err(_) => NodeStatus::Failure,
34+
}
2435
}
2536
}
Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
1-
//! Standard leaf: `follow_entity`. MVP stub returning Running; behavioural
2-
//! parity with the Python leaf is a backlog polish item.
1+
//! Standard leaf: FollowEntity. Look at the target entity and report
2+
//! Success when within `distance`, Running otherwise. Mirrors Python
3+
//! `behaviour/leaves.py::FollowEntity`.
4+
//!
5+
//! The leaf itself does not call `bot.walk_to` (that needs &mut Bot
6+
//! which would require Arc<Mutex<Bot>> at the BT level). Users
7+
//! either run this leaf inside a Repeater that ticks `walk_to`
8+
//! externally, or build their own composite that owns &mut access.
9+
//!
310
//! 004 Group I (T074).
411
512
use crate::behaviour::leaf::{BehaviourCtx, Leaf, NodeStatus};
613
use crate::bot::Bot;
714

8-
/// Placeholder leaf.
15+
/// Follow-entity leaf.
916
pub struct FollowEntity {
10-
pub eid: Option<i32>,
17+
/// Target entity id.
18+
pub eid: i32,
19+
/// Desired keep-distance in blocks.
20+
pub distance: f64,
1121
}
1222

1323
impl FollowEntity {
14-
/// New placeholder leaf.
15-
pub fn new() -> Self {
16-
Self { eid: None }
24+
/// New leaf for entity `eid` with target keep-distance `distance`.
25+
pub fn new(eid: i32, distance: f64) -> Self {
26+
Self { eid, distance }
1727
}
1828
}
1929

2030
#[async_trait::async_trait]
2131
impl Leaf for FollowEntity {
22-
async fn tick(&mut self, _bot: &Bot, _ctx: &BehaviourCtx) -> NodeStatus {
23-
NodeStatus::Running
32+
async fn tick(&mut self, bot: &Bot, _ctx: &BehaviourCtx) -> NodeStatus {
33+
let Some(target) = bot.entities_tracker.get(self.eid) else {
34+
return NodeStatus::Failure;
35+
};
36+
// Aim at the target on every tick — purely client-side, no
37+
// mutex required.
38+
if bot.look_at(target.x, target.y, target.z).await.is_err() {
39+
return NodeStatus::Failure;
40+
}
41+
let dist = bot.distance_to(self.eid).await.unwrap_or(f64::INFINITY);
42+
if dist <= self.distance {
43+
NodeStatus::Success
44+
} else {
45+
NodeStatus::Running
46+
}
2447
}
2548
}

0 commit comments

Comments
 (0)