Module mudproto.xml
¶
Mume XML Protocol.
Class XMLMode(Enum)
¶
Valid modes corresponding to supported XML tags.
Source code in mudproto/xml.py
class XMLMode(Enum):
"""Valid modes corresponding to supported XML tags."""
NONE = auto()
DESCRIPTION = auto()
EXITS = auto()
MAGIC = auto()
NAME = auto()
PROMPT = auto()
ROOM = auto()
TERRAIN = auto()
Class XMLProtocol(ConnectionInterface)
¶
Implements the Mume XML protocol.
Source code in mudproto/xml.py
class XMLProtocol(ConnectionInterface):
"""Implements the Mume XML protocol."""
tintin_replacements: ClassVar[set[bytes]] = {
b"prompt",
b"name",
b"tell",
b"narrate",
b"pray",
b"say",
b"emote",
}
"""Tag to replacement values for Tintin."""
def __init__(
self,
*args: Any,
output_format: str,
**kwargs: Any,
) -> None:
"""
Defines the constructor.
Args:
*args: Positional arguments to be passed to the parent constructor.
output_format: The output format to be used.
**kwargs: Key-word only arguments to be passed to the parent constructor.
"""
self.output_format: str = output_format
super().__init__(*args, **kwargs)
self.state: XMLState = XMLState.DATA
"""The state of the state machine."""
self._tag_buffer: bytearray = bytearray() # Used for start and end tag names.
self._text_buffer: bytearray = bytearray() # Used for the text between start and end tags.
self._dynamic_buffer: bytearray = bytearray() # Used for dynamic room descriptions.
self._line_buffer: bytearray = bytearray() # Used for non-XML lines.
self._gratuitous: bool = False
self._mode: XMLMode = XMLMode.NONE
self._parent_modes: list[XMLMode] = []
def _handle_xml_text(self, data: bytes, app_data_buffer: bytearray) -> bytes:
"""
Handles XML data that is not part of a tag.
Args:
data: The received data.
app_data_buffer: The application level data buffer.
Returns:
The remaining data.
"""
app_data, separator, data = data.partition(LT)
if self.output_format == "raw" or not self._gratuitous:
# Gratuitous text should be omitted unless format is 'raw'.
app_data_buffer.extend(app_data)
if self._mode is XMLMode.NONE:
self._line_buffer.extend(app_data)
lines = self._line_buffer.splitlines(keepends=True)
self._line_buffer.clear()
if lines and not lines[-1].endswith((CR, LF)):
# Final line is incomplete.
self._line_buffer.extend(lines.pop())
for line in lines:
if line.strip():
self.on_xml_event("line", unescape_xml_bytes(line.rstrip(CR_LF)))
elif self._mode is XMLMode.ROOM:
self._dynamic_buffer.extend(app_data)
else:
self._text_buffer.extend(app_data)
if separator:
self.state = XMLState.TAG
return data
def _handle_xml_tag(self, data: bytes, app_data_buffer: bytearray) -> bytes: # NOQA: C901, PLR0912
"""
Handles XML data that is part of a tag (I.E. enclosed in '<>').
Args:
data: The received data.
app_data_buffer: The application level data buffer.
Returns:
The remaining data.
"""
app_data, separator, data = data.partition(GT)
self._tag_buffer.extend(app_data)
if not separator:
# End of tag not reached yet.
return data
tag: bytes = bytes(self._tag_buffer).strip()
self._tag_buffer.clear()
tag_name: str = decode_bytes(tag).strip("/").split(None, 1)[0] if tag else ""
is_closing_tag: bool = tag.startswith(b"/")
if self.output_format == "raw":
app_data_buffer.extend(LT + tag + GT)
elif self.output_format == "tintin" and not self._gratuitous:
app_data_buffer.extend(get_tintin_tag_replacement(tag, self.tintin_replacements))
if tag_name == "gratuitous":
self._gratuitous = not is_closing_tag
elif is_closing_tag and get_xml_mode(tag_name) is self._mode:
# The tag is a closing tag, corresponding with the current mode.
if self._mode is XMLMode.ROOM:
self.on_xml_event("dynamic", unescape_xml_bytes(bytes(self._dynamic_buffer).lstrip(b"\r\n")))
self._dynamic_buffer.clear()
else:
self.on_xml_event(tag_name, unescape_xml_bytes(bytes(self._text_buffer)))
self._text_buffer.clear()
self._mode = self._parent_modes.pop()
elif tag_name == "magic":
# Magic tags can occur inside and outside room info.
self._parent_modes.append(self._mode)
self._mode = XMLMode.MAGIC
elif self._mode is XMLMode.NONE and tag_name == "movement":
# Movement is transmitted as a self-closing tag, I.E. opening and closing tag in one.
# Because of this, we don't need a separate mode for movement.
self.on_xml_event(tag_name, direction_from_movement(unescape_xml_bytes(tag)))
elif self._mode is XMLMode.NONE:
# A new child mode from NONE.
if tag_name == "prompt":
self._parent_modes.append(self._mode)
self._mode = XMLMode.PROMPT
elif tag_name == "room":
self._parent_modes.append(self._mode)
self._mode = XMLMode.ROOM
self.on_xml_event("room", unescape_xml_bytes(tag[5:]))
elif self._mode is XMLMode.ROOM:
# New child mode from ROOM.
if tag_name == "name":
self._parent_modes.append(self._mode)
self._mode = XMLMode.NAME
elif tag_name == "description":
self._parent_modes.append(self._mode)
self._mode = XMLMode.DESCRIPTION
elif tag_name == "exits":
self._parent_modes.append(self._mode)
self._mode = XMLMode.EXITS
elif tag_name == "terrain":
self._parent_modes.append(self._mode)
self._mode = XMLMode.TERRAIN
self.state = XMLState.DATA
return data
def on_data_received(self, data: bytes) -> None: # NOQA: D102
app_data_buffer: bytearray = bytearray()
while data:
if self.state is XMLState.DATA:
data = self._handle_xml_text(data, app_data_buffer)
elif self.state is XMLState.TAG:
data = self._handle_xml_tag(data, app_data_buffer)
if app_data_buffer:
if self.output_format == "raw":
super().on_data_received(bytes(app_data_buffer))
else:
super().on_data_received(unescape_xml_bytes(bytes(app_data_buffer)))
def on_connection_made(self) -> None: # NOQA: D102
# Turn on XML mode.
# Mode "3" tells MUME to enable XML output without sending an initial "<xml>" tag.
# Option "G" tells MUME to wrap room descriptions in gratuitous
# tags if they would otherwise be hidden.
self.write(MPI_INIT + b"X2" + LF + b"3G" + LF)
def on_connection_lost(self) -> None: # NOQA: D102
pass
def on_xml_event(self, name: str, data: bytes) -> None:
"""
Called when an XML event was received.
Args:
name: The event name.
data: The payload.
"""
Attribute is_client: bool
inherited
property
readonly
¶
True if acting as a client, False otherwise.
Attribute is_server: bool
inherited
property
readonly
¶
True if acting as a server, False otherwise.
Attribute tintin_replacements: ClassVar[set[bytes]]
¶
Tag to replacement values for Tintin.
Method __init__(self, *args, *, output_format, **kwargs)
special
¶
Defines the constructor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args |
Any |
Positional arguments to be passed to the parent constructor. |
() |
output_format |
str |
The output format to be used. |
required |
**kwargs |
Any |
Key-word only arguments to be passed to the parent constructor. |
{} |
Source code in mudproto/xml.py
def __init__(
self,
*args: Any,
output_format: str,
**kwargs: Any,
) -> None:
"""
Defines the constructor.
Args:
*args: Positional arguments to be passed to the parent constructor.
output_format: The output format to be used.
**kwargs: Key-word only arguments to be passed to the parent constructor.
"""
self.output_format: str = output_format
super().__init__(*args, **kwargs)
self.state: XMLState = XMLState.DATA
"""The state of the state machine."""
self._tag_buffer: bytearray = bytearray() # Used for start and end tag names.
self._text_buffer: bytearray = bytearray() # Used for the text between start and end tags.
self._dynamic_buffer: bytearray = bytearray() # Used for dynamic room descriptions.
self._line_buffer: bytearray = bytearray() # Used for non-XML lines.
self._gratuitous: bool = False
self._mode: XMLMode = XMLMode.NONE
self._parent_modes: list[XMLMode] = []
Method on_connection_lost(self)
¶
Called by disconnect when a connection to peer has been lost.
Source code in mudproto/xml.py
def on_connection_lost(self) -> None: # NOQA: D102
pass
Method on_connection_made(self)
¶
Called by connect when a connection to peer has been established.
Source code in mudproto/xml.py
def on_connection_made(self) -> None: # NOQA: D102
# Turn on XML mode.
# Mode "3" tells MUME to enable XML output without sending an initial "<xml>" tag.
# Option "G" tells MUME to wrap room descriptions in gratuitous
# tags if they would otherwise be hidden.
self.write(MPI_INIT + b"X2" + LF + b"3G" + LF)
Method on_data_received(self, data)
¶
Called by parse when data is received.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data |
bytes |
The received data. |
required |
Source code in mudproto/xml.py
def on_data_received(self, data: bytes) -> None: # NOQA: D102
app_data_buffer: bytearray = bytearray()
while data:
if self.state is XMLState.DATA:
data = self._handle_xml_text(data, app_data_buffer)
elif self.state is XMLState.TAG:
data = self._handle_xml_tag(data, app_data_buffer)
if app_data_buffer:
if self.output_format == "raw":
super().on_data_received(bytes(app_data_buffer))
else:
super().on_data_received(unescape_xml_bytes(bytes(app_data_buffer)))
Method on_xml_event(self, name, data)
¶
Called when an XML event was received.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name |
str |
The event name. |
required |
data |
bytes |
The payload. |
required |
Source code in mudproto/xml.py
def on_xml_event(self, name: str, data: bytes) -> None:
"""
Called when an XML event was received.
Args:
name: The event name.
data: The payload.
"""
Method write(self, data)
inherited
¶
Writes data to peer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data |
bytes |
The bytes to be written. |
required |
Source code in mudproto/xml.py
def write(self, data: bytes) -> None:
"""
Writes data to peer.
Args:
data: The bytes to be written.
"""
self._writer(data)
Class XMLState(Enum)
¶
Valid states for the state machine.
Source code in mudproto/xml.py
class XMLState(Enum):
"""Valid states for the state machine."""
DATA = auto()
TAG = auto()
Function direction_from_movement(movement)
¶
Retrieves the direction of movement from a movement tag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
movement |
bytes |
The movement tag to parse. |
required |
Returns:
| Type | Description |
|---|---|
bytes |
The direction of movement. |
Source code in mudproto/xml.py
def direction_from_movement(movement: bytes) -> bytes:
"""
Retrieves the direction of movement from a movement tag.
Args:
movement: The movement tag to parse.
Returns:
The direction of movement.
"""
match: ReBytesMatchType = DIRECTIONS_REGEX.search(movement)
return match.group("dir") if match is not None else b""
Function get_tintin_tag_replacement(tag, valid_tags)
¶
Retrieves a Tintin tag replacement from a tag name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag |
bytes |
The tag name. |
required |
valid_tags |
Iterable[bytes] |
The supported tag names. |
required |
Returns:
| Type | Description |
|---|---|
bytes |
Uppercase tag name followed by a colon if opening tag, a colon followed by uppercase tag name if closing tag, An empty bytes object if not found. |
Source code in mudproto/xml.py
def get_tintin_tag_replacement(tag: bytes, valid_tags: Iterable[bytes]) -> bytes:
"""
Retrieves a Tintin tag replacement from a tag name.
Args:
tag: The tag name.
valid_tags: The supported tag names.
Returns:
Uppercase tag name followed by a colon if opening tag,
a colon followed by uppercase tag name if closing tag,
An empty bytes object if not found.
"""
is_closing: bool = tag.startswith(b"/")
tag = tag.strip(b"/")
return b"" if tag not in valid_tags else b":" + tag.upper() if is_closing else tag.upper() + b":"
Function get_xml_mode(tag)
¶
Retrieves an XMLMode enum from a tag name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag |
str |
The tag name. |
required |
Returns:
| Type | Description |
|---|---|
XMLMode | None |
the XMLMode enum corresponding to the tag name, None if not found. |
Source code in mudproto/xml.py
def get_xml_mode(tag: str) -> XMLMode | None:
"""
Retrieves an XMLMode enum from a tag name.
Args:
tag: The tag name.
Returns:
the XMLMode enum corresponding to the tag name, None if not found.
"""
with suppress(KeyError):
return XMLMode[tag.upper()]
return None