Merge pull request #2924 from ChrisLR/godot-client

Godot client contrib
This commit is contained in:
Griatch 2022-12-01 19:07:45 +01:00 committed by GitHub
commit cdcafcf14d
5 changed files with 1059 additions and 0 deletions

View file

@ -0,0 +1,289 @@
# Godot Websocket
Contribution by ChrisLR, 2022
This contrib allows you to connect a Godot Client directly to your mud,
and display regular text with color in Godot's RichTextLabel using BBCode.
You can use Godot to provide advanced functionality with proper Evennia support.
## Installation
You need to add the following settings in your settings.py and restart evennia.
```python
PORTAL_SERVICES_PLUGIN_MODULES.append('evennia.contrib.base_systems.godotwebsocket.webclient')
GODOT_CLIENT_WEBSOCKET_PORT = 4008
GODOT_CLIENT_WEBSOCKET_CLIENT_INTERFACE = "127.0.0.1"
```
This will make evennia listen on the port 4008 for Godot.
You can change the port and interface as you want.
## Usage
The tl;dr of it is to connect using a Godot Websocket using the port defined above.
It will let you transfer data from Evennia to Godot, allowing you
to get styled text in a RichTextLabel with bbcode enabled or to handle
the extra data given from Evennia as needed.
This section assumes you have basic knowledge on how to use Godot.
You can read the following url for more details on Godot Websockets
and to implement a minimal client.
https://docs.godotengine.org/en/stable/tutorials/networking/websocket.html
The rest of this document will be for Godot 3, an example is left at the bottom
of this readme for Godot 4.
At the top of the file you must change the url to point at your mud.
```
extends Node
# The URL we will connect to
export var websocket_url = "ws://localhost:4008"
```
You must also remove the protocol from the `connect_to_url` call made
within the `_ready` function.
```
func _ready():
# ...
# Change the following line from this
var err = _client.connect_to_url(websocket_url, ["lws-mirror-protocol"])
# To this
var err = _client.connect_to_url(websocket_url)
# ...
```
This will allow you to connect to your mud.
After that you need to properly handle the data sent by evennia.
To do this, you should replace your `_on_data` method.
You will need to parse the JSON received to properly act on the data.
Here is an example
```
func _on_data():
# The following two lines will get us the data from Evennia.
var data = _client.get_peer(1).get_packet().get_string_from_utf8()
var json_data = JSON.parse(data).result
# The json_data is an array
# The first element informs us this is simple text
# so we add it to the RichTextlabel
if json_data[0] == 'text':
for msg in json_data[1]:
label.append_bbcode(msg)
# Always useful to print the data and see what we got.
print(data)
```
The first element is the type, it will be `text` if it is a message
It can be anything you would provide to the Evennia `msg` function.
The second element will be the data related to the type of message, in this case it is a list of text to display.
Since it is parsed BBCode, we can add that directly to a RichTextLabel by calling its append_bbcode method.
If you want anything better than fancy text in Godot, you will have
to leverage Evennia's OOB to send extra data.
You can [read more on OOB here](https://www.evennia.com/docs/latest/OOB.html#oob).
In this example, we send coordinates whenever we message our character.
Evennia
```python
caller.msg(coordinates=(9, 2))
```
Godot
```gdscript
func _on_data():
...
if json_data[0] == 'text':
for msg in json_data[1]:
label.append_bbcode(msg)
# Notice the first element is the name of the kwarg we used from evennia.
elif json_data[0] == 'coordinates':
var coords_data = json_data[2]
player.set_pos(coords_data)
...
```
A good idea would be to set up Godot Signals you can trigger based on the data
you receive, so you can manage the code better.
## Known Issues
- Sending SaverDicts and similar objects straight from Evennia .DB will cause issues,
cast them to dict() or list() before doing so.
- Background colors are only supported by Godot 4.
## Godot 3 Example
This is an example of a Script to use in Godot 3.
The script can be attached to the root UI node.
```gdscript
extends Node
# The URL to connect to, should be your mud.
export var websocket_url = "ws://127.0.0.1:4008"
# These are references to controls in the scene
onready var parent = get_parent()
onready var label = parent.get_node("%ChatLog")
onready var txtEdit = parent.get_node("%ChatInput")
onready var room = get_node("/root/World/Room")
# Our WebSocketClient instance
var _client = WebSocketClient.new()
var is_connected = false
func _ready():
# Connect base signals to get notified of connection open, close, errors and messages
_client.connect("connection_closed", self, "_closed")
_client.connect("connection_error", self, "_closed")
_client.connect("connection_established", self, "_connected")
_client.connect("data_received", self, "_on_data")
print('Ready')
# Initiate connection to the given URL.
var err = _client.connect_to_url(websocket_url)
if err != OK:
print("Unable to connect")
set_process(false)
func _closed(was_clean = false):
# was_clean will tell you if the disconnection was correctly notified
# by the remote peer before closing the socket.
print("Closed, clean: ", was_clean)
set_process(false)
func _connected(proto = ""):
is_connected = true
print("Connected with protocol: ", proto)
func _on_data():
# This is called when Godot receives data from evennia
var data = _client.get_peer(1).get_packet().get_string_from_utf8()
var json_data = JSON.parse(data).result
# Here we have the data from Evennia which is an array.
# The first element will be text if it is a message
# and would be the key of the OOB data you passed otherwise.
if json_data[0] == 'text':
# In this case, we simply append the data as bbcode to our label.
for msg in json_data[1]:
label.append_bbcode(msg)
elif json_data[0] == 'coordinates':
# Dummy signal emitted if we wanted to handle the new coordinates
# elsewhere in the project.
self.emit_signal('updated_coordinates', json_data[1])
# We only print this for easier debugging.
print(data)
func _process(delta):
# Required for websocket to properly react
_client.poll()
func _on_button_send():
# This is called when we press the button in the scene
# with a connected signal, it sends the written message to Evennia.
var msg = txtEdit.text
var msg_arr = ['text', [msg], {}]
var msg_str = JSON.print(msg_arr)
_client.get_peer(1).put_packet(msg_str.to_utf8())
func _notification(what):
# This is a special method that allows us to notify Evennia we are closing.
if what == MainLoop.NOTIFICATION_WM_QUIT_REQUEST:
if is_connected:
var msg_arr = ['text', ['quit'], {}]
var msg_str = JSON.print(msg_arr)
_client.get_peer(1).put_packet(msg_str.to_utf8())
get_tree().quit() # default behavior
```
## Godot 4 Example
This is an example of a Script to use in Godot 4.
Note that the version is not final so the code may break.
It requires a WebSocketClientNode as a child of the root node.
The script can be attached to the root UI node.
```gdscript
extends Control
# The URL to connect to, should be your mud.
var websocket_url = "ws://127.0.0.1:4008"
# These are references to controls in the scene
@onready
var label: RichTextLabel = get_node("%ChatLog")
@onready
var txtEdit: TextEdit = get_node("%ChatInput")
@onready
var websocket = get_node("WebSocketClient")
func _ready():
# We connect the various signals
websocket.connect('connected_to_server', self._connected)
websocket.connect('connection_closed', self._closed)
websocket.connect('message_received', self._on_data)
# We attempt to connect and print out the error if we have one.
var result = websocket.connect_to_url(websocket_url)
if result != OK:
print('Could not connect:' + str(result))
func _closed():
# This emits if the connection was closed by the remote host or unexpectedly
print('Connection closed.')
set_process(false)
func _connected():
# This emits when the connection succeeds.
print('Connected!')
func _on_data(data):
# This is called when Godot receives data from evennia
var json_data = JSON.parse_string(data)
# Here we have the data from Evennia which is an array.
# The first element will be text if it is a message
# and would be the key of the OOB data you passed otherwise.
if json_data[0] == 'text':
# In this case, we simply append the data as bbcode to our label.
for msg in json_data[1]:
# Here we include a newline at every message.
label.append_text("\n" + msg)
elif json_data[0] == 'coordinates':
# Dummy signal emitted if we wanted to handle the new coordinates
# elsewhere in the project.
self.emit_signal('updated_coordinates', json_data[1])
# We only print this for easier debugging.
print(data)
func _on_button_pressed():
# This is called when we press the button in the scene
# with a connected signal, it sends the written message to Evennia.
var msg = txtEdit.text
var msg_arr = ['text', [msg], {}]
var msg_str = JSON.stringify(msg_arr)
websocket.send(msg_str)
```

View file

@ -0,0 +1,14 @@
"""
Godot Websocket - ChrisLR 2022
This provides parsing the ansi text to bbcode used by Godot for their RichTextLabel
and also provides the proper portal service to dedicate a port for Godot's Websockets.
This allows you to connect both the regular webclient and a godot specific webclient.
You can simply connect the resulting text to Godot's RichTextLabel and have the proper display.
You could also pass extra data to this client for advanced functionality.
See the docs for more information.
"""
from evennia.contrib.base_systems.godotwebsocket.text2bbcode import parse_to_bbcode, BBCODE_PARSER
from evennia.contrib.base_systems.godotwebsocket.webclient import GodotWebSocketClient

View file

@ -0,0 +1,85 @@
"""Tests for text2bbcode """
import mock
from django.test import TestCase
from evennia.contrib.base_systems.godotwebsocket import text2bbcode
from evennia.utils import ansi
class TestText2Bbcode(TestCase):
def test_format_styles(self):
parser = text2bbcode.BBCODE_PARSER
self.assertEqual("foo", parser.format_styles("foo"))
self.assertEqual(
'[color=#800000]red[/color]foo',
parser.format_styles(
ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"
),
)
self.assertEqual(
'[bgcolor=#800000]red[/bgcolor]foo',
parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'[bgcolor=#800000][color=#008000]red[/color][/bgcolor]foo',
parser.format_styles(
ansi.ANSI_BACK_RED
+ ansi.ANSI_UNHILITE
+ ansi.ANSI_GREEN
+ "red"
+ ansi.ANSI_NORMAL
+ "foo"
),
)
self.assertEqual(
'a [u]red[/u]foo',
parser.format_styles("a " + ansi.ANSI_UNDERLINE + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a [blink]red[/blink]foo',
parser.format_styles("a " + ansi.ANSI_BLINK + "red" + ansi.ANSI_NORMAL + "foo"),
)
self.assertEqual(
'a [bgcolor=#c0c0c0][color=#000000]red[/color][/bgcolor]foo',
parser.format_styles("a " + ansi.ANSI_INVERSE + "red" + ansi.ANSI_NORMAL + "foo"),
)
def test_convert_urls(self):
parser = text2bbcode.BBCODE_PARSER
self.assertEqual("foo", parser.convert_urls("foo"))
self.assertEqual(
'a [url=http://redfoo]http://redfoo[/url] runs',
parser.convert_urls("a http://redfoo runs"),
)
def test_sub_mxp_links(self):
parser = text2bbcode.BBCODE_PARSER
mocked_match = mock.Mock()
mocked_match.groups.return_value = ["cmd", "text"]
self.assertEqual("[mxp=send cmd=cmd]text[/mxp]", parser.sub_mxp_links(mocked_match))
def test_sub_text(self):
parser = text2bbcode.BBCODE_PARSER
mocked_match = mock.Mock()
mocked_match.groupdict.return_value = {"lineend": "foo"}
self.assertEqual("\n", parser.sub_text(mocked_match))
def test_parse_bbcode(self):
self.assertEqual("foo", text2bbcode.parse_to_bbcode("foo"))
self.maxDiff = None
self.assertEqual(
text2bbcode.parse_to_bbcode("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
'[blink][bgcolor=#008080]Hello[/bgcolor][/blink]'
'[u][color=#ff0000]W[/color][/u]'
'[u][color=#00ff00]o[/color][/u]'
'[u][color=#ffff00]r[/color][/u]'
'[u][color=#0000ff]l[/color][/u]'
'[u][color=#ff00ff]d[/color][/u]'
'[u][color=#00ffff]![/color][/u]'
'[u][bgcolor=#008000][color=#00ffff]![/color][/bgcolor][/u]',
)

View file

@ -0,0 +1,592 @@
"""
Godot Websocket - ChrisLR 2022
This file contains the necessary code and data to convert text with color tags to bbcode (For godot)
"""
from evennia.utils.ansi import *
from evennia.utils.text2html import TextToHTMLparser
# All xterm256 RGB equivalents
XTERM256_FG = "\033[38;5;{}m"
XTERM256_BG = "\033[48;5;{}m"
COLOR_INDICE_TO_HEX = {
'color-000': '#000000', 'color-001': '#800000', 'color-002': '#008000', 'color-003': '#808000',
'color-004': '#000080', 'color-005': '#800080', 'color-006': '#008080', 'color-007': '#c0c0c0',
'color-008': '#808080', 'color-009': '#ff0000', 'color-010': '#00ff00', 'color-011': '#ffff00',
'color-012': '#0000ff', 'color-013': '#ff00ff', 'color-014': '#00ffff', 'color-015': '#ffffff',
'color-016': '#000000', 'color-017': '#00005f', 'color-018': '#000087', 'color-019': '#0000af',
'color-020': '#0000df', 'color-021': '#0000ff', 'color-022': '#005f00', 'color-023': '#005f5f',
'color-024': '#005f87', 'color-025': '#005faf', 'color-026': '#005fdf', 'color-027': '#005fff',
'color-028': '#008700', 'color-029': '#00875f', 'color-030': '#008787', 'color-031': '#0087af',
'color-032': '#0087df', 'color-033': '#0087ff', 'color-034': '#00af00', 'color-035': '#00af5f',
'color-036': '#00af87', 'color-037': '#00afaf', 'color-038': '#00afdf', 'color-039': '#00afff',
'color-040': '#00df00', 'color-041': '#00df5f', 'color-042': '#00df87', 'color-043': '#00dfaf',
'color-044': '#00dfdf', 'color-045': '#00dfff', 'color-046': '#00ff00', 'color-047': '#00ff5f',
'color-048': '#00ff87', 'color-049': '#00ffaf', 'color-050': '#00ffdf', 'color-051': '#00ffff',
'color-052': '#5f0000', 'color-053': '#5f005f', 'color-054': '#5f0087', 'color-055': '#5f00af',
'color-056': '#5f00df', 'color-057': '#5f00ff', 'color-058': '#5f5f00', 'color-059': '#5f5f5f',
'color-060': '#5f5f87', 'color-061': '#5f5faf', 'color-062': '#5f5fdf', 'color-063': '#5f5fff',
'color-064': '#5f8700', 'color-065': '#5f875f', 'color-066': '#5f8787', 'color-067': '#5f87af',
'color-068': '#5f87df', 'color-069': '#5f87ff', 'color-070': '#5faf00', 'color-071': '#5faf5f',
'color-072': '#5faf87', 'color-073': '#5fafaf', 'color-074': '#5fafdf', 'color-075': '#5fafff',
'color-076': '#5fdf00', 'color-077': '#5fdf5f', 'color-078': '#5fdf87', 'color-079': '#5fdfaf',
'color-080': '#5fdfdf', 'color-081': '#5fdfff', 'color-082': '#5fff00', 'color-083': '#5fff5f',
'color-084': '#5fff87', 'color-085': '#5fffaf', 'color-086': '#5fffdf', 'color-087': '#5fffff',
'color-088': '#870000', 'color-089': '#87005f', 'color-090': '#870087', 'color-091': '#8700af',
'color-092': '#8700df', 'color-093': '#8700ff', 'color-094': '#875f00', 'color-095': '#875f5f',
'color-096': '#875f87', 'color-097': '#875faf', 'color-098': '#875fdf', 'color-099': '#875fff',
'color-100': '#878700', 'color-101': '#87875f', 'color-102': '#878787', 'color-103': '#8787af',
'color-104': '#8787df', 'color-105': '#8787ff', 'color-106': '#87af00', 'color-107': '#87af5f',
'color-108': '#87af87', 'color-109': '#87afaf', 'color-110': '#87afdf', 'color-111': '#87afff',
'color-112': '#87df00', 'color-113': '#87df5f', 'color-114': '#87df87', 'color-115': '#87dfaf',
'color-116': '#87dfdf', 'color-117': '#87dfff', 'color-118': '#87ff00', 'color-119': '#87ff5f',
'color-120': '#87ff87', 'color-121': '#87ffaf', 'color-122': '#87ffdf', 'color-123': '#87ffff',
'color-124': '#af0000', 'color-125': '#af005f', 'color-126': '#af0087', 'color-127': '#af00af',
'color-128': '#af00df', 'color-129': '#af00ff', 'color-130': '#af5f00', 'color-131': '#af5f5f',
'color-132': '#af5f87', 'color-133': '#af5faf', 'color-134': '#af5fdf', 'color-135': '#af5fff',
'color-136': '#af8700', 'color-137': '#af875f', 'color-138': '#af8787', 'color-139': '#af87af',
'color-140': '#af87df', 'color-141': '#af87ff', 'color-142': '#afaf00', 'color-143': '#afaf5f',
'color-144': '#afaf87', 'color-145': '#afafaf', 'color-146': '#afafdf', 'color-147': '#afafff',
'color-148': '#afdf00', 'color-149': '#afdf5f', 'color-150': '#afdf87', 'color-151': '#afdfaf',
'color-152': '#afdfdf', 'color-153': '#afdfff', 'color-154': '#afff00', 'color-155': '#afff5f',
'color-156': '#afff87', 'color-157': '#afffaf', 'color-158': '#afffdf', 'color-159': '#afffff',
'color-160': '#df0000', 'color-161': '#df005f', 'color-162': '#df0087', 'color-163': '#df00af',
'color-164': '#df00df', 'color-165': '#df00ff', 'color-166': '#df5f00', 'color-167': '#df5f5f',
'color-168': '#df5f87', 'color-169': '#df5faf', 'color-170': '#df5fdf', 'color-171': '#df5fff',
'color-172': '#df8700', 'color-173': '#df875f', 'color-174': '#df8787', 'color-175': '#df87af',
'color-176': '#df87df', 'color-177': '#df87ff', 'color-178': '#dfaf00', 'color-179': '#dfaf5f',
'color-180': '#dfaf87', 'color-181': '#dfafaf', 'color-182': '#dfafdf', 'color-183': '#dfafff',
'color-184': '#dfdf00', 'color-185': '#dfdf5f', 'color-186': '#dfdf87', 'color-187': '#dfdfaf',
'color-188': '#dfdfdf', 'color-189': '#dfdfff', 'color-190': '#dfff00', 'color-191': '#dfff5f',
'color-192': '#dfff87', 'color-193': '#dfffaf', 'color-194': '#dfffdf', 'color-195': '#dfffff',
'color-196': '#ff0000', 'color-197': '#ff005f', 'color-198': '#ff0087', 'color-199': '#ff00af',
'color-200': '#ff00df', 'color-201': '#ff00ff', 'color-202': '#ff5f00', 'color-203': '#ff5f5f',
'color-204': '#ff5f87', 'color-205': '#ff5faf', 'color-206': '#ff5fdf', 'color-207': '#ff5fff',
'color-208': '#ff8700', 'color-209': '#ff875f', 'color-210': '#ff8787', 'color-211': '#ff87af',
'color-212': '#ff87df', 'color-213': '#ff87ff', 'color-214': '#ffaf00', 'color-215': '#ffaf5f',
'color-216': '#ffaf87', 'color-217': '#ffafaf', 'color-218': '#ffafdf', 'color-219': '#ffafff',
'color-220': '#ffdf00', 'color-221': '#ffdf5f', 'color-222': '#ffdf87', 'color-223': '#ffdfaf',
'color-224': '#ffdfdf', 'color-225': '#ffdfff', 'color-226': '#ffff00', 'color-227': '#ffff5f',
'color-228': '#ffff87', 'color-229': '#ffffaf', 'color-230': '#ffffdf', 'color-231': '#ffffff',
'color-232': '#080808', 'color-233': '#121212', 'color-234': '#1c1c1c', 'color-235': '#262626',
'color-236': '#303030', 'color-237': '#3a3a3a', 'color-238': '#444444', 'color-239': '#4e4e4e',
'color-240': '#585858', 'color-241': '#606060', 'color-242': '#666666', 'color-243': '#767676',
'color-244': '#808080', 'color-245': '#8a8a8a', 'color-246': '#949494', 'color-247': '#9e9e9e',
'color-248': '#a8a8a8', 'color-249': '#b2b2b2', 'color-250': '#bcbcbc', 'color-251': '#c6c6c6',
'color-252': '#d0d0d0', 'color-253': '#dadada', 'color-254': '#e4e4e4', 'color-255': '#eeeeee',
'bgcolor-000': '#000000', 'bgcolor-001': '#800000', 'bgcolor-002': '#008000',
'bgcolor-003': '#808000', 'bgcolor-004': '#000080', 'bgcolor-005': '#800080',
'bgcolor-006': '#008080', 'bgcolor-007': '#c0c0c0', 'bgcolor-008': '#808080',
'bgcolor-009': '#ff0000', 'bgcolor-010': '#00ff00', 'bgcolor-011': '#ffff00',
'bgcolor-012': '#0000ff', 'bgcolor-013': '#ff00ff', 'bgcolor-014': '#00ffff',
'bgcolor-015': '#ffffff', 'bgcolor-016': '#000000', 'bgcolor-017': '#00005f',
'bgcolor-018': '#000087', 'bgcolor-019': '#0000af', 'bgcolor-020': '#0000df',
'bgcolor-021': '#0000ff', 'bgcolor-022': '#005f00', 'bgcolor-023': '#005f5f',
'bgcolor-024': '#005f87', 'bgcolor-025': '#005faf', 'bgcolor-026': '#005fdf',
'bgcolor-027': '#005fff', 'bgcolor-028': '#008700', 'bgcolor-029': '#00875f',
'bgcolor-030': '#008787', 'bgcolor-031': '#0087af', 'bgcolor-032': '#0087df',
'bgcolor-033': '#0087ff', 'bgcolor-034': '#00af00', 'bgcolor-035': '#00af5f',
'bgcolor-036': '#00af87', 'bgcolor-037': '#00afaf', 'bgcolor-038': '#00afdf',
'bgcolor-039': '#00afff', 'bgcolor-040': '#00df00', 'bgcolor-041': '#00df5f',
'bgcolor-042': '#00df87', 'bgcolor-043': '#00dfaf', 'bgcolor-044': '#00dfdf',
'bgcolor-045': '#00dfff', 'bgcolor-046': '#00ff00', 'bgcolor-047': '#00ff5f',
'bgcolor-048': '#00ff87', 'bgcolor-049': '#00ffaf', 'bgcolor-050': '#00ffdf',
'bgcolor-051': '#00ffff', 'bgcolor-052': '#5f0000', 'bgcolor-053': '#5f005f',
'bgcolor-054': '#5f0087', 'bgcolor-055': '#5f00af', 'bgcolor-056': '#5f00df',
'bgcolor-057': '#5f00ff', 'bgcolor-058': '#5f5f00', 'bgcolor-059': '#5f5f5f',
'bgcolor-060': '#5f5f87', 'bgcolor-061': '#5f5faf', 'bgcolor-062': '#5f5fdf',
'bgcolor-063': '#5f5fff', 'bgcolor-064': '#5f8700', 'bgcolor-065': '#5f875f',
'bgcolor-066': '#5f8787', 'bgcolor-067': '#5f87af', 'bgcolor-068': '#5f87df',
'bgcolor-069': '#5f87ff', 'bgcolor-070': '#5faf00', 'bgcolor-071': '#5faf5f',
'bgcolor-072': '#5faf87', 'bgcolor-073': '#5fafaf', 'bgcolor-074': '#5fafdf',
'bgcolor-075': '#5fafff', 'bgcolor-076': '#5fdf00', 'bgcolor-077': '#5fdf5f',
'bgcolor-078': '#5fdf87', 'bgcolor-079': '#5fdfaf', 'bgcolor-080': '#5fdfdf',
'bgcolor-081': '#5fdfff', 'bgcolor-082': '#5fff00', 'bgcolor-083': '#5fff5f',
'bgcolor-084': '#5fff87', 'bgcolor-085': '#5fffaf', 'bgcolor-086': '#5fffdf',
'bgcolor-087': '#5fffff', 'bgcolor-088': '#870000', 'bgcolor-089': '#87005f',
'bgcolor-090': '#870087', 'bgcolor-091': '#8700af', 'bgcolor-092': '#8700df',
'bgcolor-093': '#8700ff', 'bgcolor-094': '#875f00', 'bgcolor-095': '#875f5f',
'bgcolor-096': '#875f87', 'bgcolor-097': '#875faf', 'bgcolor-098': '#875fdf',
'bgcolor-099': '#875fff', 'bgcolor-100': '#878700', 'bgcolor-101': '#87875f',
'bgcolor-102': '#878787', 'bgcolor-103': '#8787af', 'bgcolor-104': '#8787df',
'bgcolor-105': '#8787ff', 'bgcolor-106': '#87af00', 'bgcolor-107': '#87af5f',
'bgcolor-108': '#87af87', 'bgcolor-109': '#87afaf', 'bgcolor-110': '#87afdf',
'bgcolor-111': '#87afff', 'bgcolor-112': '#87df00', 'bgcolor-113': '#87df5f',
'bgcolor-114': '#87df87', 'bgcolor-115': '#87dfaf', 'bgcolor-116': '#87dfdf',
'bgcolor-117': '#87dfff', 'bgcolor-118': '#87ff00', 'bgcolor-119': '#87ff5f',
'bgcolor-120': '#87ff87', 'bgcolor-121': '#87ffaf', 'bgcolor-122': '#87ffdf',
'bgcolor-123': '#87ffff', 'bgcolor-124': '#af0000', 'bgcolor-125': '#af005f',
'bgcolor-126': '#af0087', 'bgcolor-127': '#af00af', 'bgcolor-128': '#af00df',
'bgcolor-129': '#af00ff', 'bgcolor-130': '#af5f00', 'bgcolor-131': '#af5f5f',
'bgcolor-132': '#af5f87', 'bgcolor-133': '#af5faf', 'bgcolor-134': '#af5fdf',
'bgcolor-135': '#af5fff', 'bgcolor-136': '#af8700', 'bgcolor-137': '#af875f',
'bgcolor-138': '#af8787', 'bgcolor-139': '#af87af', 'bgcolor-140': '#af87df',
'bgcolor-141': '#af87ff', 'bgcolor-142': '#afaf00', 'bgcolor-143': '#afaf5f',
'bgcolor-144': '#afaf87', 'bgcolor-145': '#afafaf', 'bgcolor-146': '#afafdf',
'bgcolor-147': '#afafff', 'bgcolor-148': '#afdf00', 'bgcolor-149': '#afdf5f',
'bgcolor-150': '#afdf87', 'bgcolor-151': '#afdfaf', 'bgcolor-152': '#afdfdf',
'bgcolor-153': '#afdfff', 'bgcolor-154': '#afff00', 'bgcolor-155': '#afff5f',
'bgcolor-156': '#afff87', 'bgcolor-157': '#afffaf', 'bgcolor-158': '#afffdf',
'bgcolor-159': '#afffff', 'bgcolor-160': '#df0000', 'bgcolor-161': '#df005f',
'bgcolor-162': '#df0087', 'bgcolor-163': '#df00af', 'bgcolor-164': '#df00df',
'bgcolor-165': '#df00ff', 'bgcolor-166': '#df5f00', 'bgcolor-167': '#df5f5f',
'bgcolor-168': '#df5f87', 'bgcolor-169': '#df5faf', 'bgcolor-170': '#df5fdf',
'bgcolor-171': '#df5fff', 'bgcolor-172': '#df8700', 'bgcolor-173': '#df875f',
'bgcolor-174': '#df8787', 'bgcolor-175': '#df87af', 'bgcolor-176': '#df87df',
'bgcolor-177': '#df87ff', 'bgcolor-178': '#dfaf00', 'bgcolor-179': '#dfaf5f',
'bgcolor-180': '#dfaf87', 'bgcolor-181': '#dfafaf', 'bgcolor-182': '#dfafdf',
'bgcolor-183': '#dfafff', 'bgcolor-184': '#dfdf00', 'bgcolor-185': '#dfdf5f',
'bgcolor-186': '#dfdf87', 'bgcolor-187': '#dfdfaf', 'bgcolor-188': '#dfdfdf',
'bgcolor-189': '#dfdfff', 'bgcolor-190': '#dfff00', 'bgcolor-191': '#dfff5f',
'bgcolor-192': '#dfff87', 'bgcolor-193': '#dfffaf', 'bgcolor-194': '#dfffdf',
'bgcolor-195': '#dfffff', 'bgcolor-196': '#ff0000', 'bgcolor-197': '#ff005f',
'bgcolor-198': '#ff0087', 'bgcolor-199': '#ff00af', 'bgcolor-200': '#ff00df',
'bgcolor-201': '#ff00ff', 'bgcolor-202': '#ff5f00', 'bgcolor-203': '#ff5f5f',
'bgcolor-204': '#ff5f87', 'bgcolor-205': '#ff5faf', 'bgcolor-206': '#ff5fdf',
'bgcolor-207': '#ff5fff', 'bgcolor-208': '#ff8700', 'bgcolor-209': '#ff875f',
'bgcolor-210': '#ff8787', 'bgcolor-211': '#ff87af', 'bgcolor-212': '#ff87df',
'bgcolor-213': '#ff87ff', 'bgcolor-214': '#ffaf00', 'bgcolor-215': '#ffaf5f',
'bgcolor-216': '#ffaf87', 'bgcolor-217': '#ffafaf', 'bgcolor-218': '#ffafdf',
'bgcolor-219': '#ffafff', 'bgcolor-220': '#ffdf00', 'bgcolor-221': '#ffdf5f',
'bgcolor-222': '#ffdf87', 'bgcolor-223': '#ffdfaf', 'bgcolor-224': '#ffdfdf',
'bgcolor-225': '#ffdfff', 'bgcolor-226': '#ffff00', 'bgcolor-227': '#ffff5f',
'bgcolor-228': '#ffff87', 'bgcolor-229': '#ffffaf', 'bgcolor-230': '#ffffdf',
'bgcolor-231': '#ffffff', 'bgcolor-232': '#080808', 'bgcolor-233': '#121212',
'bgcolor-234': '#1c1c1c', 'bgcolor-235': '#262626', 'bgcolor-236': '#303030',
'bgcolor-237': '#3a3a3a', 'bgcolor-238': '#444444', 'bgcolor-239': '#4e4e4e',
'bgcolor-240': '#585858', 'bgcolor-241': '#606060', 'bgcolor-242': '#666666',
'bgcolor-243': '#767676', 'bgcolor-244': '#808080', 'bgcolor-245': '#8a8a8a',
'bgcolor-246': '#949494', 'bgcolor-247': '#9e9e9e', 'bgcolor-248': '#a8a8a8',
'bgcolor-249': '#b2b2b2', 'bgcolor-250': '#bcbcbc', 'bgcolor-251': '#c6c6c6',
'bgcolor-252': '#d0d0d0', 'bgcolor-253': '#dadada', 'bgcolor-254': '#e4e4e4',
'bgcolor-255': '#eeeeee'
}
"""
The classes below exist to properly encapsulate text and other tag classes
because the order of how tags are opened and closed are important to display in godot.
"""
class RootTag:
"""
The Root tag class made to contain other tags.
"""
__slots__ = ('child',)
def __init__(self):
self.child = None
def __str__(self):
return str(self.child) if self.child else ""
class ChildTag:
"""
A node made to be contained.
"""
def __init__(self, parent):
self.parent = parent
if parent:
parent.child = self
def set_parent(self, parent):
self.parent = parent
if parent:
parent.child = self
class TextTag(ChildTag):
"""
A BBCodeTag node to output regular text.
Output: SomeText
"""
__slots__ = ('parent', 'child', 'text')
def __init__(self, parent, text):
super().__init__(parent)
self.text = text
self.child = None
def __str__(self):
return f"{self.text}{self.child or ''}"
class BBCodeTag(ChildTag):
"""
Base BBCodeTag node to encapsulate and be encapsulated.
"""
__slots__ = ('parent', 'child',)
code = ''
def __init__(self, parent):
super().__init__(parent)
self.child = None
def __str__(self):
return f"[{self.code}]{self.child or ''}[/{self.code}]"
class UnderlineTag(BBCodeTag):
"""
A BBCodeTag node for underlined text.
Output: [u]Underlined Text[/u]
"""
code = 'u'
class BlinkTag(BBCodeTag):
"""
A BBCodeTag node for blinking text.
Output: [blink]Blinking Text[/blink]
"""
code = 'blink'
class ColorTag(BBCodeTag):
"""
A BBCodeTag node for foreground color.
Output: [fgcolor=#000000]Colorized Text[/fgcolor]
"""
__slots__ = ('parent', 'child', 'color_hex',)
code = 'color'
def __init__(self, parent, color_hex):
super().__init__(parent)
self.color_hex = color_hex
def __str__(self):
return f"[{self.code}={self.color_hex}]{self.child or ''}[/{self.code}]"
class BGColorTag(ColorTag):
"""
A BBCodeTag node for background color.
Output: [bgcolor=#000000]Colorized Text[/bgcolor]
"""
code = 'bgcolor'
class UrlTag(BBCodeTag):
"""
A BBCodeTag node used for urls.
Output: [url=www.example.com]Child Text[/url]
"""
__slots__ = ('parent', 'child', 'url_data',)
code = 'url'
def __init__(self, parent, url_data=''):
super().__init__(parent)
self.url_data = url_data
def __str__(self):
return f"[{self.code}={self.url_data}]{self.child or ''}[/{self.code}]"
class TextToBBCODEparser(TextToHTMLparser):
"""
This class describes a parser for converting from ANSI to BBCode.
It inherits from the TextToHTMLParser and overrides the specifics for bbcode.
"""
def convert_urls(self, text):
"""
Converts urls within text to bbcode style
Args:
text (str): Text to parse
Returns:
text (str): Processed text
"""
# Converts to bbcode styled urls
return self.re_url.sub(r'[url=\1]\1[/url]\2', text)
def sub_mxp_links(self, match):
"""
Helper method to be passed to re.sub,
replaces MXP links with bbcode.
Args:
match (re.Matchobject): Match for substitution.
Returns:
text (str): Processed text.
"""
cmd, text = [grp.replace('"', "\\"") for grp in match.groups()]
val = f"[mxp=send cmd={cmd}]{text}[/mxp]"
return val
def sub_mxp_urls(self, match):
"""
Helper method to be passed to re.sub,
replaces MXP links with bbcode.
Args:
match (re.Matchobject): Match for substitution.
Returns:
text (str): Processed text.
"""
url, text = [grp.replace('"', "\\"") for grp in match.groups()]
val = f"[url={url}]{text}[/url]"
return val
def sub_text(self, match):
"""
Helper method to be passed to re.sub,
for handling all substitutions.
Args:
match (re.Matchobject): Match for substitution.
Returns:
text (str): Processed text.
"""
cdict = match.groupdict()
if cdict["lineend"]:
return "\n"
return None
def format_styles(self, text):
"""
Takes a string with parsed ANSI codes and replaces them with bbcode style tags
Args:
text (str): The string to process.
Returns:
text (str): Processed text.
"""
# split out the ANSI codes and clean out any empty items
str_list = [substr for substr in self.re_style.split(text) if substr]
inverse = False
# default color is light grey - unhilite + white
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
# default bg is black
bg = ANSI_BACK_BLACK
previous_fg = None
previous_bg = None
blink = False
underline = False
new_style = False
parts = []
root_tag = RootTag()
current_tag = root_tag
for i, substr in enumerate(str_list):
# reset all current styling
if substr == ANSI_NORMAL:
# close any existing span if necessary
parts.append(str(root_tag))
root_tag = RootTag()
current_tag = root_tag
# reset to defaults
inverse = False
hilight = ANSI_UNHILITE
fg = ANSI_WHITE
bg = ANSI_BACK_BLACK
previous_fg = None
previous_bg = None
blink = False
underline = False
new_style = False
# change color
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
# set new color
fg = substr
new_style = True
# change bg color
elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
# set new bg
bg = substr
new_style = True
# non-color codes
elif substr in self.style_codes:
new_style = True
# hilight codes
if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
# set new hilight status
hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE
# inversion codes
if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
inverse = True
# blink codes
if (
substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE)
and not blink
):
blink = True
current_tag = BlinkTag(current_tag)
# underline
if substr == ANSI_UNDERLINE and not underline:
underline = True
current_tag = UnderlineTag(current_tag)
else:
close_tags = False
color_tag = None
bgcolor_tag = None
# normal text, add text back to list
if new_style:
# prior entry was cleared, which means style change
# get indices for the fg and bg codes
bg_index = self.bglist.index(bg)
try:
color_index = self.colorlist.index(hilight + fg)
except ValueError:
# xterm256 colors don't have the hilight codes
color_index = self.colorlist.index(fg)
if inverse:
# inverse means swap fg and bg indices
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
else:
# use fg and bg indices for classes
bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
color_class = "color-{}".format(str(color_index).rjust(3, "0"))
# black bg is the default, don't explicitly style
if bg_class != "bgcolor-000":
color_hex = COLOR_INDICE_TO_HEX.get(bg_class)
bgcolor_tag = BGColorTag(None, color_hex=color_hex)
if previous_bg and previous_bg != color_hex:
close_tags = True
else:
previous_bg = color_hex
# light grey text is the default, don't explicitly style
if color_class != "color-007":
color_hex = COLOR_INDICE_TO_HEX.get(color_class)
color_tag = ColorTag(None, color_hex=color_hex)
if previous_fg and previous_fg != color_hex:
close_tags = True
else:
previous_fg = color_hex
new_tag = TextTag(None, substr)
if close_tags:
# Because the order is important, we need to close the tags and reopen those who shouldn't reset.
new_style = False
parts.append(str(root_tag))
root_tag = RootTag()
current_tag = root_tag
if blink:
current_tag = BlinkTag(current_tag)
if underline:
current_tag = UnderlineTag(current_tag)
if bgcolor_tag:
bgcolor_tag.set_parent(current_tag)
current_tag = bgcolor_tag
if color_tag:
color_tag.set_parent(current_tag)
current_tag = color_tag
new_tag.set_parent(current_tag)
current_tag = new_tag
else:
if bgcolor_tag:
bgcolor_tag.set_parent(current_tag)
current_tag = bgcolor_tag
if color_tag:
color_tag.set_parent(current_tag)
current_tag = color_tag
new_tag.set_parent(current_tag)
current_tag = new_tag
any_text = self._get_text_tag(root_tag)
if any_text:
# Only append tags if text was added.
last_part = str(root_tag)
parts.append(last_part)
# recombine back into string
return "".join(parts)
def _get_text_tag(self, root):
child = root.child
while child:
if isinstance(child, TextTag):
return child
else:
child = child.child
return None
def parse(self, text, strip_ansi=False):
"""
Main access function, converts a text containing ANSI codes
into html statements.
Args:
text (str): Text to process.
strip_ansi (bool, optional):
Returns:
text (str): Parsed text.
"""
# parse everything to ansi first
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
# convert all ansi to html
result = re.sub(self.re_string, self.sub_text, text)
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
result = self.remove_bells(result)
result = self.format_styles(result)
result = self.remove_backspaces(result)
result = self.convert_urls(result)
return result
BBCODE_PARSER = TextToBBCODEparser()
#
# Access function
#
def parse_to_bbcode(string, strip_ansi=False, parser=BBCODE_PARSER):
"""
Parses a string, replace ANSI markup with bbcode
"""
return parser.parse(string, strip_ansi=strip_ansi)

View file

@ -0,0 +1,79 @@
"""
Godot Websocket - ChrisLR 2022
This file contains the code necessary to dedicate a port to communicate with Godot via Websockets.
It uses the plugin system and should be plugged via settings as detailed in the readme.
"""
import json
from autobahn.twisted import WebSocketServerFactory
from twisted.application import internet
from evennia import settings
from evennia.contrib.base_systems.godotwebsocket.text2bbcode import parse_to_bbcode
from evennia.server.portal import webclient
from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS
from evennia.settings_default import LOCKDOWN_MODE
class GodotWebSocketClient(webclient.WebSocketClient):
"""
Implements the server-side of the Websocket connection specific to Godot.
It inherits from the basic Websocket implementation and changes only what is necessary.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_key = "godotclient/websocket"
def send_text(self, *args, **kwargs):
"""
Send text data. This will pre-process the text for
color-replacement, conversion to bbcode etc.
Args:
text (str): Text to send.
Keyword Args:
options (dict): Options-dict with the following keys understood:
- nocolor (bool): Clean out all color.
- send_prompt (bool): Send a prompt with parsed bbcode
"""
if args:
args = list(args)
text = args[0]
if text is None:
return
else:
return
flags = self.protocol_flags
options = kwargs.pop("options", {})
nocolor = options.get("nocolor", flags.get("NOCOLOR", False))
prompt = options.get("send_prompt", False)
cmd = "prompt" if prompt else "text"
args[0] = parse_to_bbcode(text, strip_ansi=nocolor)
# send to client on required form [cmdname, args, kwargs]
self.sendLine(json.dumps([cmd, args, kwargs]))
def start_plugin_services(portal):
class GodotWebsocket(WebSocketServerFactory):
"Only here for better naming in logs"
pass
factory = GodotWebsocket()
factory.noisy = False
factory.protocol = GodotWebSocketClient
factory.sessionhandler = PORTAL_SESSIONS
interface = "127.0.0.1" if LOCKDOWN_MODE else settings.GODOT_CLIENT_WEBSOCKET_CLIENT_INTERFACE
port = settings.GODOT_CLIENT_WEBSOCKET_PORT
websocket_service = internet.TCPServer(port, factory, interface=interface)
websocket_service.setName("GodotWebSocket%s:%s" % (interface, port))
portal.services.addService(websocket_service)