mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-23 23:49:47 +08:00
language updates and work on messaging
This commit is contained in:
parent
39436f910c
commit
e9ca5092ef
@ -420,10 +420,10 @@
|
||||
"assets/build/ba_data/audio/zoeOw.ogg": "https://files.ballistica.net/cache/ba1/14/f1/4f2995d78fc20dd79dfb39c5d554",
|
||||
"assets/build/ba_data/audio/zoePickup01.ogg": "https://files.ballistica.net/cache/ba1/57/ac/6ed0caecd25dc23688debed24c45",
|
||||
"assets/build/ba_data/audio/zoeScream01.ogg": "https://files.ballistica.net/cache/ba1/32/08/38dac4a79ab2acee76a75d32a310",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/97/0d/5879ed6101537373f87deeccf860",
|
||||
"assets/build/ba_data/data/langdata.json": "https://files.ballistica.net/cache/ba1/37/c5/fefc3b664420efec78431704855e",
|
||||
"assets/build/ba_data/data/languages/arabic.json": "https://files.ballistica.net/cache/ba1/0f/0e/7184059414320d32104463e41038",
|
||||
"assets/build/ba_data/data/languages/belarussian.json": "https://files.ballistica.net/cache/ba1/e2/58/c2c5964370df118c51528dc4bfa2",
|
||||
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/0c/40/6222070dc95b29e42b77dd105357",
|
||||
"assets/build/ba_data/data/languages/chinese.json": "https://files.ballistica.net/cache/ba1/d8/b8/a38187a1dfba81a42ddfbf2932be",
|
||||
"assets/build/ba_data/data/languages/chinesetraditional.json": "https://files.ballistica.net/cache/ba1/f9/78/95903907372008cdce27d948b797",
|
||||
"assets/build/ba_data/data/languages/croatian.json": "https://files.ballistica.net/cache/ba1/66/bf/6e98398016da261296b8c306560e",
|
||||
"assets/build/ba_data/data/languages/czech.json": "https://files.ballistica.net/cache/ba1/b6/67/633c424cc32e5c4afbd188d3a908",
|
||||
@ -440,7 +440,7 @@
|
||||
"assets/build/ba_data/data/languages/indonesian.json": "https://files.ballistica.net/cache/ba1/87/e5/a10ddd73cfb7996bbd576032db6a",
|
||||
"assets/build/ba_data/data/languages/italian.json": "https://files.ballistica.net/cache/ba1/34/0f/dd2e311024ceb913b8489b823fdc",
|
||||
"assets/build/ba_data/data/languages/korean.json": "https://files.ballistica.net/cache/ba1/26/8d/bf9cc8db2cc71b69e789898e1093",
|
||||
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/4f/23/b9692ca7f9407972254fb245ffb0",
|
||||
"assets/build/ba_data/data/languages/persian.json": "https://files.ballistica.net/cache/ba1/d8/b7/9098f0cb25088d233541490e3e68",
|
||||
"assets/build/ba_data/data/languages/polish.json": "https://files.ballistica.net/cache/ba1/2e/17/fb3e7ed77fa54427b434b1791793",
|
||||
"assets/build/ba_data/data/languages/portuguese.json": "https://files.ballistica.net/cache/ba1/6c/04/a528a4df9364ad4f0261cbc83f0a",
|
||||
"assets/build/ba_data/data/languages/romanian.json": "https://files.ballistica.net/cache/ba1/82/12/57bf144e12be229a9b70da9c45cb",
|
||||
|
||||
3
.idea/dictionaries/ericf.xml
generated
3
.idea/dictionaries/ericf.xml
generated
@ -1401,6 +1401,7 @@
|
||||
<w>mpath</w>
|
||||
<w>mrmaxmeier</w>
|
||||
<w>msbuild</w>
|
||||
<w>msgdict</w>
|
||||
<w>mshell</w>
|
||||
<w>msvccompiler</w>
|
||||
<w>msvcp</w>
|
||||
@ -1914,6 +1915,7 @@
|
||||
<w>rstr</w>
|
||||
<w>rtnetlink</w>
|
||||
<w>rtxt</w>
|
||||
<w>rtypes</w>
|
||||
<w>runmypy</w>
|
||||
<w>runonly</w>
|
||||
<w>runpy</w>
|
||||
@ -2140,6 +2142,7 @@
|
||||
<w>storeitemui</w>
|
||||
<w>storename</w>
|
||||
<w>strftime</w>
|
||||
<w>stringified</w>
|
||||
<w>stringprep</w>
|
||||
<w>stringptr</w>
|
||||
<w>strippable</w>
|
||||
|
||||
@ -638,6 +638,7 @@
|
||||
<w>moreso</w>
|
||||
<w>mqrspec</w>
|
||||
<w>msaa</w>
|
||||
<w>msgdict</w>
|
||||
<w>mult</w>
|
||||
<w>multing</w>
|
||||
<w>multipass</w>
|
||||
@ -882,6 +883,7 @@
|
||||
<w>rresult</w>
|
||||
<w>rscode</w>
|
||||
<w>rsgc</w>
|
||||
<w>rtypes</w>
|
||||
<w>runnables</w>
|
||||
<w>rvec</w>
|
||||
<w>rvel</w>
|
||||
@ -990,6 +992,7 @@
|
||||
<w>strcpy</w>
|
||||
<w>strdup</w>
|
||||
<w>stringi</w>
|
||||
<w>stringified</w>
|
||||
<w>strlen</w>
|
||||
<w>strs</w>
|
||||
<w>strtof</w>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||
<h4><em>last updated on 2021-08-23 for Ballistica version 1.6.5 build 20391</em></h4>
|
||||
<h4><em>last updated on 2021-09-02 for Ballistica version 1.6.5 build 20391</em></h4>
|
||||
<p>This page documents the Python classes and functions in the 'ba' module,
|
||||
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
||||
<hr>
|
||||
|
||||
@ -5,30 +5,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
|
||||
# import pytest
|
||||
import pytest
|
||||
|
||||
from efro.message import MessageProtocol, MessageSender
|
||||
from efro.dataclassio import ioprepped
|
||||
from efro.message import (Message, MessageProtocol, MessageSender,
|
||||
MessageReceiver)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
from typing import List, Type
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class _TestMessage1(Message):
|
||||
"""Just testing."""
|
||||
ival: int
|
||||
|
||||
@classmethod
|
||||
def get_response_types(cls) -> List[Type[Message]]:
|
||||
return [_TestMessage2]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class _TestMessage2(Message):
|
||||
"""Just testing."""
|
||||
bval: bool
|
||||
|
||||
|
||||
def test_protocol_creation() -> None:
|
||||
"""Test protocol creation."""
|
||||
|
||||
# This should fail because _TestMessage1 can return _TestMessage2 which
|
||||
# is not given an id here.
|
||||
with pytest.raises(ValueError):
|
||||
_protocol = MessageProtocol(message_types={1: _TestMessage1})
|
||||
|
||||
_protocol = MessageProtocol(message_types={
|
||||
1: _TestMessage1,
|
||||
2: _TestMessage2
|
||||
})
|
||||
|
||||
|
||||
def test_message_sending() -> None:
|
||||
"""Test simple message sending."""
|
||||
|
||||
protocol = MessageProtocol(message_types={})
|
||||
protocol = MessageProtocol(message_types={
|
||||
1: _TestMessage1,
|
||||
2: _TestMessage2
|
||||
})
|
||||
|
||||
class TestClass:
|
||||
"""Test."""
|
||||
class TestClassS:
|
||||
"""For testing send functionality."""
|
||||
|
||||
msg = MessageSender(protocol)
|
||||
|
||||
def __init__(self, receiver: TestClassR) -> None:
|
||||
self._receiver = receiver
|
||||
|
||||
@msg.send_raw_handler
|
||||
def _send_raw_message(self, data: bytes) -> bytes:
|
||||
"""Test."""
|
||||
print(f'WOULD SEND RAW MSG OF SIZE {len(data)}')
|
||||
print(f'WOULD SEND RAW MSG OF SIZE: {len(data)}')
|
||||
return b''
|
||||
|
||||
msg = MessageSender(protocol, _send_raw_message)
|
||||
class TestClassR:
|
||||
"""For testing receive functionality."""
|
||||
|
||||
obj = TestClass()
|
||||
print(f'MADE TEST OBJ {obj}')
|
||||
obj.msg.send('foo')
|
||||
receiver = MessageReceiver(protocol)
|
||||
|
||||
@receiver.handler
|
||||
def handle_test_message(self, msg: Message) -> Message:
|
||||
"""Test."""
|
||||
del msg # Unused
|
||||
print('Hello from test message 1 handler!')
|
||||
return _TestMessage2(bval=True)
|
||||
|
||||
obj_r = TestClassR()
|
||||
obj_s = TestClassS(receiver=obj_r)
|
||||
print(f'MADE TEST OBJS {obj_s} and {obj_r}')
|
||||
obj_s.msg.send(_TestMessage1(ival=0))
|
||||
|
||||
@ -302,6 +302,12 @@ def ioprepped(cls: Type[T]) -> Type[T]:
|
||||
return cls
|
||||
|
||||
|
||||
def is_ioprepped_dataclass(obj: Any) -> bool:
|
||||
"""Return whether the obj is an ioprepped dataclass type or instance."""
|
||||
cls = obj if isinstance(obj, type) else type(obj)
|
||||
return dataclasses.is_dataclass(cls) and hasattr(cls, PREP_ATTR)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PrepData:
|
||||
"""Data we prepare and cache for a class during prep.
|
||||
|
||||
@ -2,13 +2,19 @@
|
||||
#
|
||||
"""Entity functionality.
|
||||
|
||||
****************************************************************************
|
||||
NOTE: This is largely being replaced by dataclassio, which offers similar
|
||||
functionality in a cleaner way. Ideally we should remove this completely at
|
||||
some point, but for now we should at least try to avoid using it in new code.
|
||||
****************************************************************************
|
||||
|
||||
A system for defining structured data, supporting both static and runtime
|
||||
type safety, serialization, efficient/sparse storage, per-field value
|
||||
limits, etc. This is a heavyweight option in comparison to things such as
|
||||
dataclasses, but the increased features can make the overhead worth it for
|
||||
certain use cases.
|
||||
|
||||
Advantages compared to nested dataclasses:
|
||||
Advantages compared to raw nested dataclasses:
|
||||
- Field names separated from their data representation so can get more
|
||||
concise json data, change variable names while preserving back-compat, etc.
|
||||
- Can wrap and preserve unmapped data (so fields can be added to new versions
|
||||
@ -16,7 +22,7 @@ Advantages compared to nested dataclasses:
|
||||
- Incorrectly typed data is caught at runtime (for dataclasses we rely on
|
||||
type-checking and explicit validation calls)
|
||||
|
||||
Disadvantages compared to nested dataclasses:
|
||||
Disadvantages compared to raw nested dataclasses:
|
||||
- More complex to use
|
||||
- Significantly more heavyweight (roughly 10 times slower in quick tests)
|
||||
- Can't currently be initialized in constructors (this would probably require
|
||||
|
||||
@ -54,6 +54,10 @@ class RemoteError(Exception):
|
||||
This occurs when communication succeeds but another type of error
|
||||
occurs remotely. The error string can consist of a remote stack
|
||||
trace or a simple message depending on the context.
|
||||
|
||||
Depending on the situation, more specific error types such as CleanError
|
||||
may be raised due to the remote error, so this one is considered somewhat
|
||||
of a catch-all.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@ -11,6 +11,10 @@ from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
# NOTE: This functionality is duplicated in a newer, cleaner way in
|
||||
# dataclassio. We should consider deprecating this along with entity at
|
||||
# some point.
|
||||
|
||||
# Special attr we included for our extended type information
|
||||
# (extended-json-type)
|
||||
TYPE_TAG = '_xjtp'
|
||||
|
||||
@ -6,40 +6,137 @@ Supports static typing for message types and possible return types.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from efro.dataclassio import (ioprepped, is_ioprepped_dataclass, IOAttrs,
|
||||
dataclass_to_dict)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict, Type, Tuple, List, Any, Callable, Optional
|
||||
from typing import Dict, Type, Tuple, List, Any, Callable, Optional, Set
|
||||
from efro.error import TransportError
|
||||
|
||||
TM = TypeVar('TM', bound='MessageSender')
|
||||
|
||||
|
||||
class RemoteErrorType(Enum):
|
||||
"""Type of error that occurred in remote message handling."""
|
||||
OTHER = 0
|
||||
CLEAN = 1
|
||||
|
||||
|
||||
class Message:
|
||||
"""Base class for messages and their responses."""
|
||||
|
||||
@classmethod
|
||||
def get_response_types(cls) -> List[Type[Message]]:
|
||||
"""Return all message types this Message can result in when sent.
|
||||
Messages intended only for response types can leave this empty.
|
||||
Note: RemoteErrorMessage is handled transparently and does not
|
||||
need to be specified here.
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class RemoteErrorMessage(Message):
|
||||
"""Message saying some error has occurred on the other end."""
|
||||
error_message: Annotated[str, IOAttrs('m')]
|
||||
error_type: Annotated[RemoteErrorType, IOAttrs('t')]
|
||||
|
||||
|
||||
class MessageProtocol:
|
||||
"""Wrangles a set of message types, formats, and response types.
|
||||
Both endpoints must be using the same Protocol (even if one side
|
||||
is newer) for communication to succeed.
|
||||
Both endpoints must be using a compatible Protocol for communication
|
||||
to succeed. To maintain Protocol compatibility between revisions,
|
||||
all message types must retain the same id, message attr storage names must
|
||||
not change, newly added attrs must have default values, etc.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_types: Dict[int, Tuple[Type, List[Type]]],
|
||||
type_key: str = '_t',
|
||||
) -> None:
|
||||
def __init__(self,
|
||||
message_types: Dict[int, Type[Message]],
|
||||
type_key: Optional[str] = None,
|
||||
preserve_clean_errors: bool = True,
|
||||
remote_stack_traces: bool = False) -> None:
|
||||
"""Create a protocol with a given configuration.
|
||||
Each entry for message_types should contain an ID, a message type,
|
||||
and all possible response types.
|
||||
|
||||
If 'type_key' is provided, the message type ID is stored as the
|
||||
provided key in the message dict; otherwise it will be stored as
|
||||
part of a top level dict with the message payload appearing as a
|
||||
child dict. This is mainly for backwards compatibility.
|
||||
|
||||
If 'preserve_clean_errors' is True, efro.error.CleanError errors
|
||||
on the remote end will result in the same error raised locally.
|
||||
All other Exception types come across as efro.error.RemoteError.
|
||||
|
||||
If 'remote_stack_traces' is True, stringified remote stack traces will
|
||||
be included in the RemoteError. This should only be enabled in cases
|
||||
where the client is trusted.
|
||||
"""
|
||||
self._message_types = message_types
|
||||
self._message_types_by_id: Dict[int, Type[Message]] = {}
|
||||
self._message_ids_by_type: Dict[Type[Message], int] = {}
|
||||
for m_id, m_type in message_types.items():
|
||||
|
||||
# Make sure only valid message types were passed and each
|
||||
# id was assigned only once.
|
||||
assert isinstance(m_id, int)
|
||||
assert (is_ioprepped_dataclass(m_type)
|
||||
and issubclass(m_type, Message))
|
||||
assert self._message_types_by_id.get(m_id) is None
|
||||
|
||||
self._message_types_by_id[m_id] = m_type
|
||||
self._message_ids_by_type[m_type] = m_id
|
||||
|
||||
# Make sure all return types are valid and have been assigned
|
||||
# an ID as well.
|
||||
if __debug__:
|
||||
all_response_types: Set[Type[Message]] = set()
|
||||
for m_id, m_type in message_types.items():
|
||||
m_rtypes = m_type.get_response_types()
|
||||
assert isinstance(m_rtypes, list)
|
||||
all_response_types.update(m_rtypes)
|
||||
for cls in all_response_types:
|
||||
assert is_ioprepped_dataclass(cls) and issubclass(cls, Message)
|
||||
if cls not in self._message_ids_by_type:
|
||||
raise ValueError(f'Possible response type {cls}'
|
||||
f' was not included in message_types.')
|
||||
|
||||
self._type_key = type_key
|
||||
self._preserve_clean_errors = preserve_clean_errors
|
||||
self._remote_stack_traces = remote_stack_traces
|
||||
|
||||
def message_encode(self, message: Any) -> bytes:
|
||||
def message_encode(self, message: Message) -> bytes:
|
||||
"""Encode a message to bytes for sending."""
|
||||
print(f'WOULD ENCODE MSG: {message} TO RAW.')
|
||||
return b''
|
||||
|
||||
def message_decode(self, data: bytes) -> Any:
|
||||
m_id = self._message_ids_by_type.get(type(message))
|
||||
if m_id is None:
|
||||
raise TypeError(f'Message type is not registered in protocol:'
|
||||
f' {type(message)}')
|
||||
msgdict = dataclass_to_dict(message)
|
||||
|
||||
# Encode type as part of the message dict if desired
|
||||
# (for legacy compatibility).
|
||||
if self._type_key is not None:
|
||||
if self._type_key in msgdict:
|
||||
raise RuntimeError(f'Type-key {self._type_key}'
|
||||
f' found in msg of type {type(message)}')
|
||||
msgdict[self._type_key] = m_id
|
||||
out = msgdict
|
||||
else:
|
||||
out = {'m': msgdict, 't': m_id}
|
||||
return json.dumps(out, separators=(',', ':')).encode()
|
||||
|
||||
def message_decode(self, data: bytes) -> Message:
|
||||
"""Decode a message from bytes."""
|
||||
print(f'WOULD DECODE MSG FROM RAW: {str(data)}')
|
||||
return 'foo'
|
||||
return Message()
|
||||
|
||||
def create_sender_module(self, classname: str) -> str:
|
||||
""""Create a Python module defining a MessageSender subclass.
|
||||
@ -66,37 +163,51 @@ class MessageSender:
|
||||
Example:
|
||||
|
||||
class MyClass:
|
||||
msg = MyMessageSender(some_protocol)
|
||||
|
||||
@msg.send_raw_handler
|
||||
def send_raw_message(self, message: bytes) -> bytes:
|
||||
# Actually send the message here.
|
||||
|
||||
msg = MyMessageSender(some_protocol, send_raw_message)
|
||||
|
||||
# MyMessageSender class should provide overloads for send(), send_bg(),
|
||||
# etc. to ensure all sending happens with valid types.
|
||||
obj = MyClass()
|
||||
obj.msg.send(SomeMessageType())
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, protocol: MessageProtocol,
|
||||
send_raw_message_call: Optional[Callable[[Any, bytes],
|
||||
bytes]]) -> None:
|
||||
def __init__(self, protocol: MessageProtocol) -> None:
|
||||
self._protocol = protocol
|
||||
self._send_raw_message_call = send_raw_message_call
|
||||
self._send_raw_message_call: Optional[Callable[[Any, bytes],
|
||||
bytes]] = None
|
||||
self._bound_obj: Any = None
|
||||
|
||||
def __get__(self, obj: Any, type_in: Any = None) -> Any:
|
||||
def __get__(self: TM, obj: Any, type_in: Any = None) -> TM:
|
||||
if obj is None:
|
||||
raise RuntimeError('Must be called on an instance, not a type.')
|
||||
print(f'HELLO FROM GET {obj}')
|
||||
return self
|
||||
|
||||
def send(self, message: Any) -> Any:
|
||||
# Return a copy of ourself bound to the instance we were called from.
|
||||
bound_sender = type(self)(self._protocol)
|
||||
bound_sender._send_raw_message_call = self._send_raw_message_call
|
||||
bound_sender._bound_obj = obj
|
||||
|
||||
return bound_sender
|
||||
|
||||
def send_raw_handler(
|
||||
self, call: Callable[[Any, bytes],
|
||||
bytes]) -> Callable[[Any, bytes], bytes]:
|
||||
"""Function decorator for setting raw send method."""
|
||||
assert self._send_raw_message_call is None
|
||||
self._send_raw_message_call = call
|
||||
return call
|
||||
|
||||
def send(self, message: Message) -> Any:
|
||||
"""Send a message and receive a response.
|
||||
Will encode the message for transport and call dispatch_raw_message()
|
||||
"""
|
||||
if self._send_raw_message_call is None:
|
||||
raise RuntimeError('Unimplemented!')
|
||||
raise RuntimeError('send() is unimplemented for this type.')
|
||||
if self._bound_obj is None:
|
||||
raise RuntimeError('Cannot call on an unbound instance.')
|
||||
encoded = self._protocol.message_encode(message)
|
||||
return self._send_raw_message_call(None, encoded)
|
||||
|
||||
@ -127,7 +238,7 @@ class MessageReceiver:
|
||||
|
||||
# MyMessageReceiver should provide overloads to register_handler()
|
||||
# to ensure all registered handlers have valid types/return-types.
|
||||
@receiver.register_handler
|
||||
@receiver.handler
|
||||
def handle_some_message_type(self, message: SomeType) -> AnotherType:
|
||||
# Deal with this message type here.
|
||||
|
||||
@ -140,17 +251,25 @@ class MessageReceiver:
|
||||
"""
|
||||
|
||||
def __init__(self, protocol: MessageProtocol) -> None:
|
||||
pass
|
||||
self._protocol = protocol
|
||||
|
||||
def register(self, call: Callable) -> None:
|
||||
"""Register a call to handle a message type in the protocol."""
|
||||
def handler(
|
||||
self, call: Callable[[Any, Message], Message]
|
||||
) -> Callable[[Any, Message], Message]:
|
||||
"""Decorator for registering calls to handle message types."""
|
||||
print('WOULD REGISTER HANDLER TYPE')
|
||||
return call
|
||||
|
||||
def handle_raw_message(self, msg: bytes) -> bytes:
|
||||
"""Should be called when the receiver gets a message.
|
||||
The return value is the raw response to the message.
|
||||
"""
|
||||
print('RECEIVER WOULD HANDLE RAW MESSAGE')
|
||||
del msg # Unused
|
||||
return b''
|
||||
|
||||
async def handle_raw_message_async(self, msg: bytes) -> bytes:
|
||||
"""Should be called when the receiver gets a message.
|
||||
The return value is the raw response to the message.
|
||||
"""
|
||||
raise RuntimeError('Unimplemented!')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user