"""
Contains the required classes and various utility methods to allow the addition of 3rd
party commands into the command-line portion of the editor.
"""
from __future__ import annotations
import os
from typing import List, Type, Tuple, Union, Callable, Any, Optional
import re
from api.world import World
from api.world_loader import loader
_coordinate_regex = re.compile(r"<(?P<x>-?\d+),(?P<y>\d+),(?P<z>-?\d+)>")
[docs]def parse_coordinates(coord: str) -> Optional[Tuple[int, int, int]]:
"""
Utility function for parsing X,Y,Z coordinates from a string
:param coord: The coordinate string to parse
:return: A tuple of the coordinates as ints in X,Y,Z order
"""
match = _coordinate_regex.match(coord)
if match:
return int(match.group("x")), int(match.group("y")), int(match.group("z"))
return None
[docs]def command(command_name: str) -> Type[Union[SimpleCommand, ComplexCommand]]:
"""
Registers a class as a command. If the class' parent is SimpleCommand, then it will use the abstracted methods.
If the class' parent is ComplexCommand, then any method decorated with the `subcommand` decorator is registered as
a sub-command of the given command name.
:param command_name: The name used to identify the command, or the base command if using ComplexCommand
"""
def decorator(command_class):
command_class.registered = False
if isinstance(command_class, type):
if issubclass(command_class, SimpleCommand):
command_class.command = (command_class.run, command_name)
command_class.registered = True
elif issubclass(command_class, ComplexCommand):
command_class.base_command = command_name
command_class.registered = True
return command_class
return decorator
[docs]class _CommandBase:
"""
Abstract class that both :class:`SimpleCommand` and :class:`ComplexCommand` inherit from
Note: Any class that inherits this class won't be registered as a command, you must
inherit from :class:`SimpleCommand` or :class:`ComplexCommand`
"""
[docs] def get_mode(self, mode_class: Type[Mode]) -> Mode:
"""
Method for getting a Mode that the program is in
:param mode_class: The class of the Mode instance to get
:return: The instance of the specified Mode, None if the mode hasn't been entered
"""
return self.handler.get_mode(mode_class)
[docs] def in_mode(self, mode_class: Type[Mode]) -> bool:
"""
Method for checking whether the program is in the specified Mode
:param mode_class: The class of the Mode to check for
:return: True if the program is in the specified Mode, False otherwise
"""
return self.handler.in_mode(mode_class)
def __init__(self, cmd_handler):
self.handler = cmd_handler
# self.in_mode = self.handler.in_mode
# self.get_mode = self.handler.get_mode
[docs] def error(self, message: Any):
"""
Utility method for printing an error message
:param message: The error message
"""
print(f"=== Error: {message}")
[docs] def warning(self, message: Any):
"""
Utility method for printing a warning message
:param message: The warning message
"""
print(f"== Warning: {message}")
[docs] def get_shared_data(self, entry_path: str) -> Optional[object]:
"""
Allows parsing and access to the shared data pool from a data accessor entry marked by a ``$``
:param entry_path: The path to search for an entry, can start with a "$" but isn't required to
:return: The value stored at the entry path, or None if the entry path couldn't be found
"""
if entry_path.startswith("$"):
entry_path = entry_path[1:]
depth = entry_path.split(":")
current_dict = self.handler.shared_data
for key in depth[:-1]:
if key not in current_dict:
return None
current_dict = current_dict[key]
if depth[-1] not in current_dict:
return None
return current_dict[depth[-1]]
[docs]class SimpleCommand(_CommandBase):
"""
Represents a command that can be executed within the command line
"""
[docs] @classmethod
def get_subclasses(cls) -> List[Type[SimpleCommand]]:
"""
Utility function to get all classes that extend this one
:return: A list of all subclasses
"""
result = []
for subcls in cls.__subclasses__():
result.append(subcls)
result.extend(subcls.get_subclasses())
return result
[docs] def run(self, args: List[str]):
"""
Abstract method where the command logic is ran when the command is executed
:param args: The arguments of the full command
"""
raise NotImplementedError()
[docs] def help(self):
"""
Abstract method for displaying a detailed help message about the command
This method expects all information to be printed, nothing returned will be used
"""
raise NotImplementedError()
[docs] def short_help(self) -> str:
"""
Abstract method for displaying a short help/summary message about the command
This method should return a string that is <= 50 characters in length, anything
longer will be truncated to 50 characters
"""
raise NotImplementedError
[docs]def subcommand(sub_command_name: str) -> Callable[[List[str]], None]:
"""
Registers the decorated method as a subcommand of the containing ComplexCommand.
:param sub_command_name: The name/identifier of the subcommand
"""
def decorator(f):
f.command = (f, sub_command_name)
return f
return decorator
[docs]class ComplexCommand(_CommandBase):
"""
Represents a base command that holds sub-commands
"""
def __init_subclass__(cls, **kwargs):
cls._persistent_data = {}
cls.sub_commands = {}
for key, val in cls.__dict__.items():
sub_cmd = getattr(val, "command", None)
if sub_cmd:
cls.sub_commands[sub_cmd[1]] = sub_cmd[0]
[docs] @classmethod
def get_subclasses(cls) -> List[Type[ComplexCommand]]:
"""
Utility function to get all classes that extend this one
:return: A list of all subclasses
"""
result = []
for sub in cls.__subclasses__():
result.append(sub)
result.extend(sub.get_subclasses())
return result
[docs] @classmethod
def help(cls, command_name: str = None):
"""
Abstract method that prints a detailed description of all the commands held by the base command if called
with default arguments. If help is called on a specific command, the subcommand's name will supplied with `command_name`
This method expects all information to be printed, nothing returned will be used
:arg command_name: The name/identifier of the specific command help was called on, None if help was called on the base command of the ComplexCommand
"""
raise NotImplementedError()
[docs] @classmethod
def short_help(cls) -> str:
"""
Abstract method for displaying a short help/summary message about the general purpose of the sub-commands
This method should return a string that is <= 50 characters in length, anything
longer will be truncated to 50 characters
"""
raise NotImplementedError()
[docs]class Mode:
"""
Represents a configurable state that the command line can enter. This is useful for controlling the execution of various
commands
"""
def __init__(self, cmd_line_handler):
self.handler = cmd_line_handler
def __init_subclass__(cls, **kwargs):
cls._persistent_data = {}
def display(self) -> str:
raise NotImplementedError()
[docs] def before_execution(self, command: List[str]) -> bool:
"""
Called before the execution of a command. Return True to run the given command, False to halt the execution.
:param command: The command that is to be executed
"""
raise NotImplementedError()
[docs] def enter(self) -> bool:
"""
Called when the mode is entered
:return: Return False if the mode is not ready to be entered, otherwise return True.
"""
raise NotImplementedError()
[docs] def exit(self) -> bool:
"""
Called when the mode is exited
:return: Return False if the mode is not ready to be exited, otherwise return True. If the exit command is supplied a '-f' argument, then the return value is ignored
"""
raise NotImplementedError()
[docs]class WorldMode(Mode):
def __init__(self, cmd_line_handler, **kwargs):
super(WorldMode, self).__init__(cmd_line_handler)
self._world_path = kwargs.get("world")
self._load_format = kwargs.get("world_format")
self._load_forced = kwargs.get("forced")
self._world_name = os.path.basename(self._world_path)
self._world: World = loader.load_world(
self._world_path, format=self._load_format, forced=self._load_forced
)
@property
def world_path(self) -> str:
return self._world_path
@property
def world(self) -> World:
return self._world
def display(self) -> str:
return self._world_name
[docs] def before_execution(self, cmd) -> bool:
return True
[docs] def enter(self) -> bool:
if self.handler.in_mode(WorldMode):
print("You cannot load a world if another world is already loaded!")
return False
if __debug__:
if self._load_format is not None:
print(f"Entered world mode using {self._load_format}")
else:
print("Entered world mode")
return True
[docs] def exit(self) -> bool:
if __debug__:
print("Exiting world mode")
self._world.exit()
return True