diff --git a/mosfet.py b/mosfet.py
index 952a770..8b25e10 100644
--- a/mosfet.py
+++ b/mosfet.py
@@ -8,6 +8,7 @@ import threading
import time
import traceback
import sys
+from math import floor
from pathlib import Path
import logging
@@ -158,6 +159,84 @@ def api_global():
return response
+def _minimap_surface_cell(chunks, x, z):
+ """Return the uppermost solid/water block in a loaded world column."""
+ from mosfet.info import blocks
+
+ chunk_x, chunk_z = floor(x / 16), floor(z / 16)
+ chunk = chunks.chunks.get((chunk_x, chunk_z))
+ if chunk is None:
+ return None
+
+ local_x, local_z = x - chunk_x * 16, z - chunk_z * 16
+ min_y = getattr(chunks, 'min_y', -64)
+ for section_y in sorted(chunk.sections, reverse=True):
+ if section_y * 16 + 15 < min_y:
+ continue
+ section = chunk.sections[section_y]
+ if section.get('non_empty_blocks') == 0:
+ continue
+ for y in range(section_y * 16 + 15, max(min_y, section_y * 16) - 1, -1):
+ state_id = chunk.get_block_at(local_x, y, local_z)
+ if state_id is None or state_id == blocks.AIR:
+ continue
+ if (state_id in blocks.NON_SOLID_IDS and
+ state_id not in blocks.WATER_IDS):
+ continue
+ return {'block': blocks.BLOCKS.get(state_id, 'unknown'), 'y': y}
+ return None
+
+
+@app.route('/api/minimap')
+def api_minimap():
+ position = g.get('pos') or g.get('player_position')
+ chunks = g.get('chunks')
+ if (not g.get('connected') or position is None or position is False or
+ chunks is None or not hasattr(chunks, 'chunks')):
+ return jsonify(ready=False, cells=[])
+
+ try:
+ radius = int(request.args.get('radius', 12))
+ except (TypeError, ValueError):
+ radius = 12
+ radius = max(4, min(32, radius))
+ try:
+ radius_x = int(request.args.get('radius_x', radius))
+ except (TypeError, ValueError):
+ radius_x = radius
+ try:
+ radius_z = int(request.args.get('radius_z', radius))
+ except (TypeError, ValueError):
+ radius_z = radius
+ radius_x = max(4, min(48, radius_x))
+ radius_z = max(4, min(32, radius_z))
+ center_x, center_z = floor(position.x), floor(position.z)
+ center_y = floor(position.y)
+ lock = getattr(chunks, 'lock', None)
+ if lock is not None:
+ lock.acquire()
+ try:
+ cells = [
+ [_minimap_surface_cell(chunks, center_x + dx, center_z + dz)
+ for dx in range(-radius_x, radius_x + 1)]
+ for dz in range(-radius_z, radius_z + 1)
+ ]
+ finally:
+ if lock is not None:
+ lock.release()
+
+ ready = any(cell is not None for row in cells for cell in row)
+ return jsonify(
+ ready=ready,
+ center={'x': center_x, 'y': center_y, 'z': center_z},
+ yaw=float(g.get('yaw', 0.0) or 0.0),
+ radius=radius,
+ radius_x=radius_x,
+ radius_z=radius_z,
+ cells=cells if ready else [],
+ )
+
+
@app.route('/api/items')
def api_items():
from mosfet.info.items import ITEM_NAMES
diff --git a/tests/test_web_inventory_api.py b/tests/test_web_inventory_api.py
index 6d420b3..793360e 100644
--- a/tests/test_web_inventory_api.py
+++ b/tests/test_web_inventory_api.py
@@ -2,7 +2,9 @@ import importlib.util
from pathlib import Path
import threading
import unittest
+from munch import Munch
+from mosfet.info import blocks
from mosfet.protocol.types import Slot
@@ -32,7 +34,8 @@ class TestWebInventoryAPI(unittest.TestCase):
self.g = mosfet_web.g
self.original = {
key: self.g.get(key) for key in
- ('connected', 'game', 'inv', 'chest_viewers', 'time', 'time_base')
+ ('connected', 'game', 'inv', 'chest_viewers', 'time', 'time_base',
+ 'chunks', 'pos', 'yaw')
}
self.game = FakeGame()
self.g.connected = True
@@ -54,6 +57,42 @@ class TestWebInventoryAPI(unittest.TestCase):
item = response.get_json()['inv']['9']
self.assertEqual(item['components']['custom_name']['text'], 'Test Paper')
+ def test_minimap_api_returns_surface_blocks_centered_on_bot(self):
+ grass = blocks.MCD_BLOCKS['grass_block']['minStateId']
+
+ class FakeChunk:
+ sections = {
+ 0: {'non_empty_blocks': 1},
+ 1: {'non_empty_blocks': 0},
+ }
+
+ @staticmethod
+ def get_block_at(x, y, z):
+ return grass if (x, y, z) == (1, 5, 1) else blocks.AIR
+
+ class FakeChunks:
+ min_y = 0
+ chunks = {(0, 0): FakeChunk()}
+
+ self.g.connected = True
+ self.g.chunks = FakeChunks()
+ self.g.pos = Munch(x=1.5, y=6.0, z=1.5)
+ self.g.yaw = 90
+
+ response = self.client.get(
+ '/api/minimap?radius=4&radius_x=6&radius_z=4')
+
+ self.assertEqual(response.status_code, 200)
+ data = response.get_json()
+ self.assertTrue(data['ready'])
+ self.assertEqual(data['center'], {'x': 1, 'y': 6, 'z': 1})
+ self.assertEqual(data['yaw'], 90)
+ self.assertEqual(data['radius_x'], 6)
+ self.assertEqual(data['radius_z'], 4)
+ self.assertEqual(len(data['cells']), 9)
+ self.assertEqual(len(data['cells'][0]), 13)
+ self.assertEqual(data['cells'][4][6], {'block': 'grass_block', 'y': 5})
+
def test_global_api_exposes_world_time_ticks(self):
self.g.time = 12345.5
self.g.time_base = 12345.5
diff --git a/web_interface/build/index.html b/web_interface/build/index.html
index c3e9ef4..839586b 100644
--- a/web_interface/build/index.html
+++ b/web_interface/build/index.html
@@ -6,8 +6,8 @@
Mosfet · Minecraft Bot
-
-
+
+
@@ -72,6 +72,20 @@