From c323ea83734c40896f6540287299c456b45936f0 Mon Sep 17 00:00:00 2001 From: Tanner Date: Thu, 5 Nov 2026 23:47:05 +0000 Subject: [PATCH] Read nearby 26.3 sign text --- mosfet/commands.py | 23 ++++++++++++ mosfet/protocol/managers.py | 29 +++++++++++++-- mosfet/world.py | 58 ++++++++++++++++++++++++++++++ tests/test_signs.py | 71 +++++++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 tests/test_signs.py diff --git a/mosfet/commands.py b/mosfet/commands.py index 116c345..2535546 100644 --- a/mosfet/commands.py +++ b/mosfet/commands.py @@ -324,6 +324,29 @@ class Commands: tree = next(self.g.world.find_trees(pos, 50)) reply = str(tree)[1:-1] + ## !signs [distance] - prints nearby sign text + if command == 'signs': + distance = float(data) if data else 32 + signs = self.g.world.find_signs( + utils.pint(self.g.pos), distance) + if signs: + results = [] + for sign in signs: + text = ' | '.join(line for line in sign['lines'] + if line.strip()) + entry = '{} {}: {}'.format( + sign['side'], sign['position'], text) + results.append(entry) + print('Sign {} {}:'.format( + sign['side'], sign['position'])) + for line in sign['lines']: + print(' ' + line) + reply = '; '.join(results) + if len(reply) > 240: + reply = reply[:237] + '...' + else: + reply = 'no signs found within {} blocks'.format(distance) + ## !info [query] - replies with info on a coordinate, block, item, or player if command == 'info': if not reply: diff --git a/mosfet/protocol/managers.py b/mosfet/protocol/managers.py index ce50205..f87e02b 100644 --- a/mosfet/protocol/managers.py +++ b/mosfet/protocol/managers.py @@ -4,8 +4,9 @@ from munch import Munch from minecraft.networking.packets import clientbound, serverbound from minecraft.networking.modern import ( - AddEntityPacket, ContainerOpenPacket, ContainerSetContentPacket, - ContainerSetSlotPacket, SetEntityDataPacket, + AddEntityPacket, BlockEntityDataPacket, ContainerOpenPacket, + ContainerSetContentPacket, ContainerSetSlotPacket, SetEntityDataPacket, + nbt_plain, PaletteSection, EntityPositionSyncPacket, MoveEntityPositionPacket, MoveEntityPositionRotationPacket, MoveEntityRotationPacket, @@ -218,6 +219,7 @@ class WorldState: self.min_section = min_y // 16 self.chunks = {} self.index = {} + self.block_entities = {} from mosfet.info import blocks self.indexed_ids = blocks.INDEXED_IDS self.center = None @@ -242,6 +244,10 @@ class WorldState: chunk = ChunkColumn(packet.x, packet.z, packet.sections, self.min_section) self.chunks[(packet.x, packet.z)] = chunk + for entity in getattr(packet, 'block_entities', []): + pos = (entity['x'], entity['y'], entity['z']) + self.set_block_entity( + pos, entity['type'], entity.get('data')) for section_y, section in chunk.sections.items(): palette = section['blocks'] if not self.indexed_ids.intersection( @@ -275,6 +281,12 @@ class WorldState: return None return chunk.get_block_at(floor(x), floor(y), floor(z)) + def set_block_entity(self, position, entity_type, data): + self.block_entities[tuple(position)] = { + 'type': entity_type, + 'data': nbt_plain(data), + } + def set_block_at(self, x, y, z, block_state_id): x, y, z = floor(x), floor(y), floor(z) chunk = self.get_chunk(floor(x / 16), floor(z / 16)) @@ -300,10 +312,16 @@ class WorldState: } if not self.index[state_id]: del self.index[state_id] + self.block_entities = { + pos: entity for pos, entity in self.block_entities.items() + if not (x0 <= pos[0] < x0 + 16 and + z0 <= pos[2] < z0 + 16) + } def unload_all_chunks(self): self.chunks.clear() self.index.clear() + self.block_entities.clear() def get_loaded_area(self, ignore_empty=False): if not self.chunks: @@ -348,6 +366,7 @@ class EntityManager: for packet_type, handler in ( (AddEntityPacket, self.handle_add_entity), (SetEntityDataPacket, self.handle_entity_data), + (BlockEntityDataPacket, self.handle_block_entity_data), (RemoveEntitiesPacket, self.handle_remove_entities), (MoveEntityPositionPacket, self.handle_move), (MoveEntityPositionRotationPacket, self.handle_move), @@ -379,6 +398,12 @@ class EntityManager: type=packet.entity_type_id, yaw=packet.yaw, pitch=packet.pitch) + def handle_block_entity_data(self, packet): + location = packet.location + self.g.chunks.set_block_entity( + (location.x, location.y, location.z), + packet.block_entity_type, packet.data) + def handle_entity_data(self, packet): obj = self.g.objects.get(packet.entity_id) if obj is None or not packet.item_stacks: diff --git a/mosfet/world.py b/mosfet/world.py index 21be7a8..c38cf87 100644 --- a/mosfet/world.py +++ b/mosfet/world.py @@ -1,4 +1,5 @@ import collections +import json import re import time import random @@ -18,6 +19,63 @@ class World: def block_at(self, x, y, z): return self.g.chunks.get_block_at(x, y, z) + @staticmethod + def _sign_component_text(value): + if value is None: + return '' + if isinstance(value, str): + stripped = value.strip() + if stripped.startswith(('{', '[')): + try: + parsed = json.loads(stripped) + except ValueError: + return value + if parsed != value: + return World._sign_component_text(parsed) + return value + if isinstance(value, (list, tuple)): + return ''.join(World._sign_component_text(part) for part in value) + if isinstance(value, dict): + text = value.get('text', '') + if not text and value.get('translate'): + text = value['translate'] + if isinstance(text, (dict, list)): + text = World._sign_component_text(text) + return (str(text) + + World._sign_component_text(value.get('with', [])) + + World._sign_component_text(value.get('extra', []))) + return str(value) + + def find_signs(self, center, distance=16): + """Return nearby loaded signs as position/side/four-lines records.""" + result = [] + for position, entity in self.g.chunks.block_entities.items(): + if utils.phyp(center, position) > distance: + continue + data = entity.get('data') or {} + for side in ('front', 'back'): + text_data = data.get(side + '_text') + if not isinstance(text_data, dict): + continue + messages = text_data.get('messages', []) + lines = tuple(self._sign_component_text(line) + for line in messages[:4]) + lines += ('',) * (4 - len(lines)) + if any(line.strip() for line in lines): + result.append({ + 'position': position, 'side': side, 'lines': lines, + }) + if not any(key in data for key in ('front_text', 'back_text')): + old_lines = tuple(self._sign_component_text(data.get( + 'Text{}'.format(index), '')) for index in range(1, 5)) + if any(line.strip() for line in old_lines): + result.append({ + 'position': position, 'side': 'front', + 'lines': old_lines, + }) + result.sort(key=lambda sign: utils.phyp(center, sign['position'])) + return result + def check_air_column(self, pos, distance): for i in range(distance): check = utils.padd(pos, (0, i, 0)) diff --git a/tests/test_signs.py b/tests/test_signs.py new file mode 100644 index 0000000..0576a28 --- /dev/null +++ b/tests/test_signs.py @@ -0,0 +1,71 @@ +import unittest +from munch import Munch + +from mosfet.commands import Commands +from mosfet.protocol.managers import EntityManager, WorldState +from mosfet.world import World + + +class SignReadingTests(unittest.TestCase): + def test_world_decodes_front_and_back_sign_components(self): + chunks = Munch(block_entities={ + (2, 64, 3): { + 'type': 7, + 'data': { + 'front_text': {'messages': [ + '{"text":"Hello","extra":[{"text":" world"}]}', + '{"text":"line two"}', '', ''], + }, + 'back_text': {'messages': [ + '{"text":"back"}', '', '', ''], + }, + }, + }, + }) + signs = World(Munch(chunks=chunks)).find_signs((0, 64, 0), 10) + + self.assertEqual(signs, [ + {'position': (2, 64, 3), 'side': 'front', + 'lines': ('Hello world', 'line two', '', '')}, + {'position': (2, 64, 3), 'side': 'back', + 'lines': ('back', '', '', '')}, + ]) + + def test_block_entity_update_is_cached_for_sign_search(self): + chunks = WorldState() + manager = object.__new__(EntityManager) + manager.g = Munch(chunks=chunks) + manager.handle_block_entity_data(Munch( + location=Munch(x=4, y=65, z=-2), + block_entity_type=7, + data={'front_text': {'messages': ['{"text":"Farm"}', '', '', '']}}, + )) + + signs = World(Munch(chunks=chunks)).find_signs((4, 65, -2), 1) + self.assertEqual(signs[0]['lines'], ('Farm', '', '', '')) + + def test_signs_command_replies_with_nearby_text(self): + class FakeChat: + def __init__(self): + self.sent = [] + + def set_handler(self, handler): + self.handler = handler + + def send(self, message): + self.sent.append(message) + + chat = FakeChat() + world = Munch(find_signs=lambda _position, _distance: [{ + 'position': (1, 64, 2), 'side': 'front', + 'lines': ('Hello', 'world', '', ''), + }]) + commands = Commands(Munch( + name='mosfet1', chat=chat, world=world, pos=(0.5, 64.0, 0.5))) + commands.handle_chat(('CHAT', 'tester', 'tester: !signs')) + + self.assertEqual(chat.sent, ['front (1, 64, 2): Hello | world']) + + +if __name__ == '__main__': + unittest.main()