Source code for mciwb.signs
"""
Add an interactive capability through the placing of command signs in the world
"""
import re
from time import sleep
from typing import Callable, Dict
from mcwb.types import Item, Vec3
from mciwb.copier import CopyPaste
from mciwb.logging import log
from mciwb.player import Player
from mciwb.threads import get_client
CallbackPosFunction = Callable[[Vec3], None]
[docs]
class Signs:
"""
Monitor the world for signs placed by a player. Perform an action based
on the text of the sign.
Each sign object can be used to monitor the placing of signs by a single
player. The object can be hooked to functions that are to be called when
a sign is placed by the player.
By default each Sign object is initialized with select/copy/paste
functions. Additional signs with callback functions can be added.
For best results, use the bound methods of an object for the callback
functions. That way the user code can manage state within the object
providing those functions.
"""
_re_sign_text = re.compile("""front_text.*messages: \\['"([^"]*)"',""")
_re_sign_entity = (
"""minecraft:oak_sign{{"""
"""BlockEntityTag:{{front_text:{{"""
"""messages:['["{0}"]','[""]','[""]','[""]']}}}},"""
"""display:{{Name:'{{"text":"{0}"}}'}}"""
"""}}"""
)
_wall_sign = "minecraft:oak_wall_sign"
def __init__(self, player: Player):
self.player = player
self.copy = CopyPaste()
self.signs: Dict[str, CallbackPosFunction] = self.copy.get_commands()
def _get_target_block(self, pos: Vec3, facing: Vec3) -> Vec3:
"""
determine the target block that the sign at pos indicates
"""
# use 'execute if' with a benign command like seed
result = get_client().execute.if_.block(pos, self._wall_sign).run("seed")
if "Seed" in result:
# wall signs target the block behind them
pos += facing
else:
# standing signs target the block below them
pos += Vec3(0, -1, 0)
return pos
def _poll(self):
"""
check if a sign has been placed in front of the player
1 to 4 blocks away and take action based on sign text
"""
client = get_client()
facing = self.player.facing
player_pos = self.player.pos
for height in range(-1, 3):
for distance in range(1, 4):
sleep(0) # don't hog the connection
pos = player_pos + facing * distance
block_pos = pos.with_ints() + Vec3(0, height, 0)
data = client.data.get(block=block_pos)
match = self._re_sign_text.search(data)
if match:
text = match.group(1)
log.debug(f"Sign at {pos} has text {text}")
target = self._get_target_block(block_pos, facing)
self.do_action(text, target, block_pos)
[docs]
def do_action(self, command: str, target: Vec3, block_pos: Vec3):
"""
Perform an action based on the text of the sign. The action is to
call the callback function that is configured for this sign text.
:param command: the text of the sign
:param target: the target block that the sign indicates
:param block_pos: the position of the block that the sign is on - this is
cleared if the sign is used
"""
# if the command is not found then this is just an ordinary sign (I assume!)
if command in self.signs:
get_client().setblock(block_pos, str(Item.AIR))
log.info(f"{command} at {target}")
self.signs[command](target)
[docs]
def add_sign(self, name: str, function: CallbackPosFunction):
"""
Add a new sign type with its action callback function
:param name: the text of the sign
:param function: the callback function to be called when the sign is placed
"""
# don't allow multiple signs with the same name
self.remove_sign(name)
self.signs[name] = function
[docs]
def remove_sign(self, name: str):
"""
Stop monitoring for a sign with the given name
:param name: the text of the sign
"""
if name in self.signs:
del self.signs[name]
_sign_match = '{{"text":"{}"}}'
[docs]
def give_signs(self):
"""
Give player one of each command sign in our commands list
Check first if the player has the sign already
"""
client = get_client()
inventory = self.player.inventory
for command in self.signs:
if not self._sign_match.format(command) in inventory:
client.give(self.player.name, self._re_sign_entity.format(command))