Merge pull request #3405 from ChrisLR/godot-fix-portal-docs
[Godot] Fix Godot Contrib
This commit is contained in:
commit
958649c416
3 changed files with 181 additions and 203 deletions
|
|
@ -31,259 +31,166 @@ the extra data given from Evennia as needed.
|
||||||
|
|
||||||
This section assumes you have basic knowledge on how to use Godot.
|
This section assumes you have basic knowledge on how to use Godot.
|
||||||
You can read the following url for more details on Godot Websockets
|
You can read the following url for more details on Godot Websockets
|
||||||
and to implement a minimal client.
|
and to implement a minimal client or look at the full example at the bottom of this page.
|
||||||
|
|
||||||
https://docs.godotengine.org/en/stable/tutorials/networking/websocket.html
|
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
|
The rest of this document will be for Godot 4.
|
||||||
of this readme for Godot 4.
|
Note that some of the code shown here is partially taken from official Godot Documentation
|
||||||
|
|
||||||
|
A very basic setup in godot would require
|
||||||
|
|
||||||
|
- One RichTextLabel Node to display the Evennia Output, ensure bbcode is enabled on it.
|
||||||
|
- One Node for your websocket client code with a new Script attached.
|
||||||
|
- One TextEdit Node to enter commands
|
||||||
|
- One Button Node to press and send the commands
|
||||||
|
- Controls for the layout, in this example I have used
|
||||||
|
Panel
|
||||||
|
VBoxContainer
|
||||||
|
RichTextLabel
|
||||||
|
HBoxContainer
|
||||||
|
TextEdit
|
||||||
|
Button
|
||||||
|
|
||||||
|
I will not go over how layout works but the documentation for them is easily accessible in the godot docs.
|
||||||
|
|
||||||
|
|
||||||
At the top of the file you must change the url to point at your mud.
|
Open up the script for your client code.
|
||||||
|
|
||||||
|
We need to define the url leading to your mud, use the same values you have used in your Evennia Settings.
|
||||||
|
Next we write some basic code to get a connection going.
|
||||||
|
This will connect when the Scene is ready, poll and print the data when we receive it and close when the scene exits.
|
||||||
```
|
```
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
# The URL we will connect to
|
# The URL we will connect to.
|
||||||
export var websocket_url = "ws://localhost:4008"
|
var websocket_url = "ws://localhost:4008"
|
||||||
|
var socket := WebSocketPeer.new()
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
You must also remove the protocol from the `connect_to_url` call made
|
|
||||||
within the `_ready` function.
|
|
||||||
|
|
||||||
```
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# ...
|
if socket.connect_to_url(websocket_url) != OK:
|
||||||
# Change the following line from this
|
print("Unable to connect.")
|
||||||
var err = _client.connect_to_url(websocket_url, ["lws-mirror-protocol"])
|
set_process(false)
|
||||||
# To this
|
|
||||||
var err = _client.connect_to_url(websocket_url)
|
|
||||||
# ...
|
func _process(_delta):
|
||||||
|
socket.poll()
|
||||||
|
match socket.get_ready_state():
|
||||||
|
WebSocketPeer.STATE_OPEN:
|
||||||
|
while socket.get_available_packet_count():
|
||||||
|
print(socket.get_packet().get_string_from_ascii())
|
||||||
|
|
||||||
|
WebSocketPeer.STATE_CLOSED:
|
||||||
|
var code = socket.get_close_code()
|
||||||
|
var reason = socket.get_close_reason()
|
||||||
|
print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1])
|
||||||
|
set_process(false)
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
socket.close()
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will allow you to connect to your mud.
|
At this point, you can start your evennia server, run godot and it should print a default reply.
|
||||||
After that you need to properly handle the data sent by evennia.
|
After that you need to properly handle the data sent by evennia.
|
||||||
To do this, you should replace your `_on_data` method.
|
To do this, we will add a new function to dispatch the messages properly.
|
||||||
You will need to parse the JSON received to properly act on the data.
|
|
||||||
Here is an example
|
Here is an example
|
||||||
```
|
```
|
||||||
func _on_data():
|
func _handle_data(data):
|
||||||
# The following two lines will get us the data from Evennia.
|
print(data) # Print for debugging
|
||||||
var data = _client.get_peer(1).get_packet().get_string_from_utf8()
|
var data_array = JSON.parse_string(data)
|
||||||
var json_data = JSON.parse(data).result
|
# The first element can be used to see if its text
|
||||||
# The json_data is an array
|
if data_array[0] == 'text':
|
||||||
|
# The second element contains the messages
|
||||||
|
for msg in data_array[1]:
|
||||||
|
write_to_rtb(msg)
|
||||||
|
|
||||||
# The first element informs us this is simple text
|
func write_to_rtb(msg):
|
||||||
# so we add it to the RichTextlabel
|
output_label.append_text(msg)
|
||||||
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
|
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.
|
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.
|
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.
|
Since it is parsed BBCode, we can add that directly to a RichTextLabel by calling its append_text method.
|
||||||
|
|
||||||
If you want anything better than fancy text in Godot, you will have
|
If you want anything better than fancy text in Godot, you will have
|
||||||
to leverage Evennia's OOB to send extra data.
|
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).
|
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
|
Now to send data, we connect the Button pressed Signal to a method,
|
||||||
```python
|
read the label input and send it via the websocket, then clear the label.
|
||||||
caller.msg(coordinates=(9, 2))
|
```
|
||||||
|
func _on_button_pressed():
|
||||||
|
var msg = text_edit.text
|
||||||
|
var msg_arr = ['text', [msg], {}]
|
||||||
|
var msg_str = JSON.stringify(msg_arr)
|
||||||
|
socket.send_text(msg_str)
|
||||||
|
text_edit.text = ""
|
||||||
```
|
```
|
||||||
|
|
||||||
Godot
|
|
||||||
```
|
|
||||||
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
|
## Known Issues
|
||||||
|
|
||||||
- Sending SaverDicts and similar objects straight from Evennia .DB will cause issues,
|
- Sending SaverDicts and similar objects straight from Evennia .DB will cause issues,
|
||||||
cast them to dict() or list() before doing so.
|
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.
|
|
||||||
|
|
||||||
|
## Full Example Script
|
||||||
```
|
```
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
# The URL to connect to, should be your mud.
|
# The URL we will connect to.
|
||||||
export var websocket_url = "ws://127.0.0.1:4008"
|
var websocket_url = "ws://localhost:4008"
|
||||||
|
var socket := WebSocketPeer.new()
|
||||||
|
|
||||||
# These are references to controls in the scene
|
@onready var output_label = $"../Panel/VBoxContainer/RichTextLabel"
|
||||||
onready var parent = get_parent()
|
@onready var text_edit = $"../Panel/VBoxContainer/HBoxContainer/TextEdit"
|
||||||
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():
|
func _ready():
|
||||||
# Connect base signals to get notified of connection open, close, errors and messages
|
if socket.connect_to_url(websocket_url) != OK:
|
||||||
_client.connect("connection_closed", self, "_closed")
|
print("Unable to connect.")
|
||||||
_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)
|
set_process(false)
|
||||||
|
|
||||||
func _closed(was_clean = false):
|
func _process(_delta):
|
||||||
# was_clean will tell you if the disconnection was correctly notified
|
socket.poll()
|
||||||
# by the remote peer before closing the socket.
|
match socket.get_ready_state():
|
||||||
print("Closed, clean: ", was_clean)
|
WebSocketPeer.STATE_OPEN:
|
||||||
set_process(false)
|
while socket.get_available_packet_count():
|
||||||
|
var data = socket.get_packet().get_string_from_ascii()
|
||||||
|
_handle_data(data)
|
||||||
|
|
||||||
|
WebSocketPeer.STATE_CLOSED:
|
||||||
|
var code = socket.get_close_code()
|
||||||
|
var reason = socket.get_close_reason()
|
||||||
|
print("WebSocket closed with code: %d, reason %s. Clean: %s" % [code, reason, code != -1])
|
||||||
|
set_process(false)
|
||||||
|
|
||||||
func _connected(proto = ""):
|
func _handle_data(data):
|
||||||
is_connected = true
|
print(data) # Print for debugging
|
||||||
print("Connected with protocol: ", proto)
|
var data_array = JSON.parse_string(data)
|
||||||
|
# The first element can be used to see if its text
|
||||||
|
if data_array[0] == 'text':
|
||||||
|
# The second element contains the messages
|
||||||
|
for msg in data_array[1]:
|
||||||
|
write_to_rtb(msg)
|
||||||
|
|
||||||
func _on_data():
|
func write_to_rtb(msg):
|
||||||
# This is called when Godot receives data from evennia
|
output_label.append_text(msg)
|
||||||
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.
|
|
||||||
|
|
||||||
```
|
|
||||||
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():
|
func _on_button_pressed():
|
||||||
# This is called when we press the button in the scene
|
var msg = text_edit.text
|
||||||
# with a connected signal, it sends the written message to Evennia.
|
|
||||||
var msg = txtEdit.text
|
|
||||||
var msg_arr = ['text', [msg], {}]
|
var msg_arr = ['text', [msg], {}]
|
||||||
var msg_str = JSON.stringify(msg_arr)
|
var msg_str = JSON.stringify(msg_arr)
|
||||||
websocket.send(msg_str)
|
socket.send_text(msg_str)
|
||||||
|
text_edit.text = ""
|
||||||
|
|
||||||
```
|
func _exit_tree():
|
||||||
|
socket.close()
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
from evennia.contrib.base_systems.godotwebsocket.webclient import start_plugin_services
|
||||||
|
from evennia.server.portal.amp_server import AMPServerFactory
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.utils.unittest import TestCase
|
||||||
|
except ImportError:
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.utils import unittest
|
||||||
|
except ImportError:
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from mock import MagicMock, Mock
|
||||||
|
from twisted.internet.base import DelayedCall
|
||||||
|
from twisted.test import proto_helpers
|
||||||
|
|
||||||
|
import evennia
|
||||||
|
from evennia.server.portal.portalsessionhandler import PortalSessionHandler
|
||||||
|
from evennia.server.portal.service import EvenniaPortalService
|
||||||
|
from evennia.utils.test_resources import BaseEvenniaTest
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
|
||||||
|
class TestGodotWebSocketClient(BaseEvenniaTest):
|
||||||
|
@override_settings(GODOT_CLIENT_WEBSOCKET_CLIENT_INTERFACE="127.0.0.1", GODOT_CLIENT_WEBSOCKET_PORT='8988')
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.portal = EvenniaPortalService()
|
||||||
|
evennia.EVENNIA_PORTAL_SERVICE = self.portal
|
||||||
|
self.amp_server_factory = AMPServerFactory(self.portal)
|
||||||
|
self.amp_server = self.amp_server_factory.buildProtocol("127.0.0.1")
|
||||||
|
start_plugin_services(self.portal)
|
||||||
|
godot_ws_service = next(srv for srv in self.portal.services if srv.name.startswith('GodotWebSocket'))
|
||||||
|
|
||||||
|
factory = godot_ws_service.args[1]
|
||||||
|
self.proto = factory.protocol()
|
||||||
|
self.proto.factory = factory
|
||||||
|
|
||||||
|
evennia.PORTAL_SESSION_HANDLER = PortalSessionHandler()
|
||||||
|
self.proto.factory.sessionhandler = evennia.PORTAL_SESSION_HANDLER
|
||||||
|
self.proto.sessionhandler = evennia.PORTAL_SESSION_HANDLER
|
||||||
|
self.proto.sessionhandler.portal = Mock()
|
||||||
|
self.proto.transport = proto_helpers.StringTransport()
|
||||||
|
# self.proto.transport = proto_helpers.FakeDatagramTransport()
|
||||||
|
self.proto.transport.client = ["localhost"]
|
||||||
|
self.proto.transport.setTcpKeepAlive = Mock()
|
||||||
|
self.proto.state = MagicMock()
|
||||||
|
self.addCleanup(self.proto.factory.sessionhandler.disconnect_all)
|
||||||
|
DelayedCall.debug = True
|
||||||
|
|
||||||
|
@mock.patch("evennia.server.portal.portalsessionhandler.reactor", new=MagicMock())
|
||||||
|
def test_data_in(self):
|
||||||
|
self.proto.sessionhandler.data_in = MagicMock()
|
||||||
|
self.proto.onOpen()
|
||||||
|
msg = json.dumps(["logged_in", (), {}]).encode()
|
||||||
|
self.proto.onMessage(msg, isBinary=False)
|
||||||
|
self.proto.sessionhandler.data_in.assert_called_with(self.proto, logged_in=[[], {}])
|
||||||
|
msg = json.dumps(["text", ("|rRed Text|n",), {}]).encode()
|
||||||
|
self.proto.onMessage(msg, isBinary=False)
|
||||||
|
self.proto.sessionhandler.data_in.assert_called_with(self.proto, text=[["|rRed Text|n"], {}])
|
||||||
|
|
||||||
|
@mock.patch("evennia.server.portal.portalsessionhandler.reactor", new=MagicMock())
|
||||||
|
def test_data_out(self):
|
||||||
|
self.proto.onOpen()
|
||||||
|
self.proto.sendLine = MagicMock()
|
||||||
|
self.proto.sessionhandler.data_out(self.proto, text=[["|rRed Text|n"], {}])
|
||||||
|
self.proto.sendLine.assert_called_with(json.dumps(["text", ["[color=#ff0000]Red Text[/color]"], {}]))
|
||||||
|
|
@ -78,4 +78,4 @@ def start_plugin_services(portal):
|
||||||
port = settings.GODOT_CLIENT_WEBSOCKET_PORT
|
port = settings.GODOT_CLIENT_WEBSOCKET_PORT
|
||||||
websocket_service = internet.TCPServer(port, factory, interface=interface)
|
websocket_service = internet.TCPServer(port, factory, interface=interface)
|
||||||
websocket_service.setName("GodotWebSocket%s:%s" % (interface, port))
|
websocket_service.setName("GodotWebSocket%s:%s" % (interface, port))
|
||||||
portal.services.addService(websocket_service)
|
portal.addService(websocket_service)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue