Find chest openings within interaction range

This commit is contained in:
Tanner
2026-11-07 19:48:51 +00:00
parent 688297e968
commit 9783631f94
6 changed files with 337 additions and 27 deletions
+9 -7
View File
@@ -385,7 +385,7 @@ class LegacyGame:
packet.face = 1 packet.face = 1
self.g.connection.write_packet(packet) self.g.connection.write_packet(packet)
def open_container(self, location): def open_container(self, location, face=BlockFace.TOP, cursor=None):
location = tuple(int(value) for value in location) location = tuple(int(value) for value in location)
positions = {location} positions = {location}
world = self.g.get('world') world = self.g.get('world')
@@ -399,7 +399,7 @@ class LegacyGame:
'positions': tuple(sorted(positions)), 'positions': tuple(sorted(positions)),
'requested_at': time.monotonic(), 'requested_at': time.monotonic(),
} }
self.place_block(location, BlockFace.TOP) self.place_block(location, face, cursor=cursor)
def handle_window(self, packet): def handle_window(self, packet):
print(packet) print(packet)
@@ -1048,14 +1048,16 @@ class Game(LegacyGame):
return True return True
def place_block(self, location, face, hand=0, cursor_y=None, def place_block(self, location, face, hand=0, cursor_y=None,
sneaking=False): sneaking=False, cursor=None):
location = tuple(int(value) for value in location) location = tuple(int(value) for value in location)
if cursor_y is None: if cursor is None:
cursor_y = 0.99 if int(face) == 1 else 0.5 if cursor_y is None:
cursor_y = 0.99 if int(face) == 1 else 0.5
cursor = (0.5, cursor_y, 0.5)
packet = UseItemOnBlockPacket( packet = UseItemOnBlockPacket(
hand=hand, location=location, face=int(face), hand=hand, location=location, face=int(face),
cursor_x=0.5, cursor_y=cursor_y, cursor_x=float(cursor[0]), cursor_y=float(cursor[1]),
cursor_z=0.5, inside_block=False, world_border_hit=False, cursor_z=float(cursor[2]), inside_block=False, world_border_hit=False,
sequence=self._next_sequence()) sequence=self._next_sequence())
if sneaking: if sneaking:
self.g.connection.write_packet(PlayerInputPacket(flags=0x20), self.g.connection.write_packet(PlayerInputPacket(flags=0x20),
+24 -16
View File
@@ -69,12 +69,13 @@ class CacheItemsStates:
self.area = container['position'] self.area = container['position']
self.container_item_ids = container['item_ids'] self.container_item_ids = container['item_ids']
self.is_labeled_container = True self.is_labeled_container = True
for opening in w.find_cache_openings(self.area): for opening in w.find_cache_openings(self.area, p):
navpath = w.path_to_place(p, opening) navpath = w.path_to_place(p, opening['position'])
if navpath: if navpath:
print('Going to labeled chest', container['labels'], print('Going to labeled chest', container['labels'],
self.area) self.area)
self.opening = opening self.opening = opening['position']
self.open_interaction = opening
self.g.path = navpath self.g.path = navpath
self.state = self.going_to_trapped_chest self.state = self.going_to_trapped_chest
return return
@@ -96,11 +97,12 @@ class CacheItemsStates:
chest = self.trapped_chests[0] chest = self.trapped_chests[0]
self.area = chest self.area = chest
for opening in w.find_cache_openings(chest): for opening in w.find_cache_openings(chest, p):
navpath = w.path_to_place(p, opening) navpath = w.path_to_place(p, opening['position'])
if navpath: if navpath:
print('navpath to trapped chest:', navpath) print('navpath to trapped chest:', navpath)
self.opening = opening self.opening = opening['position']
self.open_interaction = opening
self.g.path = navpath self.g.path = navpath
self.state = self.going_to_trapped_chest self.state = self.going_to_trapped_chest
return return
@@ -110,12 +112,12 @@ class CacheItemsStates:
def going_to_trapped_chest(self): def going_to_trapped_chest(self):
if utils.pint(self.g.pos) == self.opening: if utils.pint(self.g.pos) == self.opening:
self.g.look_at = self.area self.g.look_at = self.open_interaction['look_at']
self.wait_time = 0.5 self.wait_time = 0.5
self.state = self.turn_to_trapped_chest self.state = self.turn_to_trapped_chest
def turn_to_trapped_chest(self): def turn_to_trapped_chest(self):
self.g.look_at = self.area self.g.look_at = self.open_interaction['look_at']
self.wait_time -= utils.TICK self.wait_time -= utils.TICK
if self.wait_time <= 0: if self.wait_time <= 0:
self.state = self.open_chest self.state = self.open_chest
@@ -144,12 +146,14 @@ class CacheItemsStates:
return return
self.area = area self.area = area
openings = w.find_cache_openings(self.area) openings = w.find_cache_openings(self.area, p)
for o in openings: for opening in openings:
navpath = w.path_to_place(p, o) navpath = w.path_to_place(p, opening['position'])
self.opening = o if navpath:
if navpath: break self.opening = opening['position']
self.open_interaction = opening
break
else: # for else: # for
print('Unable to get to cache area', self.area) print('Unable to get to cache area', self.area)
self.bad_areas.append(self.area) self.bad_areas.append(self.area)
@@ -161,12 +165,12 @@ class CacheItemsStates:
def going_to_area(self): def going_to_area(self):
if utils.pint(self.g.pos) == self.opening: if utils.pint(self.g.pos) == self.opening:
self.g.look_at = self.area self.g.look_at = self.open_interaction['look_at']
self.wait_time = 0.5 self.wait_time = 0.5
self.state = self.turn_to_cache_spot self.state = self.turn_to_cache_spot
def turn_to_cache_spot(self): def turn_to_cache_spot(self):
self.g.look_at = self.area self.g.look_at = self.open_interaction['look_at']
self.wait_time -= utils.TICK self.wait_time -= utils.TICK
if self.wait_time <= 0: if self.wait_time <= 0:
self.state = self.place_chest self.state = self.place_chest
@@ -180,7 +184,10 @@ class CacheItemsStates:
print('Opening chest') print('Opening chest')
self.failed_transfer_slots.clear() self.failed_transfer_slots.clear()
self.pending_transfer = None self.pending_transfer = None
self.g.game.open_container(self.area) self.g.game.open_container(
self.open_interaction['target'],
face=self.open_interaction['face'],
cursor=self.open_interaction['cursor'])
self.wait_time = 1 self.wait_time = 1
self.state = self.wait self.state = self.wait
@@ -368,6 +375,7 @@ class CacheItemsStates:
self.container_item_ids = None self.container_item_ids = None
self.area = None self.area = None
self.opening = None self.opening = None
self.open_interaction = None
self.trapped_chests = [] self.trapped_chests = []
self.bad_areas = [] self.bad_areas = []
self.wait_time = 0 self.wait_time = 0
+190 -2
View File
@@ -3,6 +3,7 @@ import json
import re import re
import time import time
import random import random
from math import floor
from itertools import count from itertools import count
from copy import copy from copy import copy
@@ -11,6 +12,10 @@ from mosfet import path
from mosfet.info import blocks from mosfet.info import blocks
from mosfet.info import items from mosfet.info import items
from mosfet.info import mobs from mosfet.info import mobs
from minecraft.networking.types import BlockFace
CHEST_OPENING_RADIUS = 3
class World: class World:
def __init__(self, global_state): def __init__(self, global_state):
@@ -625,8 +630,191 @@ class World:
self._bed_occupancy_states[bed] = occupied self._bed_occupancy_states[bed] = occupied
return occupied return occupied
def find_cache_openings(self, area): @staticmethod
return self.find_bed_openings(area) def _is_chest_state(state_id):
name = blocks.BLOCKS.get(state_id)
return (name is not None and name != 'ender_chest' and
(name == 'chest' or name.endswith('_chest')))
def _cache_opening_standable(self, position):
at_feet = self.block_at(*position)
above = self.block_at(*utils.padd(position, path.BLOCK_ABOVE))
below = self.block_at(*utils.padd(position, path.BLOCK_BELOW))
return (at_feet in blocks.NON_SOLID_IDS and
at_feet not in blocks.AVOID_IDS and
above in blocks.NON_SOLID_IDS and
above not in blocks.AVOID_IDS and
below is not None and
below not in blocks.NON_SOLID_IDS and
below not in blocks.AVOID_IDS)
@staticmethod
def _segment_intersects_aabb(start, end, minimum, maximum):
lower, upper = 0.0, 1.0
for axis in range(3):
origin = start[axis]
delta = end[axis] - origin
if abs(delta) < 1e-9:
if origin < minimum[axis] or origin > maximum[axis]:
return False
continue
first = (minimum[axis] - origin) / delta
second = (maximum[axis] - origin) / delta
if first > second:
first, second = second, first
lower = max(lower, first)
upper = min(upper, second)
if lower > upper:
return False
return upper >= 0.0 and lower <= 1.0
def _has_cache_line_of_sight(self, start, end, target):
first = tuple(floor(min(start[i], end[i])) for i in range(3))
last = tuple(floor(max(start[i], end[i])) for i in range(3))
for x in range(first[0], last[0] + 1):
for y in range(first[1], last[1] + 1):
for z in range(first[2], last[2] + 1):
position = (x, y, z)
if position == target:
continue
if not self._segment_intersects_aabb(
start, end, position, utils.padd(position, (1, 1, 1))):
continue
state_id = self.block_at(*position)
if state_id is None:
return False
for box in blocks.COLLISION_BOXES.get(state_id, ()):
minimum = (x + box[0], y + box[1], z + box[2])
maximum = (x + box[3], y + box[4], z + box[5])
if self._segment_intersects_aabb(
start, end, minimum, maximum):
return False
return True
@staticmethod
def _chest_interaction_points(target, boxes):
points = []
for box in boxes:
x0, y0, z0, x1, y1, z1 = box
xm, zm = (x0 + x1) / 2, (z0 + z1) / 2
for fraction in (0.2, 0.5, 0.8):
y = y0 + (y1 - y0) * fraction
points.extend((
(target[0] + xm, target[1] + y, target[2] + z0),
(target[0] + xm, target[1] + y, target[2] + z1),
(target[0] + x0, target[1] + y, target[2] + zm),
(target[0] + x1, target[1] + y, target[2] + zm),
))
for x_fraction, z_fraction in (
(0.25, 0.25), (0.25, 0.75),
(0.75, 0.25), (0.75, 0.75), (0.5, 0.5)):
points.append((
target[0] + x0 + (x1 - x0) * x_fraction,
target[1] + y1,
target[2] + z0 + (z1 - z0) * z_fraction,
))
return points
@staticmethod
def _ray_chest_surface_hit(start, end, target, boxes):
negative_faces = (BlockFace.WEST, BlockFace.BOTTOM, BlockFace.NORTH)
positive_faces = (BlockFace.EAST, BlockFace.TOP, BlockFace.SOUTH)
delta = tuple(end[i] - start[i] for i in range(3))
hits = []
for box in boxes:
minimum = tuple(target[i] + box[i] for i in range(3))
maximum = tuple(target[i] + box[i + 3] for i in range(3))
enter, leave = float('-inf'), float('inf')
entering_face = None
for axis in range(3):
if abs(delta[axis]) < 1e-9:
if (start[axis] < minimum[axis] or
start[axis] > maximum[axis]):
break
continue
near = (minimum[axis] - start[axis]) / delta[axis]
far = (maximum[axis] - start[axis]) / delta[axis]
face = negative_faces[axis]
if near > far:
near, far = far, near
face = positive_faces[axis]
if near > enter:
enter = near
entering_face = face
leave = min(leave, far)
if enter > leave:
break
else:
if entering_face is None or enter < 0 or enter > 1:
continue
hit_point = tuple(start[i] + delta[i] * enter
for i in range(3))
cursor = tuple(hit_point[i] - target[i] for i in range(3))
hits.append((enter, entering_face, cursor, hit_point))
if not hits:
return None
_distance, face, cursor, hit_point = min(hits, key=lambda hit: hit[0])
return face, cursor, hit_point
def find_cache_openings(self, area, center=None, radius=None):
"""Find standable nearby spots with a clear ray to a chest surface."""
area = tuple(area)
center = tuple(center if center is not None else area)
radius = CHEST_OPENING_RADIUS if radius is None else radius
state_id = self.block_at(*area)
targets = (self._double_chest_positions(area)
if self._is_chest_state(state_id) else (area,))
default_chest = min(blocks.CHEST_IDS)
interactions = {}
for target in targets:
target_state = self.block_at(*target)
boxes = blocks.COLLISION_BOXES.get(target_state) or \
blocks.COLLISION_BOXES[default_chest]
hit_points = self._chest_interaction_points(target, boxes)
for horizontal in utils.search_2d(radius + 1):
for dy in range(-radius - 1, radius + 2):
offset = (horizontal[0], dy, horizontal[2])
opening = utils.padd(target, offset)
if (opening in targets or
not self._cache_opening_standable(opening)):
continue
eye = (opening[0] + 0.5, opening[1] + 1.62,
opening[2] + 0.5)
for aim_point in hit_points:
if not self._has_cache_line_of_sight(
eye, aim_point, target):
continue
hit = self._ray_chest_surface_hit(
eye, aim_point, target, boxes)
if hit is None:
continue
face, cursor, hit_point = hit
hit_distance = utils.phyp(eye, hit_point)
if hit_distance > radius:
continue
interaction = {
'position': opening,
'target': target,
'face': face,
'cursor': cursor,
'hit_point': hit_point,
'look_at': tuple(value - 0.5
for value in hit_point),
}
distance = utils.phyp(center, opening)
interaction['distance'] = distance
interaction['hit_distance'] = hit_distance
previous = interactions.get(opening)
if (previous is None or
interaction['hit_distance'] <
previous['hit_distance']):
interactions[opening] = interaction
break
return sorted(interactions.values(),
key=lambda result: (result['distance'],
result['position']))
def find_objects(self, object_ids): def find_objects(self, object_ids):
result = [] result = []
+88
View File
@@ -0,0 +1,88 @@
import unittest
from munch import Munch
from minecraft.networking.types import BlockFace
from mosfet.info import blocks
from mosfet.world import CHEST_OPENING_RADIUS, World
class FakeChunks:
def __init__(self, states):
self.states = states
def get_block_at(self, x, y, z):
return self.states.get((x, y, z), blocks.AIR)
class CacheOpeningTests(unittest.TestCase):
def setUp(self):
self.chest = (0, 64, 0)
chest_state = min(blocks.CHEST_IDS)
stone = blocks.STONE
states = {self.chest: chest_state}
for x in range(-4, 5):
for z in range(-4, 5):
states[(x, 63, z)] = stone
self.world = World(Munch(chunks=FakeChunks(states)))
def test_openings_include_visible_positions_up_to_configured_radius(self):
opening = (3, 64, 0)
openings = self.world.find_cache_openings(
self.chest, center=(3, 64, 0))
by_position = {tuple(candidate['position']): candidate
for candidate in openings}
self.assertEqual(CHEST_OPENING_RADIUS, 3)
self.assertIn(opening, by_position)
self.assertTrue(all(candidate['hit_distance'] <= CHEST_OPENING_RADIUS
for candidate in openings))
self.assertIn(by_position[opening]['face'], (
BlockFace.NORTH, BlockFace.SOUTH, BlockFace.WEST,
BlockFace.EAST, BlockFace.TOP,
))
self.assertEqual(len(by_position[opening]['cursor']), 3)
def test_range_is_measured_from_eye_to_interaction_surface(self):
opening = (3, 63, 0)
self.world.g.chunks.states[opening] = blocks.AIR
self.world.g.chunks.states[(3, 62, 0)] = blocks.STONE
openings = self.world.find_cache_openings(
self.chest, center=opening)
by_position = {tuple(candidate['position']): candidate
for candidate in openings}
self.assertIn(opening, by_position)
self.assertLessEqual(by_position[opening]['hit_distance'], 3)
def test_sign_collision_shape_does_not_block_sight_but_solid_block_does(self):
start = (3.5, 65.62, 0.5)
target = (0.5, 64.875, 0.5)
sign_state = blocks.MCD_BLOCKS['oak_sign']['minStateId']
obstruction = (1, 65, 0)
self.world.g.chunks.states[obstruction] = sign_state
self.assertTrue(self.world._has_cache_line_of_sight(
start, target, self.chest))
self.world.g.chunks.states[obstruction] = blocks.STONE
self.assertFalse(self.world._has_cache_line_of_sight(
start, target, self.chest))
def test_virtual_chest_openings_are_found_before_placement(self):
target = (0, 65, 0)
for x in range(-3, 4):
for z in range(-3, 4):
self.world.g.chunks.states[(x, 64, z)] = blocks.STONE
openings = self.world.find_cache_openings(
target, center=(3, 65, 0))
self.assertTrue(openings)
self.assertTrue(all(candidate['target'] == target
for candidate in openings))
if __name__ == '__main__':
unittest.main()
+2 -2
View File
@@ -93,8 +93,8 @@ class ChestDetectionTests(unittest.TestCase):
chunks=Munch(get_block_at=lambda *_position: None), chunks=Munch(get_block_at=lambda *_position: None),
world=Munch(_double_chest_positions=lambda _position: world=Munch(_double_chest_positions=lambda _position:
(self.position,))) (self.position,)))
game.place_block = lambda location, face: sent.append( game.place_block = lambda location, face, **kwargs: sent.append(
(location, face)) (location, face, kwargs))
game.open_container(self.position) game.open_container(self.position)
+24
View File
@@ -2,7 +2,9 @@ import unittest
from munch import Munch from munch import Munch
from minecraft.networking.modern import ClientTickEnd, UseItemOnBlockPacket from minecraft.networking.modern import ClientTickEnd, UseItemOnBlockPacket
from minecraft.networking.types import BlockFace
from mosfet.game import Game from mosfet.game import Game
from mosfet.info import blocks
from mosfet.protocol.modern import PlayerInputPacket from mosfet.protocol.modern import PlayerInputPacket
@@ -15,6 +17,28 @@ class FakeConnection:
class SneakPlacementTests(unittest.TestCase): class SneakPlacementTests(unittest.TestCase):
def test_open_container_sends_visible_face_and_hit_cursor(self):
chest = (2, 65, -4)
connection = FakeConnection()
game = Game.__new__(Game)
game.g = Munch(
connection=connection,
chunks=Munch(get_block_at=lambda *position:
min(blocks.CHEST_IDS) if tuple(position) == chest
else None),
world=Munch())
game.sequence = 0
game.open_container(
chest, face=BlockFace.EAST, cursor=(0.9375, 0.5, 0.5))
packet = next(packet for packet, _force in connection.packets
if isinstance(packet, UseItemOnBlockPacket))
self.assertEqual(packet.location, chest)
self.assertEqual(packet.face, BlockFace.EAST)
self.assertEqual((packet.cursor_x, packet.cursor_y, packet.cursor_z),
(0.9375, 0.5, 0.5))
def test_sneaking_wraps_block_placement_and_is_released(self): def test_sneaking_wraps_block_placement_and_is_released(self):
connection = FakeConnection() connection = FakeConnection()
game = Game.__new__(Game) game = Game.__new__(Game)