language updates and work on messaging

This commit is contained in:
Eric Froemling 2021-09-02 08:37:50 -05:00
parent 39436f910c
commit e9ca5092ef
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
10 changed files with 248 additions and 48 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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))

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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'

View File

@ -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!')