A rather alarming amount of work for one day.
This commit is contained in:
commit
f73902bef2
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
__pycache__
|
||||
|
51
Control.gd
Normal file
51
Control.gd
Normal file
@ -0,0 +1,51 @@
|
||||
extends Control
|
||||
|
||||
#var packet_socket_server : KiriPacketSocket
|
||||
var wrapper_instance : KiriPythonWrapperInstance
|
||||
|
||||
#var active_connections = []
|
||||
|
||||
func _ready():
|
||||
|
||||
var jrpc : KiriJSONRPC = KiriJSONRPC.new()
|
||||
var x = jrpc.make_request("make_request", [ "bar", "wharrgarbl", 123 ], 1)
|
||||
#var x = jrpc.make_request("can_translate_message", [], 1)
|
||||
print(x)
|
||||
|
||||
var c = jrpc.process_action_safer(x)
|
||||
print("process action: ", c)
|
||||
|
||||
#packet_socket_server = KiriPacketSocket.new()
|
||||
##packet_socket_server.start_server(["127.0.0.1", 9506])
|
||||
#packet_socket_server.start_client(["127.0.0.1", 9506])
|
||||
#active_connections.append(packet_socket_server)
|
||||
|
||||
wrapper_instance = KiriPythonWrapperInstance.new(
|
||||
"/storage/git2/GodotJSONRPCTest/addons/kiripythonrpcwrapper/KiriPythonRPCWrapper/test_module/__init__.py")
|
||||
|
||||
wrapper_instance.start_process()
|
||||
|
||||
func _process(_delta):
|
||||
pass
|
||||
|
||||
#var next_connection = packet_socket_server.get_next_server_connection()
|
||||
#if next_connection:
|
||||
#active_connections.append(next_connection)
|
||||
#print("GOT A NEW CONNECTION!")
|
||||
|
||||
#for conn in active_connections:
|
||||
#var p = conn.get_next_packet()
|
||||
#while p:
|
||||
#print("PACKET: ", p)
|
||||
#conn.send_packet(p)
|
||||
#p = conn.get_next_packet()
|
||||
|
||||
wrapper_instance.poll()
|
||||
#print(wrapper_instance.get_status())
|
||||
|
||||
wrapper_instance.call_rpc_async("func_to_call", [12345], func(stuff):
|
||||
print(stuff.response)
|
||||
)
|
||||
|
||||
#wrapper_instance.call_rpc_sync("func_to_call", [12345])
|
||||
|
50
KiriJSONRPC.gd
Normal file
50
KiriJSONRPC.gd
Normal file
@ -0,0 +1,50 @@
|
||||
# KiriJSONRPC
|
||||
#
|
||||
# This just wraps JSONRPC and adds a little more sanity-checking, like
|
||||
# preventing the JSONRPC's own methods from being called by an RPC.
|
||||
|
||||
extends JSONRPC
|
||||
class_name KiriJSONRPC
|
||||
|
||||
func process_action_safer(action: Variant, recurse: bool = false) -> Variant:
|
||||
|
||||
if action is String:
|
||||
action = JSON.parse_string(action)
|
||||
|
||||
# Do some basic type sanity checking.
|
||||
var invalid_request : bool = false
|
||||
if not (action is Dictionary):
|
||||
invalid_request = true
|
||||
elif not ("method" in action):
|
||||
invalid_request = true
|
||||
elif not (action["method"] is String):
|
||||
invalid_request = true
|
||||
|
||||
if invalid_request:
|
||||
return make_response_error(
|
||||
JSONRPC.INVALID_REQUEST,
|
||||
"Invalid request")
|
||||
|
||||
# Exclude JSONRPC's own built-in methods. Why the heck are these allowed
|
||||
# here?
|
||||
var method_name = action["method"]
|
||||
var id = action["id"]
|
||||
var bad_message_names = [
|
||||
"make_notification",
|
||||
"make_request",
|
||||
"make_response",
|
||||
"make_response_error",
|
||||
"process_action",
|
||||
"process_string",
|
||||
"set_scope"
|
||||
]
|
||||
|
||||
if method_name in bad_message_names:
|
||||
return make_response_error(
|
||||
JSONRPC.METHOD_NOT_FOUND,
|
||||
"Method not found: " + method_name,
|
||||
id)
|
||||
|
||||
return process_action(action, recurse)
|
||||
|
||||
|
256
addons/kiripythonrpcwrapper/KiriPacketSocket/KiriPacketSocket.gd
Normal file
256
addons/kiripythonrpcwrapper/KiriPacketSocket/KiriPacketSocket.gd
Normal file
@ -0,0 +1,256 @@
|
||||
extends RefCounted
|
||||
class_name KiriPacketSocket
|
||||
|
||||
var _should_quit : bool = false
|
||||
var _packet_buffer : KiriPacketBuffer = KiriPacketBuffer.new()
|
||||
var _state : KiriSocketState = KiriSocketState.DISCONNECTED
|
||||
var _outgoing_packet_queue : Array = []
|
||||
|
||||
var _state_lock : Mutex = Mutex.new()
|
||||
var _worker_thread : Thread = null
|
||||
|
||||
var _new_connections_to_server : Array = []
|
||||
var _error_string : String = ""
|
||||
|
||||
# This class exists in __init__.py, ported as the original Python. Any
|
||||
# functional changes to this should be reflected in that implementation as well.
|
||||
class KiriPacketBuffer:
|
||||
var _receive_buffer : PackedByteArray = []
|
||||
var _packet_buffer : Array = []
|
||||
|
||||
func _grab_complete_packets():
|
||||
while len(_receive_buffer) >= 4:
|
||||
|
||||
var next_packet_size : int = \
|
||||
_receive_buffer[0] | \
|
||||
(_receive_buffer[1] << 8) | \
|
||||
(_receive_buffer[2] << 16) | \
|
||||
(_receive_buffer[3] << 24)
|
||||
|
||||
if len(_receive_buffer) >= 4 + next_packet_size:
|
||||
var next_packet = _receive_buffer.slice(4, 4 + next_packet_size)
|
||||
assert(len(next_packet) == next_packet_size)
|
||||
_receive_buffer = _receive_buffer.slice(4 + len(next_packet))
|
||||
_packet_buffer.append(next_packet)
|
||||
|
||||
else:
|
||||
|
||||
break
|
||||
|
||||
func _have_complete_packet():
|
||||
_grab_complete_packets()
|
||||
return len(_packet_buffer) > 0
|
||||
|
||||
func get_next_packet():
|
||||
if not _have_complete_packet():
|
||||
return null
|
||||
return _packet_buffer.pop_front()
|
||||
|
||||
func add_bytes(incoming_bytes : PackedByteArray):
|
||||
_receive_buffer.append_array(incoming_bytes)
|
||||
|
||||
enum KiriSocketState {
|
||||
DISCONNECTED = 0,
|
||||
CONNECTING = 1,
|
||||
CONNECTED = 2,
|
||||
SERVER_STARTING = 3,
|
||||
SERVER_LISTENING = 4,
|
||||
ERROR = 5
|
||||
}
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
assert(not _worker_thread)
|
||||
|
||||
func send_packet(packet_bytes : PackedByteArray):
|
||||
assert(packet_bytes)
|
||||
_state_lock.lock()
|
||||
_outgoing_packet_queue.append(packet_bytes)
|
||||
_state_lock.unlock()
|
||||
|
||||
func get_next_packet():
|
||||
_state_lock.lock()
|
||||
var ret = _packet_buffer.get_next_packet()
|
||||
_state_lock.unlock()
|
||||
return ret
|
||||
|
||||
func get_next_server_connection():
|
||||
_state_lock.lock()
|
||||
var ret = null
|
||||
if len(_new_connections_to_server) > 0:
|
||||
ret = _new_connections_to_server.pop_front()
|
||||
_state_lock.unlock()
|
||||
return ret
|
||||
|
||||
func get_last_error():
|
||||
_state_lock.lock()
|
||||
var ret = _error_string
|
||||
_state_lock.unlock()
|
||||
return ret
|
||||
|
||||
func is_disconnected_or_error():
|
||||
_state_lock.lock()
|
||||
var bad_states = [
|
||||
KiriSocketState.DISCONNECTED,
|
||||
KiriSocketState.ERROR
|
||||
]
|
||||
|
||||
var ret : bool = false
|
||||
if _state in bad_states:
|
||||
ret = true
|
||||
|
||||
_state_lock.unlock()
|
||||
|
||||
return ret
|
||||
|
||||
func get_state():
|
||||
_state_lock.lock()
|
||||
var ret = _state
|
||||
_state_lock.unlock()
|
||||
return ret
|
||||
|
||||
func start_server(address):
|
||||
|
||||
_set_state(KiriSocketState.SERVER_STARTING)
|
||||
|
||||
assert(not _worker_thread)
|
||||
_worker_thread = Thread.new()
|
||||
_worker_thread.start(_server_thread_func.bind(address))
|
||||
|
||||
func start_client(address):
|
||||
|
||||
_set_state(KiriSocketState.CONNECTING)
|
||||
|
||||
assert(not _worker_thread)
|
||||
|
||||
_worker_thread = Thread.new()
|
||||
_worker_thread.start(_client_thread_func.bind(address))
|
||||
|
||||
func stop():
|
||||
|
||||
assert(_worker_thread)
|
||||
_should_quit = true
|
||||
_worker_thread.wait_to_finish()
|
||||
_worker_thread = null
|
||||
_should_quit = false
|
||||
|
||||
func is_running():
|
||||
return not (_worker_thread == null)
|
||||
|
||||
func _normal_communication_loop(sock : StreamPeer, address):
|
||||
|
||||
while not _should_quit:
|
||||
|
||||
if sock.poll() != OK:
|
||||
break
|
||||
|
||||
if sock.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
||||
break
|
||||
|
||||
# Get new data.
|
||||
_state_lock.lock()
|
||||
var available_bytes = sock.get_available_bytes()
|
||||
if available_bytes > 0:
|
||||
var incoming_bytes = sock.get_data(available_bytes)
|
||||
_packet_buffer.add_bytes(PackedByteArray(incoming_bytes[1]))
|
||||
if incoming_bytes[0] != OK:
|
||||
break
|
||||
_state_lock.unlock()
|
||||
|
||||
# Send all packets from queue.
|
||||
_state_lock.lock()
|
||||
while len(self._outgoing_packet_queue):
|
||||
var next_outgoing_packet = _outgoing_packet_queue.pop_front()
|
||||
var len_to_send = len(next_outgoing_packet)
|
||||
sock.put_u8((len_to_send & 0x000000ff) >> 0)
|
||||
sock.put_u8((len_to_send & 0x0000ff00) >> 8)
|
||||
sock.put_u8((len_to_send & 0x00ff0000) >> 16)
|
||||
sock.put_u8((len_to_send & 0xff000000) >> 24)
|
||||
sock.put_data(next_outgoing_packet)
|
||||
_state_lock.unlock()
|
||||
|
||||
OS.delay_usec(1)
|
||||
|
||||
func _client_thread_func(address):
|
||||
|
||||
var sock : StreamPeerTCP = StreamPeerTCP.new()
|
||||
|
||||
# Connect to the server.
|
||||
_set_state(KiriSocketState.CONNECTING)
|
||||
var connect_err = sock.connect_to_host(address[0], address[1])
|
||||
|
||||
if connect_err == OK:
|
||||
_set_state(KiriSocketState.CONNECTED)
|
||||
|
||||
_normal_communication_loop(sock, address)
|
||||
|
||||
# We are now disconnected.
|
||||
_set_state(KiriSocketState.DISCONNECTED)
|
||||
sock.disconnect_from_host()
|
||||
|
||||
else:
|
||||
_set_state(KiriSocketState.ERROR, "Connection failed")
|
||||
|
||||
func _set_state(state : KiriSocketState, error_string=null):
|
||||
_state_lock.lock()
|
||||
_state = state
|
||||
|
||||
if _state == KiriSocketState.ERROR:
|
||||
assert(error_string)
|
||||
_error_string = error_string
|
||||
else:
|
||||
assert(not error_string)
|
||||
_error_string = ""
|
||||
|
||||
_state_lock.unlock()
|
||||
|
||||
func _server_to_client_thread_func(connection : StreamPeerTCP, address):
|
||||
|
||||
_set_state(KiriSocketState.CONNECTED)
|
||||
_normal_communication_loop(connection, address)
|
||||
|
||||
# FIXME: Missing some error handling here due to exception differences
|
||||
# between Python and GDScript.
|
||||
|
||||
# Only switch to "disconnected" if we were most recently
|
||||
# connected, otherwise we could mask an error.
|
||||
if get_state() == KiriSocketState.CONNECTED:
|
||||
_set_state(KiriSocketState.DISCONNECTED)
|
||||
|
||||
func _server_thread_func(address):
|
||||
|
||||
while not _should_quit:
|
||||
|
||||
var sock : TCPServer = TCPServer.new()
|
||||
|
||||
var listen_err = sock.listen(address[1], address[0])
|
||||
|
||||
if listen_err != OK:
|
||||
|
||||
# FIXME: I wonder if we should do this in the main
|
||||
# thread so we can get the exceptions back up to
|
||||
# the start_server function and up from there.
|
||||
_set_state(KiriSocketState.ERROR, "Could not listen on port.")
|
||||
break
|
||||
|
||||
_set_state(KiriSocketState.SERVER_LISTENING)
|
||||
|
||||
while not _should_quit:
|
||||
|
||||
if sock.is_connection_available():
|
||||
var connection : StreamPeerTCP = sock.take_connection()
|
||||
var new_client : KiriPacketSocket = KiriPacketSocket.new()
|
||||
new_client._start_client_connection_from_server(connection, address)
|
||||
_state_lock.lock()
|
||||
_new_connections_to_server.append(new_client)
|
||||
_state_lock.unlock()
|
||||
|
||||
OS.delay_usec(1)
|
||||
|
||||
func _start_client_connection_from_server(connection : StreamPeerTCP, address):
|
||||
|
||||
assert(not _worker_thread)
|
||||
_worker_thread = Thread.new()
|
||||
_worker_thread.start(_server_to_client_thread_func.bind(connection, address))
|
||||
|
||||
|
20
addons/kiripythonrpcwrapper/KiriPacketSocket/LICENSE.md
Normal file
20
addons/kiripythonrpcwrapper/KiriPacketSocket/LICENSE.md
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright © 2024 Kiri Jolly
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
“Software”), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
14
addons/kiripythonrpcwrapper/KiriPacketSocket/README.md
Normal file
14
addons/kiripythonrpcwrapper/KiriPacketSocket/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# KiriPacketSocket
|
||||
|
||||
TODO - explanation
|
||||
|
||||
## Python setup instructions and use
|
||||
|
||||
TODO - setup and a simple tutorial
|
||||
TODO - embed example script
|
||||
|
||||
## Godot setup instructions and use
|
||||
|
||||
TODO - setup and a simple tutorial
|
||||
TODO - embed example project (remember to add .gdignore)
|
||||
|
418
addons/kiripythonrpcwrapper/KiriPacketSocket/__init__.py
Normal file
418
addons/kiripythonrpcwrapper/KiriPacketSocket/__init__.py
Normal file
@ -0,0 +1,418 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Copyright © 2024 Kiri Jolly
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the “Software”), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import enum
|
||||
|
||||
class PacketSocket:
|
||||
"""Socket wrapper that encapsulates its own packet protocol and
|
||||
manages its own threads. Polling-style interface.
|
||||
|
||||
Packets are simply binary blobs with a little-endian 32-bit
|
||||
unsigned integer size prepended to them. Input data will be split
|
||||
into individual packets based on these.
|
||||
|
||||
TCP/IP only.
|
||||
|
||||
Main public API:
|
||||
|
||||
- start_client(address) - Connect to an open port.
|
||||
|
||||
- start_server(address) - Start a server with an open port. This
|
||||
will start receiving connections, which much be accepted with
|
||||
get_next_server_connection().
|
||||
|
||||
- get_next_server_connection() - Get the next incoming connection
|
||||
(returns None if nothing is currently connecting, or a new
|
||||
PacketSocket that can actually receive packets, otherwise).
|
||||
Polling interface.
|
||||
|
||||
- send_packet(b) - Sends a byte array (b) as a packet. This adds
|
||||
it to a queue which is send in its own thread.
|
||||
|
||||
- get_next_packet() - Gets the next complete incoming packet as a
|
||||
byte array or None if no packet yet (polling interface).
|
||||
|
||||
- get_last_error() - Gets the last error as a string.
|
||||
|
||||
- is_disconnected_or_error() - True if we've had a problem (use
|
||||
get_last_error() to see what the problem was).
|
||||
|
||||
- stop() - Shutdown server or disconnect client (undoes
|
||||
start_client/start_server). Also must be used on the
|
||||
PacketSocket objects returned by get_next_server_connection().
|
||||
|
||||
Internal packet format:
|
||||
|
||||
- size: 4-byte little-endian unsigned integer indicating the
|
||||
number of bytes to follow for the next packet.
|
||||
|
||||
- data: As many bytes of data that were specified in the 'size'.
|
||||
|
||||
Note: 'size' does not include the space needed for the 'size'
|
||||
value itself.
|
||||
|
||||
Example packet (bytes):
|
||||
[ 0x03, 0x00, 0x00, 0x00, 0x41, 0x42, 0x43 ]
|
||||
|
||||
The first four bytes, [ 0x03, 0x00, 0x00, 0x00 ], indicate the
|
||||
size of the data, in a little endian unsigned integer. In this
|
||||
case that decodes to three. The next three bytes, [ 0x41, 0x42,
|
||||
0x43 ] are the packet data. In this case they represent the ASCII
|
||||
string "ABC".
|
||||
|
||||
To send this example packet, one would use send_packet(b'ABC').
|
||||
|
||||
On the receiving side, the return value of get_next_packet() would
|
||||
be b'ABC'.
|
||||
|
||||
This class exists in KiriPacketSocket.gd, ported to GDScript. Any
|
||||
functional changes to this should be reflected in that
|
||||
implementation as well.
|
||||
"""
|
||||
|
||||
class PacketBuffer:
|
||||
"""Receiving buffer for packets. Accumulates bytes until
|
||||
complete packets are formed.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._receive_buffer = b''
|
||||
self._packet_buffer = []
|
||||
|
||||
def _grab_complete_packets(self):
|
||||
|
||||
while len(self._receive_buffer) >= 4:
|
||||
|
||||
next_packet_size = int.from_bytes(
|
||||
self._receive_buffer[0:4],
|
||||
"little")
|
||||
|
||||
if len(self._receive_buffer) >= 4 + next_packet_size:
|
||||
|
||||
next_packet = self._receive_buffer[4 : 4 + next_packet_size]
|
||||
self._receive_buffer = self._receive_buffer[4 + len(next_packet):]
|
||||
self._packet_buffer.append(next_packet)
|
||||
|
||||
else:
|
||||
|
||||
break
|
||||
|
||||
def _have_complete_packet(self):
|
||||
self._grab_complete_packets()
|
||||
return len(self._packet_buffer) > 0
|
||||
|
||||
def get_next_packet(self):
|
||||
if not self._have_complete_packet():
|
||||
return None
|
||||
return self._packet_buffer.pop(0)
|
||||
|
||||
def add_bytes(self, incoming_bytes):
|
||||
self._receive_buffer += incoming_bytes
|
||||
|
||||
class SocketState(enum.Enum):
|
||||
DISCONNECTED = 0
|
||||
CONNECTING = 1
|
||||
CONNECTED = 2
|
||||
SERVER_STARTING = 3
|
||||
SERVER_LISTENING = 4
|
||||
ERROR = 5
|
||||
|
||||
def __init__(self):
|
||||
self._should_quit = False
|
||||
self._packet_buffer = self.PacketBuffer()
|
||||
self._state = self.SocketState.DISCONNECTED
|
||||
self._outgoing_packet_queue = []
|
||||
|
||||
self._state_lock = threading.Lock()
|
||||
self._worker_thread = None
|
||||
|
||||
self._new_connections_to_server = []
|
||||
self._error_string = None
|
||||
|
||||
def __del__(self):
|
||||
# WE BETTER NOT HAVE ZOMBIE THREADS SITTING AROUND.
|
||||
assert(not self._worker_thread)
|
||||
|
||||
def send_packet(self, packet_bytes):
|
||||
"""Add a binary blob to the send queue."""
|
||||
assert(packet_bytes)
|
||||
with self._state_lock:
|
||||
|
||||
assert(packet_bytes)
|
||||
self._outgoing_packet_queue.append(packet_bytes)
|
||||
|
||||
def get_next_packet(self):
|
||||
"""Get a binary blob from the receive queue."""
|
||||
with self._state_lock:
|
||||
ret = self._packet_buffer.get_next_packet()
|
||||
|
||||
return ret
|
||||
|
||||
def get_next_server_connection(self):
|
||||
"""For servers: Get the next incoming connection as a PacketSocket instance.
|
||||
|
||||
Returns None if there are no incoming connections.
|
||||
|
||||
"""
|
||||
with self._state_lock:
|
||||
ret = None
|
||||
if len(self._new_connections_to_server):
|
||||
ret = self._new_connections_to_server.pop(0)
|
||||
return ret
|
||||
|
||||
def get_last_error(self):
|
||||
"""Get the last error, as a string. (From the thrown exception.)
|
||||
|
||||
Returns None if there has not been an error.
|
||||
|
||||
"""
|
||||
with self._state_lock:
|
||||
return self._error_string
|
||||
|
||||
def is_disconnected_or_error(self):
|
||||
"""Returns True if this socket has disconnected, or thrown an
|
||||
error. (One way or another, the connection is gone and
|
||||
resources may be cleaned up.)
|
||||
|
||||
Note: May still have un-processed packets queued.
|
||||
|
||||
"""
|
||||
with self._state_lock:
|
||||
|
||||
bad_states = [
|
||||
self.SocketState.DISCONNECTED,
|
||||
self.SocketState.ERROR
|
||||
]
|
||||
|
||||
if self._state in bad_states:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_state(self):
|
||||
"""Returns the current SocketState of this object."""
|
||||
|
||||
with self._state_lock:
|
||||
return self._state
|
||||
|
||||
def start_server(self, address):
|
||||
"""For servers: Start a listening server. (And start worker
|
||||
thread.)
|
||||
|
||||
Address is a tuple with a host IP (string) and a port number
|
||||
(int). Use "0.0.0.0" to open on every interface.
|
||||
|
||||
"""
|
||||
|
||||
self._set_state(self.SocketState.SERVER_STARTING)
|
||||
|
||||
assert(not self._worker_thread)
|
||||
self._worker_thread = threading.Thread(
|
||||
target=self._server_thread_func,
|
||||
args=[address],
|
||||
daemon=True)
|
||||
|
||||
self._worker_thread.start()
|
||||
|
||||
def start_client(self, address):
|
||||
"""For clients: Attempt to connect to a listening server. (And
|
||||
start worker thread.)
|
||||
|
||||
Address is a tuple with a host IP (string) and a port number
|
||||
(int).
|
||||
|
||||
"""
|
||||
|
||||
self._set_state(self.SocketState.CONNECTING)
|
||||
|
||||
assert(not self._worker_thread)
|
||||
|
||||
self._worker_thread = threading.Thread(
|
||||
target=self._client_thread_func,
|
||||
args=[address],
|
||||
daemon=True)
|
||||
|
||||
self._worker_thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""Disconnect and shutdown the thread.
|
||||
|
||||
For servers: Note that this does not disconnect PacketSocket
|
||||
instances that were established from this server.
|
||||
|
||||
"""
|
||||
|
||||
assert(self._worker_thread)
|
||||
self._should_quit = True
|
||||
self._worker_thread.join()
|
||||
self._worker_thread = None
|
||||
self._should_quit = False
|
||||
|
||||
def is_running(self):
|
||||
return not (self._worker_thread == None)
|
||||
|
||||
def _normal_communication_loop(self, sock, address):
|
||||
"""Shared communication loop between clients and servers."""
|
||||
|
||||
# Packet wranging timeout. Should be low so we can send and
|
||||
# receive packets fast.
|
||||
sock.settimeout(0.0001)
|
||||
|
||||
while not self._should_quit:
|
||||
|
||||
# Get new data.
|
||||
try:
|
||||
incoming_bytes = sock.recv(1024)
|
||||
if not incoming_bytes:
|
||||
break
|
||||
with self._state_lock:
|
||||
self._packet_buffer.add_bytes(incoming_bytes)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
# Send all packets from queue.
|
||||
with self._state_lock:
|
||||
|
||||
while len(self._outgoing_packet_queue):
|
||||
next_outgoing_packet = self._outgoing_packet_queue.pop(0)
|
||||
sock.send(len(next_outgoing_packet).to_bytes(4, "little"))
|
||||
sock.send(next_outgoing_packet)
|
||||
|
||||
def _client_thread_func(self, address):
|
||||
"""Client startup thread function. Attempts to establishes connection."""
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
|
||||
try:
|
||||
# Connect to the server.
|
||||
self._set_state(self.SocketState.CONNECTING)
|
||||
sock.connect(address)
|
||||
|
||||
self._set_state(self.SocketState.CONNECTED)
|
||||
|
||||
self._normal_communication_loop(sock, address)
|
||||
|
||||
# We are now disconnected.
|
||||
self._set_state(self.SocketState.DISCONNECTED)
|
||||
sock.close()
|
||||
|
||||
except ConnectionError as ex:
|
||||
self._set_state(self.SocketState.ERROR, str(ex))
|
||||
|
||||
def _set_state(self, state, error_string=None):
|
||||
with self._state_lock:
|
||||
|
||||
self._state = state
|
||||
|
||||
if self._state == self.SocketState.ERROR:
|
||||
assert(error_string)
|
||||
self._error_string = error_string
|
||||
else:
|
||||
assert(not error_string)
|
||||
self._error_string = None
|
||||
|
||||
|
||||
def _server_to_client_thread_func(self, connection, address):
|
||||
"""Server connection startup thread function. Initiated
|
||||
internally from a server listening for connections.
|
||||
|
||||
"""
|
||||
|
||||
self._set_state(self.SocketState.CONNECTED)
|
||||
|
||||
try:
|
||||
self._normal_communication_loop(connection, address)
|
||||
except ConnectionError as ex:
|
||||
self._set_state(self.SocketState.ERROR, str(ex))
|
||||
|
||||
# Only switch to "disconnected" if we were most recently
|
||||
# connected, otherwise we could mask an error.
|
||||
if self.get_state() == self.SocketState.CONNECTED:
|
||||
self._set_state(self.SocketState.DISCONNECTED)
|
||||
|
||||
def _server_thread_func(self, address):
|
||||
"""Server thread function. Attempts to bind to an address and
|
||||
listen for incoming connections, in a loop.
|
||||
|
||||
"""
|
||||
|
||||
while not self._should_quit:
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
|
||||
# Timeout for waiting on incoming connections. This
|
||||
# can be "large". We aren't doing any message
|
||||
# wrangling besides getting these in. Worst case we
|
||||
# add latency to shutdown with this.
|
||||
sock.settimeout(0.01)
|
||||
|
||||
try:
|
||||
|
||||
# FXIME: This seems to be for UDP ports?
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
sock.bind(address)
|
||||
sock.listen()
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# FIXME: I wonder if we should do this in the main
|
||||
# thread so we can get the exceptions back up to
|
||||
# the start_server function and up from there.
|
||||
self._set_state(self.SocketState.ERROR, str(ex))
|
||||
break
|
||||
|
||||
|
||||
self._set_state(self.SocketState.SERVER_LISTENING)
|
||||
|
||||
while not self._should_quit:
|
||||
|
||||
try:
|
||||
connection, address = sock.accept()
|
||||
|
||||
new_client = PacketSocket()
|
||||
new_client._start_client_connection_from_server(connection, address)
|
||||
|
||||
with self._state_lock:
|
||||
self._new_connections_to_server.append(new_client)
|
||||
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
def _start_client_connection_from_server(self, connection, address):
|
||||
"""Entrypoint for PacketSocket instances created by servers
|
||||
accepting connections.
|
||||
|
||||
"""
|
||||
|
||||
assert(not self._worker_thread)
|
||||
self._worker_thread = threading.Thread(
|
||||
target=self._server_to_client_thread_func,
|
||||
args=(connection, address),
|
||||
daemon=True)
|
||||
self._worker_thread.start()
|
11
addons/kiripythonrpcwrapper/KiriPythonRPCWrapper.gd
Normal file
11
addons/kiripythonrpcwrapper/KiriPythonRPCWrapper.gd
Normal file
@ -0,0 +1,11 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
func _enter_tree():
|
||||
# Initialization of the plugin goes here.
|
||||
pass
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
# Clean-up of the plugin goes here.
|
||||
pass
|
154
addons/kiripythonrpcwrapper/KiriPythonRPCWrapper/__init__.py
Executable file
154
addons/kiripythonrpcwrapper/KiriPythonRPCWrapper/__init__.py
Executable file
@ -0,0 +1,154 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import argparse
|
||||
import time
|
||||
import psutil
|
||||
import json
|
||||
|
||||
import KiriPacketSocket
|
||||
|
||||
# Parse arguments
|
||||
arg_parser = argparse.ArgumentParser(
|
||||
prog="KiriPythonRPCWrapper",
|
||||
description="Wrapper for Python modules to RPCs from Godot.",
|
||||
epilog="")
|
||||
|
||||
arg_parser.add_argument("--script", type=str, required=True)
|
||||
arg_parser.add_argument("--port", type=int, required=True)
|
||||
arg_parser.add_argument("--parent_pid", type=int, required=True)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
|
||||
# module_path = "../KiriPacketSocket/__init__.py"
|
||||
# module_name = "KiriPacketSocket"
|
||||
|
||||
module_path = args.script
|
||||
module_name = ""
|
||||
|
||||
# Attempt to load the module.
|
||||
module_spec = importlib.util.spec_from_file_location(
|
||||
module_name, module_path)
|
||||
module = importlib.util.module_from_spec(module_spec)
|
||||
module_spec.loader.exec_module(module)
|
||||
|
||||
# This will be all the functions we find in the module that don't
|
||||
# start with "_".
|
||||
known_entrypoints = {}
|
||||
|
||||
# Scan the module for "public" functions.
|
||||
for entrypoint in dir(module):
|
||||
|
||||
# Skip anything starting with "_". Probably not meant to be
|
||||
# exposed.
|
||||
if entrypoint.startswith("_"):
|
||||
continue
|
||||
|
||||
attr = getattr(module, entrypoint)
|
||||
|
||||
# if hasattr(attr, "__call__"):
|
||||
if callable(attr):
|
||||
known_entrypoints[entrypoint] = attr
|
||||
|
||||
# Connect to server.
|
||||
packet_socket = KiriPacketSocket.PacketSocket()
|
||||
packet_socket.start_client(("127.0.0.1", args.port))
|
||||
while packet_socket.get_state() == packet_socket.SocketState.CONNECTING:
|
||||
time.sleep(0.001)
|
||||
|
||||
if packet_socket.get_state() != packet_socket.SocketState.CONNECTED:
|
||||
packet_socket.stop()
|
||||
raise Exception("Failed to connect to RPC host.")
|
||||
|
||||
print("Starting packet processing.")
|
||||
|
||||
|
||||
|
||||
def send_error_response(code, message, request_id):
|
||||
ret_dict = {
|
||||
"jsonrpc" : "2.0",
|
||||
"error" : {
|
||||
"code" : code,
|
||||
"message" : message
|
||||
},
|
||||
"id" : request_id
|
||||
}
|
||||
ret_dict_json = json.dumps(ret_dict)
|
||||
packet_socket.send_packet(ret_dict_json.encode("utf-8"))
|
||||
|
||||
def send_response(result, request_id):
|
||||
try:
|
||||
ret_dict = {
|
||||
"jsonrpc" : "2.0",
|
||||
"result" : ret,
|
||||
"id" : request_id
|
||||
}
|
||||
ret_dict_json = json.dumps(ret_dict)
|
||||
packet_socket.send_packet(ret_dict_json.encode("utf-8"))
|
||||
except Exception as e:
|
||||
send_error_response(-32603, "Error sending result: " + str(e), request_id)
|
||||
|
||||
# Start processing packets.
|
||||
while True:
|
||||
|
||||
# Shutdown when we lose connection to host.
|
||||
if packet_socket.get_state() != packet_socket.SocketState.CONNECTED:
|
||||
packet_socket.stop()
|
||||
raise Exception("Disconnected from RPC host.")
|
||||
|
||||
# Watch parent PID so we can clean up when needed.
|
||||
if not psutil.pid_exists(args.parent_pid):
|
||||
packet_socket.stop()
|
||||
raise Exception("RPC host process died")
|
||||
|
||||
next_packet = packet_socket.get_next_packet()
|
||||
while next_packet:
|
||||
this_packet = next_packet
|
||||
next_packet = packet_socket.get_next_packet()
|
||||
|
||||
print("GOT PACKET: ", this_packet)
|
||||
|
||||
# FIXME: Handle batches.
|
||||
|
||||
# Parse the incoming dict.
|
||||
try:
|
||||
request_dict_json = this_packet.decode("utf-8")
|
||||
request_dict = json.loads(request_dict_json)
|
||||
except Exception as e:
|
||||
send_error_response(-32700, "Error parsing packet: " + str(e), request_id)
|
||||
continue
|
||||
|
||||
# Make sure all the fields are there.
|
||||
try:
|
||||
method = request_dict["method"]
|
||||
func_args = request_dict["params"]
|
||||
request_id = request_dict["id"]
|
||||
except Exception as e:
|
||||
send_error_response(-32602, "Missing field: " + str(e), request_id)
|
||||
continue
|
||||
|
||||
# Make sure the method is something we scanned earlier.
|
||||
try:
|
||||
func = known_entrypoints[method]
|
||||
except Exception as e:
|
||||
send_error_response(-32601, "Method not found: " + str(e), request_id)
|
||||
continue
|
||||
|
||||
# Call the dang function.
|
||||
try:
|
||||
ret = func(*func_args)
|
||||
except Exception as e:
|
||||
send_error_response(-32603, "Call failed: " + str(e), request_id)
|
||||
continue
|
||||
|
||||
send_response(ret, request_id)
|
||||
|
||||
time.sleep(0.0001)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
this_is_not_a_thing_to_call = 0
|
||||
|
||||
def func_to_call(asdf):
|
||||
print("called with: ", asdf)
|
||||
global this_is_not_a_thing_to_call
|
||||
this_is_not_a_thing_to_call += 1
|
||||
return str(asdf) + "blah"
|
||||
|
||||
def other_func_to_call():
|
||||
print("jksdmckjsdncjksncs")
|
||||
|
10
addons/kiripythonrpcwrapper/KiriPythonRPCWrapper_start.py
Executable file
10
addons/kiripythonrpcwrapper/KiriPythonRPCWrapper_start.py
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import time
|
||||
|
||||
try:
|
||||
import KiriPythonRPCWrapper
|
||||
except Exception as e:
|
||||
print(e)
|
||||
time.sleep(5)
|
||||
|
219
addons/kiripythonrpcwrapper/KiriPythonWrapperInstance.gd
Normal file
219
addons/kiripythonrpcwrapper/KiriPythonWrapperInstance.gd
Normal file
@ -0,0 +1,219 @@
|
||||
extends RefCounted
|
||||
class_name KiriPythonWrapperInstance
|
||||
|
||||
var external_process_pid = -1
|
||||
|
||||
var server_packet_socket : KiriPacketSocket = null
|
||||
var communication_packet_socket : KiriPacketSocket = null
|
||||
|
||||
var python_script_path : String = ""
|
||||
|
||||
enum KiriPythonWrapperStatus {
|
||||
STATUS_RUNNING,
|
||||
STATUS_STOPPED
|
||||
}
|
||||
|
||||
func _init(python_file_path : String):
|
||||
python_script_path = python_file_path
|
||||
|
||||
func _get_python_executable():
|
||||
# FIXME: Adjust per-OS. Maybe test a few locations.
|
||||
return "/usr/bin/python3"
|
||||
|
||||
func _get_wrapper_script():
|
||||
# FIXME: Paths will be different for builds.
|
||||
var script_path = self.get_script().get_path()
|
||||
var script_dirname = script_path.get_base_dir()
|
||||
return ProjectSettings.globalize_path( \
|
||||
script_dirname + "/KiriPythonRPCWrapper_start.py")
|
||||
|
||||
func get_status():
|
||||
|
||||
if external_process_pid == -1:
|
||||
return KiriPythonWrapperStatus.STATUS_STOPPED
|
||||
|
||||
if not OS.is_process_running(external_process_pid):
|
||||
return KiriPythonWrapperStatus.STATUS_STOPPED
|
||||
|
||||
return KiriPythonWrapperStatus.STATUS_RUNNING
|
||||
|
||||
func start_process():
|
||||
|
||||
# FIXME: Make sure we don't have one running.
|
||||
|
||||
var open_port = 9500
|
||||
|
||||
assert(not server_packet_socket)
|
||||
server_packet_socket = KiriPacketSocket.new()
|
||||
while true:
|
||||
server_packet_socket.start_server(["127.0.0.1", open_port])
|
||||
|
||||
# Wait for the server to start.
|
||||
while server_packet_socket.get_state() == KiriPacketSocket.KiriSocketState.SERVER_STARTING:
|
||||
OS.delay_usec(1)
|
||||
|
||||
# If we're successfully listening, then we found a port to use and we
|
||||
# don't need to loop anymore.
|
||||
if server_packet_socket.get_state() == KiriPacketSocket.KiriSocketState.SERVER_LISTENING:
|
||||
break
|
||||
|
||||
# This port is busy. Try the next one.
|
||||
server_packet_socket.stop()
|
||||
open_port += 1
|
||||
|
||||
print("Port: ", open_port)
|
||||
|
||||
var python_exe_path : String = _get_python_executable()
|
||||
var wrapper_script_path : String = _get_wrapper_script()
|
||||
|
||||
var startup_command : Array = [
|
||||
"xterm", "-e",
|
||||
python_exe_path,
|
||||
wrapper_script_path,
|
||||
"--script", python_script_path,
|
||||
"--port", open_port,
|
||||
"--parent_pid", OS.get_process_id()]
|
||||
|
||||
print("startup command: ", startup_command)
|
||||
|
||||
external_process_pid = OS.create_process(
|
||||
startup_command[0], startup_command.slice(1), true)
|
||||
|
||||
print("external process: ", external_process_pid)
|
||||
|
||||
func stop_process():
|
||||
|
||||
if external_process_pid != -1:
|
||||
OS.kill(external_process_pid)
|
||||
external_process_pid = -1
|
||||
|
||||
|
||||
# Clean up server and communication sockets.
|
||||
if server_packet_socket:
|
||||
server_packet_socket.stop()
|
||||
server_packet_socket = null
|
||||
|
||||
if communication_packet_socket:
|
||||
communication_packet_socket.stop()
|
||||
communication_packet_socket = null
|
||||
|
||||
|
||||
class KiriPythonWrapperActiveRequest:
|
||||
|
||||
enum KiriPythonWrapperActiveRequestState {
|
||||
STATE_WAITING_TO_SEND,
|
||||
STATE_SENT,
|
||||
STATE_RESPONSE_RECEIVED
|
||||
}
|
||||
|
||||
var id : int
|
||||
var method_name : String
|
||||
var arguments : Variant # Dictionary or Array
|
||||
var callback # Callable or null
|
||||
var state : KiriPythonWrapperActiveRequestState
|
||||
var response # Return value from the call
|
||||
var error_response = ""
|
||||
|
||||
var _active_request_queue = {}
|
||||
var _request_counter = 0
|
||||
|
||||
func call_rpc_async(method : String, args : Variant, callback = null) -> int:
|
||||
|
||||
assert((args is Dictionary) or (args is Array))
|
||||
assert((callback == null) or (callback is Callable))
|
||||
|
||||
var new_request = KiriPythonWrapperActiveRequest.new()
|
||||
new_request.id = _request_counter
|
||||
_request_counter += 1
|
||||
new_request.method_name = method
|
||||
new_request.arguments = args
|
||||
new_request.callback = callback
|
||||
|
||||
_active_request_queue[new_request.id] = new_request
|
||||
|
||||
return new_request.id
|
||||
|
||||
func call_rpc_sync(method : String, args : Variant):
|
||||
|
||||
# Kinda hacky. We're using arrays because we can change the contents.
|
||||
# Without the array or something else mutable we'd just end up with the
|
||||
# internal pointer pointing to different values without affecting these
|
||||
# ones.
|
||||
var done_array = [false]
|
||||
var response_list = []
|
||||
var request_id = call_rpc_async(method, args, func(request_ob):
|
||||
done_array[0] = true
|
||||
response_list.append(request_ob.response)
|
||||
)
|
||||
|
||||
# Wait (block) until we get a response.
|
||||
while not done_array[0]:
|
||||
|
||||
# Bail out if something happened to our instance or connection to it.
|
||||
if communication_packet_socket:
|
||||
if communication_packet_socket.is_disconnected_or_error():
|
||||
break
|
||||
|
||||
poll()
|
||||
OS.delay_usec(1)
|
||||
|
||||
if len(response_list):
|
||||
return response_list[0]
|
||||
|
||||
return null
|
||||
|
||||
func poll():
|
||||
|
||||
# Hand-off between listening socket and actual communications socket.
|
||||
if server_packet_socket:
|
||||
communication_packet_socket = server_packet_socket.get_next_server_connection()
|
||||
if communication_packet_socket:
|
||||
server_packet_socket.stop()
|
||||
server_packet_socket = null
|
||||
|
||||
if communication_packet_socket:
|
||||
|
||||
# Send all waiting requests
|
||||
for request_id in _active_request_queue:
|
||||
var request : KiriPythonWrapperActiveRequest = _active_request_queue[request_id]
|
||||
if request.state == request.KiriPythonWrapperActiveRequestState.STATE_WAITING_TO_SEND:
|
||||
var request_dict = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": request.method_name,
|
||||
"params": request.arguments,
|
||||
"id": request.id
|
||||
}
|
||||
var request_dict_json = JSON.stringify(request_dict)
|
||||
communication_packet_socket.send_packet(request_dict_json.to_utf8_buffer())
|
||||
request.state = request.KiriPythonWrapperActiveRequestState.STATE_SENT
|
||||
|
||||
# Check for responses.
|
||||
var packet = communication_packet_socket.get_next_packet()
|
||||
while packet != null:
|
||||
var packet_dict = JSON.parse_string(packet.get_string_from_utf8())
|
||||
if packet_dict:
|
||||
if packet_dict.has("id"):
|
||||
var request_id = packet_dict["id"]
|
||||
|
||||
# floats aren't even allowed in JSON RPC as an id. Probably
|
||||
# meant it to be an int.
|
||||
if request_id is float:
|
||||
request_id = int(request_id)
|
||||
|
||||
if _active_request_queue.has(request_id):
|
||||
var request : KiriPythonWrapperActiveRequest = \
|
||||
_active_request_queue[request_id]
|
||||
if "result" in packet_dict:
|
||||
request.response = packet_dict["result"]
|
||||
else:
|
||||
request.error_response = "Couldn't find result on packet."
|
||||
if request.callback:
|
||||
request.callback.call(request)
|
||||
|
||||
# Clean up request.
|
||||
_active_request_queue.erase(request_id)
|
||||
|
||||
packet = communication_packet_socket.get_next_packet()
|
||||
|
||||
|
||||
|
7
addons/kiripythonrpcwrapper/plugin.cfg
Normal file
7
addons/kiripythonrpcwrapper/plugin.cfg
Normal file
@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="KiriPythonRPCWrapper"
|
||||
description=""
|
||||
author="Kiri"
|
||||
version=""
|
||||
script="KiriPythonRPCWrapper.gd"
|
28
addons/kiripythonrpcwrapper/test_rpc.py
Executable file
28
addons/kiripythonrpcwrapper/test_rpc.py
Executable file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import time
|
||||
|
||||
import KiriPacketSocket
|
||||
|
||||
ps = KiriPacketSocket.PacketSocket()
|
||||
ps.start_server(("127.0.0.1", 9506))
|
||||
|
||||
connections = []
|
||||
|
||||
while True:
|
||||
|
||||
psc = ps.get_next_server_connection()
|
||||
if psc:
|
||||
print("Got connection.")
|
||||
connections.append(psc)
|
||||
psc.send_packet(b'ABCDEFGHIJ')
|
||||
|
||||
|
||||
for conn in connections:
|
||||
p = conn.get_next_packet()
|
||||
while p:
|
||||
print(p)
|
||||
conn.send_packet(p + b'1')
|
||||
p = conn.get_next_packet()
|
||||
|
||||
time.sleep(0.0001)
|
12
control.tscn
Normal file
12
control.tscn
Normal file
@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bl7j8w8guq0ns"]
|
||||
|
||||
[ext_resource type="Script" path="res://Control.gd" id="1_xu82u"]
|
||||
|
||||
[node name="Control" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_xu82u")
|
1
icon.svg
Normal file
1
icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 813 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H447l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c3 34 55 34 58 0v-86c-3-34-55-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>
|
After Width: | Height: | Size: 950 B |
37
icon.svg.import
Normal file
37
icon.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dd4sp7xpjgqrp"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
29
project.godot
Normal file
29
project.godot
Normal file
@ -0,0 +1,29 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="GodotJSONRPCTest"
|
||||
config/features=PackedStringArray("4.2", "GL Compatibility")
|
||||
run/max_fps=60
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[display]
|
||||
|
||||
window/vsync/vsync_mode=0
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/kiripythonrpcwrapper/plugin.cfg")
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
Loading…
Reference in New Issue
Block a user