From 13b9be3e15534d57c9a40473c16671aed2e35d6e Mon Sep 17 00:00:00 2001 From: Jordan Maxwell Date: Wed, 13 Nov 2024 17:23:25 -0600 Subject: [PATCH] Add initial interfaces and a more complete binding (#1) * Initial rework * Fixed README * Fixed various typos * Changed naming style * Misc. adjustments * Misc. updates * Additional SS bindings and sorted by docs * Cleaned up control messages * Updated names * Added client agent constants * Completed client agent bindings * Misc. additions * Misc updates * Added PR template * Added basic README * Fix incorrect folder * Create preview builds * Automatically determine requirements from requirements.txt * Misc. changes --- .github/PULL_REQUEST_TEMPLATE.md | 13 + .../{python-publish.yml => main.yml} | 15 +- .gitignore | 39 +- README.md | 49 +- panda3d_astron/client.py | 404 ++++++ panda3d_astron/interfaces/__init__.py | 0 panda3d_astron/interfaces/clientagent.py | 536 +++++++ panda3d_astron/{ => interfaces}/database.py | 52 +- panda3d_astron/interfaces/events.py | 165 +++ panda3d_astron/{ => interfaces}/messenger.py | 14 +- panda3d_astron/interfaces/state.py | 723 ++++++++++ panda3d_astron/internal.py | 543 ++++++++ panda3d_astron/msgtypes.py | 486 ++++--- panda3d_astron/repository.py | 1237 ----------------- requirements.txt | 2 + setup.py | 19 +- 16 files changed, 2866 insertions(+), 1431 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md rename .github/workflows/{python-publish.yml => main.yml} (78%) create mode 100644 panda3d_astron/client.py create mode 100644 panda3d_astron/interfaces/__init__.py create mode 100644 panda3d_astron/interfaces/clientagent.py rename panda3d_astron/{ => interfaces}/database.py (89%) create mode 100644 panda3d_astron/interfaces/events.py rename panda3d_astron/{ => interfaces}/messenger.py (92%) create mode 100644 panda3d_astron/interfaces/state.py create mode 100644 panda3d_astron/internal.py delete mode 100644 panda3d_astron/repository.py create mode 100644 requirements.txt diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..cf7e7f1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## Issue description + + +## Solution description + + +## Checklist +I have done my best to ensure that… +* [] …this change follows the coding style and design patterns of the codebase +* [] …I own the intellectual property rights to this code +* [] …the intent of this change is clearly explained +* [] …the changed code is adequately covered by the test suite, where possible. \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/main.yml similarity index 78% rename from .github/workflows/python-publish.yml rename to .github/workflows/main.yml index df70198..a0b1508 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/main.yml @@ -40,13 +40,23 @@ jobs: major_pattern: "(MAJOR)" minor_pattern: "(MINOR)" - # Build the package using the version + # Export the version as an environment variable and also export + # the current release vs prerelease state as an environment variable. + # This is determined if the event_name is release or not. Finally + # build the package. - name: Build package + if: github.event_name != 'pull_request' run: | export MAJOR=${{ steps.package-version.outputs.major }} export MINOR=${{ steps.package-version.outputs.minor }} export PATCH=${{ steps.package-version.outputs.patch }} + if [ "${{ github.event_name }}" == "release" ]; then + export RELEASE="true" + else + export RELEASE="false" + fi + echo "Building version $MAJOR.$MINOR.$PATCH" python -m build @@ -54,6 +64,7 @@ jobs: - name: Load secret id: op-load-secret uses: 1password/load-secrets-action@v2 + if: github.event_name != 'pull_request' with: export-env: false env: @@ -63,7 +74,7 @@ jobs: # Publish the package to PyPi if a release is created - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - if: github.event_name == 'release' + if: github.event_name != 'pull_request' with: user: __token__ password: ${{ steps.op-load-secret.outputs.SECRET }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6e4761..82f9275 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ parts/ sdist/ var/ wheels/ -pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -50,6 +49,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -72,6 +72,7 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook @@ -82,7 +83,9 @@ profile_default/ ipython_config.py # pyenv -.python-version +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -91,7 +94,24 @@ ipython_config.py # install all needed dependencies. #Pipfile.lock -# PEP 582; used by e.g. github.com/David-OConnor/pyflow +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff @@ -127,3 +147,16 @@ dmypy.json # Pyre type checker .pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/README.md b/README.md index a8ffa2e..c32179d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,45 @@ -Panda3D Astron -============== -![GitHub issues](https://img.shields.io/github/issues/NxtStudios/p3d-rest?style=for-the-badge) -![PyPI - Status](https://img.shields.io/pypi/status/panda3d-astron?style=for-the-badge) -![Engine](https://img.shields.io/static/v1?style=for-the-badge&label=Engine&message=Panda3D&color=red) +# Panda3D Astron -Panda3D Astron server support as a Python 3 module. Allows the use of Astron with stock Panda3D +[![Build, Test, and Publish](https://github.com/thetestgame/panda3d-astron/actions/workflows/main.yml/badge.svg)](https://github.com/thetestgame/panda3d-astron/actions/workflows/main.yml) +![PyPI - Version](https://img.shields.io/pypi/v/panda3d-astron) -### Requirements +## Overview -- The Panda3D SDK (get it here) +Panda3D Astron is a package that integrates the Astron distributed server framework with the Panda3D game engine. This allows for the creation of large-scale multiplayer games using Panda3D. + +## Installation + +You can install the package using pip: + +```bash +pip install panda3d-astron +``` + +## Dependencies + +- `panda3d` +- `panda3d-toolbox` + +These dependencies will be installed automatically when you install `panda3d-astron`. + +## Usage + +To use Panda3D Astron in your project, simply import it along with Panda3D: + +```python +import panda3d_astron +from panda3d.core import * +``` ## License -Panda3D REST is licensed under the MIT license. See the provided LICENSE file for details. + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request on GitHub. + +## Contact + +For any questions or inquiries, please contact the project maintainers through GitHub. + diff --git a/panda3d_astron/client.py b/panda3d_astron/client.py new file mode 100644 index 0000000..e4ec454 --- /dev/null +++ b/panda3d_astron/client.py @@ -0,0 +1,404 @@ +""" +This module contains the AstronClientRepository class, which is a subclass of the ClientRepositoryBase class +from the Panda3D direct.distributed module. This class is used to implement the client-side of the Astron +distributed networking system. It is used to communicate with an Astron ClientAgent instance, which is the +server-side of the Astron distributed networking system. +""" + +from direct.directnotify import DirectNotifyGlobal +from direct.distributed.ClientRepositoryBase import ClientRepositoryBase +from direct.distributed.PyDatagram import PyDatagram +from direct.distributed.MsgTypes import * +from direct.showbase import ShowBase # __builtin__.config +from direct.task.TaskManagerGlobal import * # taskMgr +from direct.distributed.PyDatagramIterator import PyDatagramIterator +from direct.distributed import DoInterestManager as interest_mgr + +from panda3d_astron import msgtypes +from panda3d_toolbox import runtime + +class AstronClientRepository(ClientRepositoryBase): + """ + The Astron implementation of a clients repository for + communication with an Astron ClientAgent. + + This repo will emit events for: + * CLIENT_HELLO_RESP + * CLIENT_EJECT ( error_code, reason ) + * CLIENT_OBJECT_LEAVING ( do_id ) + * CLIENT_ADD_INTEREST ( context, interest_id, parent_id, zone_id ) + * CLIENT_ADD_INTEREST_MULTIPLE ( icontext, interest_id, parent_id, [zone_ids] ) + * CLIENT_REMOVE_INTEREST ( context, interest_id ) + * CLIENT_DONE_INTEREST_RESP ( context, interest_id ) + * LOST_CONNECTION () + """ + + notify = DirectNotifyGlobal.directNotify.newCategory("repository") + + # This is required by DoCollectionManager, even though it's not + # used by this implementation. + GameGlobalsId = 0 + + def __init__(self, *args, **kwargs): + ClientRepositoryBase.__init__(self, *args, **kwargs) + runtime.base.finalExitCallbacks.append(self.shutdown) + + self.message_handlers = { + msgtypes.CLIENT_HELLO_RESP: self.handleHelloResp, + msgtypes.CLIENT_EJECT: self.handleEject, + msgtypes.CLIENT_ENTER_OBJECT_REQUIRED: self.handleEnterObjectRequired, + msgtypes.CLIENT_ENTER_OBJECT_REQUIRED_OTHER: self.handleEnterObjectRequiredOther, + msgtypes.CLIENT_ENTER_OBJECT_REQUIRED_OWNER: self.handleEnterObjectRequiredOwner, + msgtypes.CLIENT_ENTER_OBJECT_REQUIRED_OTHER_OWNER: self.handleEnterObjectRequiredOtherOwner, + msgtypes.CLIENT_OBJECT_SET_FIELD: self.handleUpdateField, + msgtypes.CLIENT_OBJECT_SET_FIELDS: self.handleUpdateFields, + msgtypes.CLIENT_OBJECT_LEAVING: self.handleObjectLeaving, + msgtypes.CLIENT_OBJECT_LOCATION: self.handleObjectLocation, + msgtypes.CLIENT_ADD_INTEREST: self.handleAddInterest, + msgtypes.CLIENT_ADD_INTEREST_MULTIPLE: self.handleAddInterestMultiple, + msgtypes.CLIENT_REMOVE_INTEREST: self.handleRemoveInterest, + msgtypes.CLIENT_DONE_INTEREST_RESP: self.handleInterestDoneMessage, + } + + def handleDatagram(self, di: PyDatagramIterator) -> None: + """ + Handles incoming datagrams from an Astron ClientAgent instance. + """ + + msg_type = self.getMsgType() + message_handler = self.message_handlers.get(msg_type, None) + if message_handler is None: + self.notify.error('Received unknown message type: %d!' % msg_type) + return + + message_handler(di) + self.consider_heartbeat() + + handle_datagram = handleDatagram + + def consider_heartbeat(self) -> None: + """ + Sends a heartbeat message to the Astron client agent if one has not been sent recently. + """ + + super().considerHeartbeat() + + def handleHelloResp(self, di: PyDatagramIterator) -> None: + """ + Handles the CLIENT_HELLO_RESP packet sent by the Client Agent to the client when the client's CLIENT_HELLO is accepted. + """ + + runtime.messenger.send("CLIENT_HELLO_RESP", []) + + def handleEject(self, di: PyDatagramIterator) -> None: + """ + Handles the CLIENT_DISCONNECT sent by the client to the Client Agent to notify that it is going to close the connection. + """ + + error_code = di.get_uint16() + reason = di.get_string() + + runtime.messenger.send("CLIENT_EJECT", [error_code, reason]) + + def handleEnterObjectRequired(self, di: PyDatagramIterator) -> None: + """ + """ + + do_id = di.get_uint32() + parent_id = di.get_uint32() + zone_id = di.get_uint32() + dclass_id = di.get_uint16() + dclass = self.dclassesByNumber[dclass_id] + self.generateWithRequiredFields(dclass, do_id, di, parent_id, zone_id) + + def handleEnterObjectRequiredOther(self, di: PyDatagramIterator) -> None: + """ + """ + + do_id = di.get_uint32() + parent_id = di.get_uint32() + zone_id = di.get_uint32() + dclass_id = di.get_uint16() + dclass = self.dclassesByNumber[dclass_id] + self.generateWithRequiredOtherFields(dclass, do_id, di, parent_id, zone_id) + + def handleEnterObjectRequiredOwner(self, di: PyDatagramIterator) -> None: + """ + """ + + avatar_doId = di.get_uint32() + parent_id = di.get_uint32() + zone_id = di.get_uint32() + dclass_id = di.get_uint16() + dclass = self.dclassesByNumber[dclass_id] + self.generateWithRequiredFieldsOwner(dclass, avatar_doId, di) + + def handleEnterObjectRequiredOtherOwner(self, di: PyDatagramIterator) -> None: + """ + """ + + avatar_doId = di.get_uint32() + parent_id = di.get_uint32() + zone_id = di.get_uint32() + dclass_id = di.get_uint16() + dclass = self.dclassesByNumber[dclass_id] + self.generateWithRequiredOtherFieldsOwner(dclass, avatar_doId, di) + + def generateWithRequiredFieldsOwner(self, dclass: object, doId: int, di: PyDatagramIterator) -> None: + """ + """ + + if doId in self.doId2ownerView: + # ...it is in our dictionary. + # Just update it. + self.notify.error('duplicate owner generate for %s (%s)' % ( + doId, dclass.getName())) + distObj = self.doId2ownerView[doId] + assert distObj.dclass == dclass + distObj.generate() + distObj.updateRequiredFields(dclass, di) + # updateRequiredFields calls announceGenerate + elif self.cacheOwner.contains(doId): + # ...it is in the cache. + # Pull it out of the cache: + distObj = self.cacheOwner.retrieve(doId) + assert distObj.dclass == dclass + # put it in the dictionary: + self.doId2ownerView[doId] = distObj + # and update it. + distObj.generate() + distObj.updateRequiredFields(dclass, di) + # updateRequiredFields calls announceGenerate + else: + # ...it is not in the dictionary or the cache. + # Construct a new one + classDef = dclass.getOwnerClassDef() + if classDef == None: + self.notify.error("Could not create an undefined %s object. Have you created an owner view?" % (dclass.getName())) + distObj = classDef(self) + distObj.dclass = dclass + # Assign it an Id + distObj.doId = doId + # Put the new do in the dictionary + self.doId2ownerView[doId] = distObj + # Update the required fields + distObj.generateInit() # Only called when constructed + distObj.generate() + distObj.updateRequiredFields(dclass, di) + # updateRequiredFields calls announceGenerate + return distObj + + generate_with_required_Fields_owner = generateWithRequiredFieldsOwner + + def handleUpdateFields(self, di): + """ + """ + + # Can't test this without the server actually sending it. + self.notify.error("CLIENT_OBJECT_SET_FIELDS not implemented!") + # # Here's some tentative code and notes: + # do_id = di.getUint32() + # field_count = di.getUint16() + # for i in range(0, field_count): + # field_id = di.getUint16() + # field = self.get_dc_file().get_field_by_index(field_id) + # # print(type(field)) + # # print(field) + # # FIXME: Get field type, unpack value, create and send message. + # # value = di.get?() + # # Assemble new message + + def handleObjectLeaving(self, di): + """ + """ + + do_id = di.get_uint32() + dist_obj = self.doId2do.get(do_id) + dist_obj.delete() + self.deleteObject(do_id) + + runtime.messenger.send("CLIENT_OBJECT_LEAVING", [do_id]) + + def handleAddInterest(self, di): + """ + """ + + context = di.get_uint32() + interest_id = di.get_uint16() + parent_id = di.get_uint32() + zone_id = di.get_uint32() + + runtime.messenger.send("CLIENT_ADD_INTEREST", [context, interest_id, parent_id, zone_id]) + self.addInternalInterestHandle(context, interest_id, parent_id, [zone_id]) + + def handleAddInterestMultiple(self, di): + """ + """ + + context = di.get_uint32() + interest_id = di.get_uint16() + parent_id = di.get_uint32() + zone_ids = [di.get_uint32() for i in range(0, di.get_uint16())] + + runtime.messenger.send("CLIENT_ADD_INTEREST_MULTIPLE", [context, interest_id, parent_id, zone_ids]) + self.addInternalInterestHandle(context, interest_id, parent_id, zone_ids) + + def addInternalInterestHandle(self, context, interest_id, parent_id, zone_ids) -> None: + """ + """ + + # make sure we've got parenting rules set in the DC + if parent_id not in (self.getGameDoId(),): + parent = self.getDo(parent_id) + if not parent: + self.notify.error('Attempting to add interest under unknown object %s' % parent_id) + else: + if not parent.hasParentingRules(): + self.notify.error('No setParentingRules defined in the DC for object %s (%s)' % (parent_id, parent.__class__.__name__)) + + interest_mgr.DoInterestManager._interests[interest_id] = interest_mgr.InterestState( + None, interest_mgr.InterestState.StateActive, context, None, parent_id, zone_ids, self._completeEventCount, True) + + def handleRemoveInterest(self, di): + """ + """ + + context = di.get_uint32() + interest_id = di.get_uint16() + runtime.messenger.send("CLIENT_REMOVE_INTEREST", [context, interest_id]) + + def deleteObject(self, doId): + """ + implementation copied from ClientRepository.py + Removes the object from the client's view of the world. This + should normally not be called directly except in the case of + error recovery, since the server will normally be responsible + for deleting and disabling objects as they go out of scope. + After this is called, future updates by server on this object + will be ignored (with a warning message). The object will + become valid again the next time the server sends a generate + message for this doId. + This is not a distributed message and does not delete the + object on the server or on any other client. + """ + + if doId in self.doId2do: + # If it is in the dictionary, remove it. + obj = self.doId2do[doId] + # Remove it from the dictionary + del self.doId2do[doId] + # Disable, announce, and delete the object itself... + # unless delayDelete is on... + obj.deleteOrDelay() + if self.isLocalId(doId): + self.freeDoId(doId) + elif self.cache.contains(doId): + # If it is in the cache, remove it. + self.cache.delete(doId) + if self.isLocalId(doId): + self.freeDoId(doId) + else: + # Otherwise, ignore it + self.notify.warning( + "Asked to delete non-existent DistObj " + str(doId)) + + delete_object = deleteObject + + def sendUpdate(self, distObj, fieldName, args): + """ + Sends a normal update for a single field. + """ + + dg = distObj.dclass.clientFormatUpdate( + fieldName, distObj.doId, args) + self.send(dg) + + send_update = sendUpdate + + def sendHello(self, version_string: str = None): + """ + Sends our CLIENT_HELLO protocol handshake packet to the game server. Uses the supplied argument version if present + otherwise default to the server-version PRC variable + """ + + if version_string == None: + version_string = ConfigVariableString('server-version', '') + + if version_string == None or version_string == '': + self.notify.error('No server version defined at the time of hello. Please set a "server-version" string in your panda runtime conrfiguration file') + return + + dg = PyDatagram() + dg.add_uint16(msgtypes.CLIENT_HELLO) + dg.add_uint32(self.get_dc_file().get_hash()) + dg.add_string(version_string) + self.send(dg) + + send_hello = sendHello + + def sendHeartbeat(self): + """ + """ + + datagram = PyDatagram() + datagram.addUint16(msgtypes.CLIENT_HEARTBEAT) + self.send(datagram) + + send_heartbeat = sendHeartbeat + + def sendAddInterest(self, context, interest_id, parent_id, zone_id): + """ + """ + + dg = PyDatagram() + dg.add_uint16(msgtypes.CLIENT_ADD_INTEREST) + dg.add_uint32(context) + dg.add_uint16(interest_id) + dg.add_uint32(parent_id) + dg.add_uint32(zone_id) + self.send(dg) + + send_add_interest = sendAddInterest + + def sendAddInterestMultiple(self, context, interest_id, parent_id, zone_ids): + """ + """ + + dg = PyDatagram() + dg.add_uint16(msgtypes.CLIENT_ADD_INTEREST_MULTIPLE) + dg.add_uint32(context) + dg.add_uint16(interest_id) + dg.add_uint32(parent_id) + dg.add_uint16(len(zone_ids)) + for zone_id in zone_ids: + dg.add_uint32(zone_id) + self.send(dg) + + send_add_interest_multiple = sendAddInterestMultiple + + def sendRemoveInterest(self, context, interest_id): + """ + """ + + dg = PyDatagram() + dg.add_uint16(msgtypes.CLIENT_REMOVE_INTEREST) + dg.add_uint32(context) + dg.add_uint16(interest_id) + self.send(dg) + + send_remove_interest = sendRemoveInterest + + def lostConnection(self): + """ + """ + + runtime.messenger.send("LOST_CONNECTION") + + def disconnect(self): + """ + This implicitly deletes all objects from the repository. + """ + + for do_id in self.doId2do.keys(): + self.deleteObject(do_id) + ClientRepositoryBase.disconnect(self) \ No newline at end of file diff --git a/panda3d_astron/interfaces/__init__.py b/panda3d_astron/interfaces/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/panda3d_astron/interfaces/clientagent.py b/panda3d_astron/interfaces/clientagent.py new file mode 100644 index 0000000..7de5861 --- /dev/null +++ b/panda3d_astron/interfaces/clientagent.py @@ -0,0 +1,536 @@ +""" +Client Agent Interface for Astron +""" + +import enum + +from direct.directnotify import DirectNotifyGlobal +from direct.distributed.PyDatagram import PyDatagram +from direct.distributed.PyDatagramIterator import PyDatagramIterator + +from panda3d import direct +from panda3d import core as p3d +from panda3d_astron import msgtypes + +class ClientState(enum.IntEnum): + """ + Possible states for a client connection. These are used to track the state of a client + and control the client's access to the game world. + """ + + CLIENT_STATE_NEW = 0 + CLIENT_STATE_ESTABLISHED = 2 + CLIENT_STATE_DISCONNECTED = 3 + +class ClientEjectCodes(enum.IntEnum): + """ + Constant network eject codes that are used to indicate the reason for ejecting a client. + """ + + EJECT_LOGGED_IN_ELSE_WHERE = 100 + + EJECT_OVERSIZED_DATAGRAM = 106 + EJECT_INVALID_FIRST_MSG = 107 + EJECT_INVALID_MSGTYPE = 108 + EJECT_INVALID_MSGSIZE = 109 + + EJECT_SANDBOX_VIOLATION = 113 + EJECT_ILLEGAL_INTEREST_OP = 115 + EJECT_MANIP_INVALID_OBJECT = 117 + EJECT_PERM_VIOLATION_SET_FIELD = 118 + EJECT_PERM_VIOLATION_SET_LOCATION = 119 + + EJECT_LOGIN_ISSUE = 122 + + EJECT_INVALID_VERSION = 124 + EJECT_INVALID_HASH = 125 + EJECT_ADMIN_ACCESS_VIOLATION = 126 + + EJECT_ADMIN_EJECTED = 151 + EJECT_MOD_EJECTED = 152 + EJECT_ANTI_CHEAT_EJECTED = 153 + EJECT_GAMESERVER_MAINTENANCE = 154 + EJECT_GAMESERVER_PROCESSING_ERROR = 155 + + EJECT_HEARTBEAT_TIMEOUT = 345 + EJECT_SERVER_PROCESSING_ERROR = 346 + EJECT_CLIENT_AGENT_IO_SEND_ERROR = 347 + EJECT_CLIENT_AGENT_IO_READ_ERROR = 348 + EJECT_SERVER_IO_SEND_ERROR = 349 + EJECT_SERVER_IO_READ_ERROR = 350 + +class ClientAgentInterface(object): + """ + Network interface for the client agent. + """ + + def __init__(self, air: object): + """ + Initialize the client agent interface. + """ + + self.air = air + self.__callbacks = {} + + @property + def notify(self) -> object: + """ + Retrieves the parent repositories notify object + """ + + return self.air.notify + + def handle_datagram(self, msg_type: int, di: object) -> None: + """ + Handles client agent datagrams + """ + + if msg_type == msgtypes.CLIENTAGENT_GET_NETWORK_ADDRESS_RESP: + self.handle_get_network_address_resp(di) + elif msg_type == msgtypes.CLIENTAGENT_GET_TLVS_RESP: + self.handle_get_tlvs_response(di) + elif msg_type == msgtypes.CLIENTAGENT_DONE_INTEREST_RESP: + self.handle_client_agent_interest_done_resp(di) + else: + message_name = msgtypes.MsgId2Names.get(msg_type, str(msg_type)) + self.notify.warning('Received unknown client agent message: %s' % message_name) + + def set_client_state(self, clientChannel: int, state: int) -> None: + """ + Sets the state of the client on the CA. + Useful for logging in and logging out, and for little else. + """ + + # If we were given an enum, convert it to its value + if hasattr(state, 'value'): + state = state.value + + # Build and send the datagram + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_SET_STATE) + dg.add_uint16(state) + + self.air.send(dg) + + setClientState = set_client_state + + def set_client_state_new(self, clientChannel: int) -> None: + """ + Sets the state of the client to CLIENT_STATE_NEW. + """ + + self.set_client_state(clientChannel, ClientState.CLIENT_STATE_NEW) + + setClientStateNew = set_client_state_new + + def set_client_state_established(self, clientChannel: int) -> None: + """ + Sets the state of the client to CLIENT_STATE_ESTABLISHED. + """ + + self.set_client_state(clientChannel, ClientState.CLIENT_STATE_ESTABLISHED) + + setClientStateEstablished = set_client_state_established + + def set_client_id(self, clientChannel: int, clientId: int) -> None: + """ + Changes the sender used to represent this client. This is useful if application/game components need to identify the avatar/account a given message came from: + by changing the sender channel to include this information, the server can easily determine the account ID of a client that sends a field update. + + Note: This also results in the CA opening the new channel, if it isn't open already. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_SET_CLIENT_ID) + dg.add_uint64(clientId) + + self.air.send(dg) + + setClientId = set_client_id + + def set_client_account_id(self, clientChannel: int, accountId: int) -> None: + """ + Changes the client id associated with the client channel to reflect the account the client is authenticated with. This is useful if application/game components need to identify the account a given message came from. + by changing the sender channel to include this information, the server can easily determine the account ID of a client that sends a field update. + + Note: This also results in the CA opening the new channel, if it isn't open already. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_SET_CLIENT_ID) + dg.add_uint64(accountId << 32) + + self.air.send(dg) + + setAccountId = set_client_account_id + + def set_client_avatar_id(self, clientChannel: int, avatarId: int) -> None: + """ + Changes the client id associated with the client channel to reflect the avatar the client is authenticated with. This is useful if application/game components need to identify the avatar a given message came from. + by changing the sender channel to include this information, the server can easily determine the avatar ID of a client that sends a field update. + + Note: This also results in the CA opening the new channel, if it isn't open already. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_SET_CLIENT_ID) + dg.add_uint64(clientChannel << 32 | avatarId) + + self.air.send(dg) + + setAvatarId = set_client_avatar_id + + def remove_client_avatar_id(self, clientChannel: int) -> None: + """ + Changes the client id associated with the client channel to reflect the avatar the client is authenticated with. This is useful if application/game components need to identify the avatar a given message came from. + by changing the sender channel to include this information, the server can easily determine the account ID of a client that sends a field update. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_SET_CLIENT_ID) + dg.add_uint64(clientChannel << 32) + + self.air.send(dg) + + removeAvatarId = remove_client_avatar_id + + def send_client_datagram(self, clientChannel: int, datagram: PyDatagram) -> None: + """ + Send a raw datagram down the pipe to the client. This is useful for sending app/game-specific messages to the client, debugging, etc. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_SEND_DATAGRAM) + dg.add_string(datagram.getMessage()) + + self.air.send(dg) + + def eject(self, clientChannel: int, reasonCode: int, reason: str) -> None: + """ + Kicks the client residing at the specified clientChannel, using the specifed reasoning. + """ + + # If we were given an enum, convert it to its value + if hasattr(reasonCode, 'value'): + reasonCode = reasonCode.value + + # Build and send the datagram + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_EJECT) + dg.add_uint16(reasonCode) + dg.add_string(reason) + + self.air.send(dg) + + Eject = eject + + def drop(self, clientChannel: int) -> None: + """ + Similar to eject, but causes the CA to silently close the client connection, providing no explanation whatsoever to the client. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_DROP) + + self.air.send(dg) + + Drop = drop + + def get_client_network_address(self, clientId: int, callback: object) -> None: + """ + Get the endpoints of a client connection. + You should already be sure the client actually exists, otherwise the + callback will never be called. + Callback is called as: callback(remoteIp, remotePort, localIp, localPort) + """ + + ctx = self.air.get_context() + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(clientId, self.air.ourChannel, msgtypes.CLIENTAGENT_GET_NETWORK_ADDRESS) + dg.add_uint32(ctx) + + self.air.send(dg) + + getClientNetworkAddress = get_client_network_address + + def handle_get_network_address_resp(self, di: object) -> None: + """ + Handle the response to a get_network_address request. + """ + + ctx = di.get_uint32() + remoteIp = di.get_string() + remotePort = di.get_uint16() + localIp = di.get_string() + localPort = di.get_uint16() + + if ctx not in self.__callbacks: + self.notify.warning('Received unexpected CLIENTAGENT_GET_NETWORK_ADDRESS_RESP (ctx: %d)' % ctx) + return + + try: + self.__callbacks[ctx](remoteIp, remotePort, localIp, localPort) + finally: + del self.__callbacks[ctx] + + def declare_object(self, clientChannel: int, doId: int, zoneId: int) -> None: + """ + Because Client Agents verify the integrity of field updates, they must know the dclass of a given object to ensure that the incoming field update is for a field that the dclass/object actually has. + Therefore, clients are normally unable to send messages to objects unless they are either configured as an UberDOG or are currently visible to the client. + This message explicitly tells the CA that a given object exists, of a given type, and allows the client to send field updates to that object even if the client cannot currently see that object. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_DECLARE_OBJECT) + dg.add_uint32(doId) + dg.add_uint32(zoneId) + + self.air.send(dg) + + declareObject = declare_object + + def undeclare_object(self, clientChannel: int, doId: int) -> None: + """ + Antithesis of declare_object: the object is no longer explicitly declared, and the client can no longer send updates on this object without seeing it. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_UNDECLARE_OBJECT) + dg.add_uint32(doId) + + self.air.send(dg) + + def client_add_session_object(self, clientChannel: int, doId: int) -> None: + """ + Declares the specified DistributedObject to be a "session object", + meaning that it is destroyed when the client disconnects. + Generally used for avatars owned by the client. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_ADD_SESSION_OBJECT) + dg.add_uint32(doId) + + self.air.send(dg) + + clientAddSessionObject = client_add_session_object + + def client_remove_session_object(self, clientChannel: int, doId: int) -> None: + """ + Antithesis of client_add_session_object. The declared object is no longer tied to the client's session, and will therefore + not be deleted if the client drops (nor will the client be dropped if this object is deleted). + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_REMOVE_SESSION_OBJECT) + dg.add_uint32(doId) + + self.air.send(dg) + + def set_fields_sendable(self, do: object, channelId: int, fieldNameList: list = []) -> None: + """ + Overrides the security of a field(s) specified, allows an owner of a DistributedObject to send + the field(s) regardless if its marked ownsend/clsend. + """ + + dg = PyDatagram() + dg.addServerHeader(channelId, self.air.ourChannel, msgtypes.CLIENTAGENT_SET_FIELDS_SENDABLE) + fieldIds = [] + for fieldName in fieldNameList: + field = do.dclass.getFieldByName(fieldName) + + if not field: + continue + + fieldIds.append(field.getNumber()) + + dg.add_uint32(do.doId) + dg.add_uint16(len(fieldIds)) + for fieldId in fieldIds: + dg.add_uint16(fieldId) + + self.air.send(dg) + + setAllowClientSend = set_fields_sendable + set_allow_client_send = set_fields_sendable # Legacy alias + + def get_tlvs(self, clientChannel: int, callback: object) -> None: + """ + Requests the TLVs associated with the client. + """ + + ctx = self.air.get_context() + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_GET_CLIENTTLVS) + dg.add_uint32(ctx) + + self.air.send(dg) + + getTlvs = get_tlvs + + def handle_get_tlvs_response(self, di: PyDatagramIterator) -> None: + """ + Returns the blob representation of the TLVs associated with the client, as provided by HAProxy. + """ + + ctx = di.get_uint32() + tlvs = di.get_remaining_bytes() + + if ctx not in self.__callbacks: + self.notify.warning('Received unexpected CLIENTAGENT_GET_CLIENTTLVS_RESP (ctx: %d)' % ctx) + return + + try: + self.__callbacks[ctx](tlvs) + finally: + del self.__callbacks[ctx] + + def client_open_channel(self, clientChannel: int, newChannel: int) -> None: + """ + Instruct the client session to open a channel on the MD. Messages sent to this new channel will be processed by the CA. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_OPEN_CHANNEL) + dg.add_uint64(newChannel) + + self.air.send(dg) + + clientOpenChannel = client_open_channel + + def client_close_channel(self, clientChannel: int, channel: int) -> None: + """ + This message is the antithesis of the message above. The channel is immediately closed, even if the channel was automatically opened. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_CLOSE_CHANNEL) + dg.add_uint64(channel) + + self.air.send(dg) + + clientCloseChannel = client_close_channel + + def client_add_post_remove(self, clientChannel:int, datagram: object) -> None: + """ + Similar to CONTROL_ADD_POST_REMOVE, this hangs a "post-remove" message on the client. If the client is ever disconnected, the post-remove messages will be sent out automatically. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_ADD_POST_REMOVE) + dg.add_string(datagram.getMessage()) + + self.air.send(dg) + + clientAddPostRemove = client_add_post_remove + + def client_clear_post_remove(self, clientChannel: int) -> None: + """ + Removes all post-remove messages from the client. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_CLEAR_POST_REMOVES) + + self.air.send(dg) + + clientClearPostRemove = client_clear_post_remove + + def client_add_interest(self, client_channel: int, interest_id: int, parent_id: int, zone_id: int, callback: object = None) -> None: + """ + Opens an interest on the behalf of the client. This, used in conjunction + with add_interest: visible (or preferably, disabled altogether), will mitigate + possible security risks. + """ + + dg = PyDatagram() + dg.addServerHeader(client_channel, self.air.ourChannel, msgtypes.CLIENTAGENT_ADD_INTEREST) + dg.add_uint16(interest_id) + dg.add_uint32(parent_id) + dg.add_uint32(zone_id) + + self.air.send(dg) + + if callback != None: + ctx = (client_channel, interest_id) + self.__callbacks[ctx] = callback + + clientAddInterest = client_add_interest + + def client_add_interest_multiple(self, client_channel: int, interest_id: int, parent_id: int, zone_list: int, callback: object = None) -> None: + """ + Opens multiple interests on the behalf of the client. This, used in conjunction + with add_interest: visible (or preferably, disabled altogether), will mitigate + possible security risks. + """ + + dg = PyDatagram() + dg.addServerHeader(client_channel, self.air.ourChannel, msgtypes.CLIENTAGENT_ADD_INTEREST_MULTIPLE) + dg.add_uint16(interest_id) + dg.add_uint32(parent_id) + + dg.add_uint16(len(zone_list)) + for zoneId in zone_list: + dg.add_uint32(zoneId) + + if callback != None: + ctx = (client_channel, interest_id) + self.__callbacks[ctx] = callback + + self.air.send(dg) + + clientAddInterestMultiple = client_add_interest_multiple + + def client_remove_interest(self, client_channel: int, interest_id: int, callback: object = None) -> None: + """ + Removes an interest on the behalf of the client. This, used in conjunction + with add_interest: visible (or preferably, disabled altogether), will mitigate + possible security risks. + """ + + dg = PyDatagram() + dg.addServerHeader(client_channel, self.air.ourChannel, msgtypes.CLIENTAGENT_REMOVE_INTEREST) + dg.add_uint16(interest_id) + + self.air.send(dg) + + if callback != None: + ctx = (client_channel, interest_id) + self.__callbacks[ctx] = callback + + clientRemoveInterest = client_remove_interest + + def handle_client_agent_interest_done_resp(self, di: PyDatagramIterator) -> None: + """ + Sent by the ClientAgent to the caller of CLIENTAGENT_ADD_INTEREST to inform them + that the interest operation has completed. + """ + + client_channel = di.get_uint64() + interest_id = di.get_uint16() + ctx = (client_channel, interest_id) + + if ctx not in self.__callbacks: + return + + try: + self.__callbacks[ctx](client_channel, interest_id) + finally: + del self.__callbacks[ctx] + + def send_system_message(self, message: str, clientChannel: int = 10) -> None: + """ + Sends a CLIENT_SYSTEM_MESSAGE to the given client channel. + """ + + dg = PyDatagram() + dg.addServerHeader(clientChannel, self.air.ourChannel, msgtypes.CLIENTAGENT_SEND_SYSTEM_MESSAGE) + dg.add_string(message) + + self.send_client_datagram(clientChannel, dg) + + sendSystemMessage = send_system_message \ No newline at end of file diff --git a/panda3d_astron/database.py b/panda3d_astron/interfaces/database.py similarity index 89% rename from panda3d_astron/database.py rename to panda3d_astron/interfaces/database.py index 619adbd..d598e53 100644 --- a/panda3d_astron/database.py +++ b/panda3d_astron/interfaces/database.py @@ -1,9 +1,11 @@ from panda3d.core import * from panda3d.direct import DCPacker + from direct.directnotify import DirectNotifyGlobal from direct.distributed.ConnectionRepository import ConnectionRepository from direct.distributed.PyDatagram import PyDatagram from direct.distributed.PyDatagramIterator import PyDatagramIterator + from panda3d_astron.msgtypes import * class AstronDatabaseInterface: @@ -15,14 +17,25 @@ class AstronDatabaseInterface: Do not create this class directly; instead, use AstronInternalRepository's dbInterface attribute. """ - notify = DirectNotifyGlobal.directNotify.newCategory("AstronDatabaseInterface") def __init__(self, air): + """ + Initialize the Astron database interface. + """ + self.air = air self._callbacks = {} self._dclasses = {} + @property + def notify(self) -> object: + """ + Retrieves the parent repositories notify object + """ + + return self.air.notify + def createObject(self, databaseId, dclass, fields={}, callback=None): """ Create an object in the specified database. @@ -61,7 +74,9 @@ def createObject(self, databaseId, dclass, fields={}, callback=None): dg.appendData(fieldPacker.getBytes()) self.air.send(dg) - def handleCreateObjectResp(self, di): + create_object = createObject + + def handle_create_object_resp(self, di): ctx = di.getUint32() doId = di.getUint32() @@ -118,7 +133,9 @@ def queryObject(self, databaseId, doId, callback, dclass=None, fieldNames=()): dg.addUint16(field.getNumber()) self.air.send(dg) - def handleQueryObjectResp(self, msgType, di): + query_object = queryObject + + def handle_query_object_resp(self, msgType, di): ctx = di.getUint32() success = di.getUint8() @@ -248,7 +265,9 @@ def updateObject(self, databaseId, doId, dclass, newFields, oldFields=None, call # Oh well, better honor their request: callback(None) - def handleUpdateObjectResp(self, di, multi): + update_object = updateObject + + def handle_update_object_resp(self, di, multi): ctx = di.getUint32() success = di.getUint8() @@ -295,14 +314,21 @@ def handleUpdateObjectResp(self, di, multi): finally: del self._callbacks[ctx] - def handleDatagram(self, msgType, di): - if msgType == DBSERVER_CREATE_OBJECT_RESP: - self.handleCreateObjectResp(di) - elif msgType in (DBSERVER_OBJECT_GET_ALL_RESP, + def handle_datagram(self, msg_type: int, di: object) -> None: + """ + Handle a datagram from the database server. + """ + + if msg_type == DBSERVER_CREATE_OBJECT_RESP: + self.handle_create_object_resp(di) + elif msg_type in (DBSERVER_OBJECT_GET_ALL_RESP, DBSERVER_OBJECT_GET_FIELDS_RESP, DBSERVER_OBJECT_GET_FIELD_RESP): - self.handleQueryObjectResp(msgType, di) - elif msgType == DBSERVER_OBJECT_SET_FIELD_IF_EQUALS_RESP: - self.handleUpdateObjectResp(di, False) - elif msgType == DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS_RESP: - self.handleUpdateObjectResp(di, True) \ No newline at end of file + self.handle_query_object_resp(msg_type, di) + elif msg_type == DBSERVER_OBJECT_SET_FIELD_IF_EQUALS_RESP: + self.handle_update_object_resp(di, False) + elif msg_type == DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS_RESP: + self.handle_update_object_resp(di, True) + else: + message_name = MsgId2Names.get(msg_type, str(msg_type)) + self.notify.warning('Received unknown database message: %s' % message_name) \ No newline at end of file diff --git a/panda3d_astron/interfaces/events.py b/panda3d_astron/interfaces/events.py new file mode 100644 index 0000000..517e706 --- /dev/null +++ b/panda3d_astron/interfaces/events.py @@ -0,0 +1,165 @@ +""" +""" + +from direct.distributed.PyDatagram import PyDatagram +from panda3d import core as p3d +from panda3d_toolbox import runtime +import collections + +# Helper functions for logging output: +def msgpack_length(dg, length, fix, maxfix, tag8, tag16, tag32): + if length < maxfix: + dg.addUint8(fix + length) + elif tag8 is not None and length < 1<<8: + dg.addUint8(tag8) + dg.addUint8(length) + elif tag16 is not None and length < 1<<16: + dg.addUint8(tag16) + dg.addBeUint16(length) + elif tag32 is not None and length < 1<<32: + dg.addUint8(tag32) + dg.addBeUint32(length) + else: + raise ValueError('Value too big for MessagePack') + +def msgpack_encode(dg, element): + if element == None: + dg.addUint8(0xc0) + elif element is False: + dg.addUint8(0xc2) + elif element is True: + dg.addUint8(0xc3) + elif isinstance(element, int): + if -32 <= element < 128: + dg.addInt8(element) + elif 128 <= element < 256: + dg.addUint8(0xcc) + dg.addUint8(element) + elif 256 <= element < 65536: + dg.addUint8(0xcd) + dg.addBeUint16(element) + elif 65536 <= element < (1<<32): + dg.addUint8(0xce) + dg.addBeUint32(element) + elif (1<<32) <= element < (1<<64): + dg.addUint8(0xcf) + dg.addBeUint64(element) + elif -128 <= element < -32: + dg.addUint8(0xd0) + dg.addInt8(element) + elif -32768 <= element < -128: + dg.addUint8(0xd1) + dg.addBeInt16(element) + elif -1<<31 <= element < -32768: + dg.addUint8(0xd2) + dg.addBeInt32(element) + elif -1<<63 <= element < -1<<31: + dg.addUint8(0xd3) + dg.addBeInt64(element) + else: + raise ValueError('int out of range for msgpack: %d' % element) + elif isinstance(element, dict): + msgpack_length(dg, len(element), 0x80, 0x10, None, 0xde, 0xdf) + for k,v in list(element.items()): + msgpack_encode(dg, k) + msgpack_encode(dg, v) + elif isinstance(element, list): + msgpack_length(dg, len(element), 0x90, 0x10, None, 0xdc, 0xdd) + for v in element: + msgpack_encode(dg, v) + elif isinstance(element, str): + # 0xd9 is str 8 in all recent versions of the MsgPack spec, but somehow + # Logstash bundles a MsgPack implementation SO OLD that this isn't + # handled correctly so this function avoids it too + msgpack_length(dg, len(element), 0xa0, 0x20, None, 0xda, 0xdb) + dg.appendData(element.encode('utf-8')) + elif isinstance(element, float): + # Python does not distinguish between floats and doubles, so we send + # everything as a double in MsgPack: + dg.addUint8(0xcb) + dg.addBeFloat64(element) + else: + raise TypeError('Encountered non-MsgPack-packable value: %r' % element) + +class EventLoggerInterface(object): + """ + This class provides a simple interface for logging events to the central + Event Logger. It is intended to be used by the AI and UberDOG to log events + that are of interest to the game as a whole. The Event Logger is a separate + server that listens for event messages and logs them to a central database. + """ + + def __init__(self, air: object): + """ + Initialize the Event Logger interface. This will set up the necessary + network connections to the Event Logger server, if one is configured. + """ + + self.air = air + self.eventLogId = runtime.config.GetString('eventlog-id', 'AIR:%d' % self.air.ourChannel) + self.eventSocket = None + + eventLogHost = runtime.config.GetString('eventlog-host', '') + if eventLogHost: + if ':' in eventLogHost: + host, port = eventLogHost.split(':', 1) + self.setEventLogHost(host, int(port)) + else: + self.setEventLogHost(eventLogHost) + + @property + def notify(self) -> object: + """ + Retrieves the parent repositories notify object + """ + + return self.air.notify + + def setEventLogHost(self, host, port=7197): + """ + Set the target host for Event Logger messaging. This should be pointed + at the UDP IP:port that hosts the cluster's running Event Logger. + Providing a value of None or an empty string for 'host' will disable + event logging. + """ + + if not host: + self.eventSocket = None + return + + address = p3d.SocketAddress() + if not address.setHost(host, port): + self.notify.warning('Invalid Event Log host specified: %s:%s' % (host, port)) + self.eventSocket = None + else: + self.eventSocket = p3d.SocketUDPOutgoing() + self.eventSocket.InitToAddress(address) + + set_event_log_host = setEventLogHost + + def writeServerEvent(self, logtype, *args, **kwargs): + """ + Write an event to the central Event Logger, if one is configured. + The purpose of the Event Logger is to keep a game-wide record of all + interesting in-game events that take place. Therefore, this function + should be used whenever such an interesting in-game event occurs. + """ + + if self.eventSocket is not None: + return # No event logger configured! + + log = collections.OrderedDict() + log['type'] = logtype + log['sender'] = self.eventLogId + + for i,v in enumerate(args): + # +1 because the logtype was _0, so we start at _1 + log['_%d' % (i+1)] = v + + log.update(kwargs) + + dg = PyDatagram() + msgpack_encode(dg, log) + self.eventSocket.Send(dg.getMessage()) + + write_server_event = writeServerEvent \ No newline at end of file diff --git a/panda3d_astron/messenger.py b/panda3d_astron/interfaces/messenger.py similarity index 92% rename from panda3d_astron/messenger.py rename to panda3d_astron/interfaces/messenger.py index 3a08265..619a6c9 100644 --- a/panda3d_astron/messenger.py +++ b/panda3d_astron/interfaces/messenger.py @@ -1,22 +1,25 @@ from direct.directnotify import DirectNotifyGlobal from direct.distributed.PyDatagram import PyDatagram from direct.showbase.Messenger import Messenger + from pickle import dumps, loads -class NetMessenger(Messenger): +class NetMessengerInterface(Messenger): """ This works very much like the Messenger class except that messages are sent over the network and (possibly) handled (accepted) on a remote machine (server). """ + notify = DirectNotifyGlobal.directNotify.newCategory('NetMessenger') - def __init__(self, air, baseChannel=20000, baseMsgType=20000): + def __init__(self, air: object, baseChannel: int = 20000, baseMsgType: int = 20000): """ air is the AI Repository. baseChannel is the channel that the first message is sent on. baseMsgType is the MsgType of the same. """ + assert self.notify.debugCall() Messenger.__init__(self) self.air=air @@ -80,22 +83,23 @@ def accept(self, message, *args): Messenger.accept(self, message, *args) - def send(self, message, sentArgs=[]): + def send(self, message: str, sentArgs: list = []) -> None: """ Send message to anything that's listening for it. """ + assert self.notify.debugCall() datagram = self.prepare(message, sentArgs) self.air.send(datagram) Messenger.send(self, message, sentArgs=sentArgs) - def handle(self, msgType, di): + def handle_datagram(self, msgType: int, di: object) -> None: """ Send data from the net on the local netMessenger. """ - assert self.notify.debugCall() + assert self.notify.debugCall() if msgType not in self.__type2message: self.notify.warning('Received unknown message: %d' % msgType) return diff --git a/panda3d_astron/interfaces/state.py b/panda3d_astron/interfaces/state.py new file mode 100644 index 0000000..ca5fced --- /dev/null +++ b/panda3d_astron/interfaces/state.py @@ -0,0 +1,723 @@ +""" +State Server Interface for Astron +""" + +from direct.directnotify import DirectNotifyGlobal +from direct.distributed.PyDatagram import PyDatagram + +from panda3d import direct +from panda3d import core as p3d +from panda3d_astron import msgtypes + +class StateServerInterface(object): + """ + Network interface for working with the state server + """ + + def __init__(self, air: object): + """ + Initializes the state server interface + """ + + self.air = air + self.__callbacks = {} + + @property + def notify(self) -> object: + """ + Retrieves the parent repositories notify object + """ + + return self.air.notify + + def handle_datagram(self, msg_type: int, di: object) -> None: + """ + Handles state server datagrams + """ + + if msg_type in (msgtypes.STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED, + msgtypes.STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER, + msgtypes.STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED, + msgtypes.STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED_OTHER): + other = msg_type == msgtypes.STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER or \ + msg_type == msgtypes.STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED_OTHER + self.handle_object_entry(di, other) + elif msg_type == msgtypes.STATESERVER_OBJECT_ENTER_INTEREST_WITH_REQUIRED: + ctx = di.get_uint32() + self.handle_object_entry(di, False, ctx) + elif msg_type in (msgtypes.STATESERVER_OBJECT_CHANGING_AI, + msgtypes.STATESERVER_OBJECT_DELETE_RAM): + self.handle_object_exit(di) + elif msg_type == msgtypes.STATESERVER_OBJECT_GET_FIELD_RESP: + self.handle_get_field_resp(di) + elif msg_type == msgtypes.STATESERVER_OBJECT_GET_FIELDS_RESP: + self.handle_get_fields_resp(di) + elif msg_type == msgtypes.STATESERVER_OBJECT_CHANGING_LOCATION: + self.handle_obj_location(di) + elif msg_type == msgtypes.STATESERVER_OBJECT_GET_LOCATION_RESP: + self.handle_get_location_resp(di) + elif msg_type == msgtypes.STATESERVER_OBJECT_GET_ALL_RESP: + self.handle_get_object_resp(di) + elif msg_type == msgtypes.DBSS_OBJECT_GET_ACTIVATED_RESP: + self.handle_get_activated_resp(di) + else: + message_name = msgtypes.MsgId2Names.get(msg_type, str(msg_type)) + self.notify.warning('Received unknown state server message: %s' % message_name) + + def create_object_with_required(self, doId: int, parentId: int, zoneId: int, dclass: direct.DCClass, fields: dict) -> None: + """ + Create an object on the State Server, specifying its initial location as (parent_id, zone_id), its class, + and initial field data. The object then broadcasts an ENTER_LOCATION message to its location channel, + and sends a CHANGING_ZONE with old location (0,0) to its parent (if it has one). + + Additionally, the object sends a GET_LOCATION to its children over the parent messages channel (1 << 32|parent_id) with context 1001 (STATESERVER_CONTEXT_WAKE_CHILDREN). + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_CREATE_OBJECT_WITH_REQUIRED) + + dg.add_uint32(doId) + dg.add_uint32(parentId) + dg.add_uint32(zoneId) + dg.add_uint16(dclass.get_number()) + + packer = direct.DCPacker() + for i in range(dclass.get_num_inherited_fields()): + field = dclass.get_inherited_field(i) + if field.is_required() and field.get_name() in fields: + packer.begin_pack(field) + field.pack_args(packer, fields[field.get_name()]) + packer.end_pack() + + dg.append_data(packer.get_bytes()) + self.air.send(dg) + + def create_object_with_required_other(self, doId: int, parentId: int, zoneId: int, dclass: direct.DCClass, fields: dict) -> None: + """ + Create an object on the State Server, specifying its initial location as (parent_id, zone_id), its class, + and initial field data. The object then broadcasts an ENTER_LOCATION message to its location channel, + and sends a CHANGING_ZONE with old location (0,0) to its parent (if it has one). + + Additionally, the object sends a GET_LOCATION to its children over the parent messages channel (1 << 32|parent_id) with context 1001 (STATESERVER_CONTEXT_WAKE_CHILDREN). + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_CREATE_OBJECT_WITH_REQUIRED_OTHER) + + dg.add_uint32(doId) + dg.add_uint32(parentId) + dg.add_uint32(zoneId) + dg.add_uint16(dclass.get_number()) + + packer = direct.DCPacker() + for i in range(dclass.get_num_inherited_fields()): + field = dclass.get_inherited_field(i) + if field.is_required() and field.get_name() in fields: + packer.begin_pack(field) + field.pack_args(packer, fields[field.get_name()]) + packer.end_pack() + + dg.append_data(packer.get_bytes()) + self.air.send(dg) + + def delete_ai_objects(self, channel: int) -> None: + """ + Used by an AI Server to inform the State Server that it is going down. + The State Server will then delete all objects matching the ai_channel. + + The AI will typically hang this on its connected MD using ADD_POST_REMOVE, so that the message goes + out automatically if the AI loses connection unexpectedly. + """ + + dg = PyDatagram() + dg.addServerHeader(self.air.serverId, channel, msgtypes.STATESERVER_DELETE_AI_OBJECTS) + + self.air.send(dg) + + deleteAIObjects = delete_ai_objects + + def get_object_field(self, doId: int, field: str, callback: object) -> None: + """ + Get a single field from an object. + You should already be sure the object actually exists, otherwise the + callback will never be called. + Callback is called as: callback(doId, field, value) + """ + + ctx = self.air.get_context() + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_GET_FIELD) + + dg.add_uint32(ctx) + dg.add_uint32(doId) + dg.add_string(field) + + self.air.send(dg) + + getObjectField = get_object_field + + def handle_get_field_resp(self, di: object) -> None: + """ + Handles STATESERVER_OBJECT_GET_FIELD_RESP messages + """ + + ctx = di.get_uint32() + doId = di.get_uint32() + field = di.get_string() + value = di.get_string() + + if ctx not in self.__callbacks: + self.notify.warning('Received unexpected STATESERVER_OBJECT_GET_FIELD_RESP (ctx: %d)' % ctx) + return + + try: + self.__callbacks[ctx](doId, field, value) + finally: + del self.__callbacks[ctx] + + def get_object_fields(self, doId: int, fields: list, callback: object) -> None: + """ + Get multiple fields from an object. + You should already be sure the object actually exists, otherwise the + callback will never be called. + Callback is called as: callback(doId, fields) + """ + + ctx = self.air.get_context() + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_GET_FIELDS) + + dg.add_uint32(ctx) + dg.add_uint32(doId) + + dg.add_uint16(len(fields)) + for field in fields: + dg.add_string(field) + + self.air.send(dg) + + getObjectFields = get_object_fields + + def handle_get_fields_resp(self, di: object) -> None: + """ + Handles STATESERVER_OBJECT_GET_FIELDS_RESP messages + """ + + ctx = di.get_uint32() + doId = di.get_uint32() + fields = {} + + if ctx not in self.__callbacks: + self.notify.warning('Received unexpected STATESERVER_OBJECT_GET_FIELDS_RESP (ctx: %d)' % ctx) + return + + count = di.get_uint16() + for i in range(count): + field = di.get_string() + value = di.get_string() + fields[field] = value + + try: + self.__callbacks[ctx](doId, fields) + finally: + del self.__callbacks[ctx] + + def get_object(self, doId: int, callback: object) -> None: + """ + Get the entire state of an object. + You should already be sure the object actually exists, otherwise the + callback will never be called. + Callback is called as: callback(doId, parentId, zoneId, dclass, fields) + """ + + ctx = self.air.get_context() + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_GET_ALL) + dg.add_uint32(ctx) + dg.add_uint32(doId) + + self.air.send(dg) + + def handle_get_object_resp(self, di: object) -> None: + """ + Handles STATESERVER_OBJECT_GET_ALL_RESP messages + """ + + ctx = di.get_uint32() + doId = di.get_uint32() + parentId = di.get_uint32() + zoneId = di.get_uint32() + classId = di.get_uint16() + + if ctx not in self.__callbacks: + self.notify.warning('Received unexpected STATESERVER_OBJECT_GET_ALL_RESP (ctx: %d)' % ctx) + return + + if classId not in self.air.dclassesByNumber: + self.notify.warning('Received STATESERVER_OBJECT_GET_ALL_RESP for unknown dclass=%d! (Object %d)' % (classId, doId)) + return + + dclass = self.air.dclassesByNumber[classId] + + fields = {} + unpacker = direct.DCPacker() + unpacker.setUnpackData(di.getRemainingBytes()) + + # Required: + for i in range(dclass.getNumInheritedFields()): + field = dclass.getInheritedField(i) + if not field.isRequired() or field.asMolecularField(): continue + unpacker.beginUnpack(field) + fields[field.getName()] = field.unpackArgs(unpacker) + unpacker.endUnpack() + + # Other: + other = unpacker.rawUnpackUint16() + for i in range(other): + field = dclass.getFieldByIndex(unpacker.rawUnpackUint16()) + unpacker.beginUnpack(field) + fields[field.getName()] = field.unpackArgs(unpacker) + unpacker.endUnpack() + + try: + self.__callbacks[ctx](doId, parentId, zoneId, dclass, fields) + finally: + del self.__callbacks[ctx] + + def set_object_field(self, doId: int, field: str, value: object) -> None: + """ + Set a single field on an object. + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_SET_FIELD) + + dg.add_uint32(doId) + dg.add_string(field) + dg.add_string(value) + + self.air.send(dg) + + setObjectField = set_object_field + + def set_object_fields(self, doId: int, fields: dict) -> None: + """ + Set multiple fields on an object. + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_SET_FIELDS) + + dg.add_uint32(doId) + dg.add_uint16(len(fields)) + for field, value in fields.items(): + dg.add_string(field) + dg.add_string(value) + + self.air.send(dg) + + setObjectFields = set_object_fields + + def delete_object_field(self, doId: int, field: str) -> None: + """ + Delete a single field from an object. + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_DELETE_FIELD) + + dg.add_uint32(doId) + dg.add_string(field) + + self.air.send(dg) + + deleteObjectField = delete_object_field + + def delete_object_fields(self, doId: int, fields: list) -> None: + """ + Delete multiple fields from an object. + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_DELETE_FIELDS) + + dg.add_uint32(doId) + dg.add_uint16(len(fields)) + for field in fields: + dg.add_string(field) + + self.air.send(dg) + + deleteObjectFields = delete_object_fields + + def delete_object(self, doId: int) -> None: + """ + Delete an object from the State Server. + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_DELETE_RAM) + + dg.add_uint32(doId) + + self.air.send(dg) + + deleteObject = delete_object + + def set_location(self, do: object, parentId: int, zoneId: int) -> None: + """ + Send a SET_LOCATION message to the State Server to move the object to + the specified parentId/zoneId. + """ + + # If we've been passed a DistributedObject, extract the doId. + # This allows us to pass either a doId or a DistributedObject. + doId = do + if hasattr(do, 'doId'): + doId = do.doId + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_SET_LOCATION) + dg.add_uint32(parentId) + dg.add_uint32(zoneId) + + self.air.send(dg) + + # Legacy methods for the original Panda3D Distributed Object implementation + setLocation = set_location + sendSetLocation = set_location + + def handle_obj_location(self, di: object) -> None: + """ + Handles STATE_SERVER_OBJECT_CHANGING_LOCATION messages + """ + + doId = di.get_uint32() + parentId = di.get_uint32() + zoneId = di.get_uint32() + + do = self.air.doId2do.get(doId) + + if not do: + self.notify.warning('Received location for unknown doId=%d!' % (doId)) + return + + do.setLocation(parentId, zoneId) + + def handle_object_entry(self, di: object, other: bool, ctx: object = None) -> None: + """ + Handles STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED, STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER, + STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED, and STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED_OTHER messages + + If ctx is provided, the callback will be called with the object information. + """ + + doId = di.get_uint32() + parentId = di.get_uint32() + zoneId = di.get_uint32() + classId = di.get_uint16() + + # Check if we have been provided a callback context. If a context + # was provided we will call the callback with the object information. + if ctx is not None: + if ctx in self.__callbacks: + try: + self.__callbacks[ctx](doId, parentId, zoneId, classId) + finally: + del self.__callbacks[ctx] + + if classId not in self.air.dclassesByNumber: + self.notify.warning('Received entry for unknown dclass=%d! (Object %d)' % (classId, doId)) + return + + if doId in self.air.doId2do: + return # We already know about this object; ignore the entry. + + dclass = self.air.dclassesByNumber[classId] + do = dclass.getClassDef()(self.air) + do.dclass = dclass + do.doId = doId + + # The DO came in off the server, so we do not unregister the channel when it dies: + do.doNotDeallocateChannel = True + self.air.addDOToTables(do, location=(parentId, zoneId)) + + # Now for generation: + do.generate() + if other: + do.updateAllRequiredOtherFields(dclass, di) + else: + do.updateAllRequiredFields(dclass, di) + + def handle_object_exit(self, di: object) -> None: + """ + Handles STATE_SERVER_OBJECT_CHANGING_AI and + STATE_SERVER_OBJECT_DELETE_RAM messages + """ + + doId = di.get_uint32() + if doId not in self.air.doId2do: + self.notify.warning('Received AI exit for unknown object %d' % (doId)) + return + + do = self.air.doId2do[doId] + self.air.removeDOFromTables(do) + do.delete() + do.sendDeleteEvent() + + def get_location(self, doId: int, callback: object) -> None: + """ + Ask a DistributedObject where it is. + You should already be sure the object actually exists, otherwise the + callback will never be called. + Callback is called as: callback(doId, parentId, zoneId) + """ + + ctx = self.air.get_context() + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_GET_LOCATION) + dg.add_uint32(ctx) + + self.air.send(dg) + + def handle_get_location_resp(self, di: object) -> None: + """ + Handles STATESERVER_OBJECT_GET_LOCATION_RESP messages + """ + + ctx = di.get_uint32() + doId = di.get_uint32() + parentId = di.get_uint32() + zoneId = di.get_uint32() + + if ctx not in self.__callbacks: + self.notify.warning('Received unexpected STATESERVER_OBJECT_GET_LOCATION_RESP (ctx: %d)' % ctx) + return + + try: + self.__callbacks[ctx](doId, parentId, zoneId) + finally: + del self.__callbacks[ctx] + + def handle_get_activated_resp(self, di: object) -> None: + """ + Handles DBSS_OBJECT_GET_ACTIVATED_RESP messages + """ + + ctx = di.getUint32() + doId = di.getUint32() + activated = di.getUint8() + + if ctx not in self.__callbacks: + self.notify.warning('Received unexpected DBSS_OBJECT_GET_ACTIVATED_RESP (ctx: %d)' %ctx) + return + + try: + self.__callbacks[ctx](doId, activated) + finally: + del self.__callbacks[ctx] + + def get_activated(self, doId: int, callback: object) -> None: + """ + Ask the Database state server if a DistributedObject is activated. This will fire off + a callback with the result. + """ + + ctx = self.get_context() + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.DBSS_OBJECT_GET_ACTIVATED) + dg.addUint32(ctx) + dg.addUint32(doId) + + self.air.send(dg) + + + def register_delete_ai_objects_post_remove(self, server_id :int) -> None: + """ + Registers the delete_ai_objects method to be called when the AI disconnects. + """ + + dg = PyDatagram() + dg.addServerHeader(server_id, self.air.ourChannel, msgtypes.STATESERVER_DELETE_AI_OBJECTS) + + dg.addChannel(self.air.ourChannel) + self.air.add_post_remove(dg) + + registerDeleteAIObjectsPostRemove = register_delete_ai_objects_post_remove + + def request_delete(self, do: object) -> None: + """ + Request the deletion of an object that already exists on the State Server. + You should use do.requestDelete() instead. This is not meant to be + called directly unless you really know what you are doing. + """ + + dg = PyDatagram() + dg.addServerHeader(do.doId, self.ourChannel, msgtypes.STATESERVER_OBJECT_DELETE_RAM) + dg.ad_uint32(do.doId) + self.air.send(dg) + + requestDelete = request_delete + + def set_owner(self, doId: int, newOwner: int) -> None: + """ + Sets the owner of a DistributedObject. This will enable the new owner to send "ownsend" fields, + and will generate an OwnerView. + """ + + dg = PyDatagram() + dg.addServerHeader(doId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_SET_OWNER) + dg.add_uint64(newOwner) + self.air.send(dg) + + setOwner = set_owner + + def set_ai(self, doId: int, aiChannel: int) -> None: + """ + Sets the AI of the specified DistributedObjectAI to be the specified channel. + Generally, you should not call this method, and instead call DistributedObjectAI.setAI. + """ + + dg = PyDatagram() + dg.addServerHeader(doId, aiChannel, msgtypes.STATESERVER_OBJECT_SET_AI) + dg.add_uint64(aiChannel) + self.air.send(dg) + + setAI = set_ai + + def generate_with_required(self, do: object, parentId: int, zoneId: int, optionalFields: list = []) -> None: + """ + Generate an object onto the State Server, choosing an ID from the pool. + You should use do.generateWithRequired(...) instead. This is not meant + to be called directly unless you really know what you are doing. + """ + + doId = self.air.allocate_channel() + self.generate_with_required_and_id(do, doId, parentId, zoneId, optionalFields) + + generateWithRequired = generate_with_required + + def generate_with_required_and_id(self, do: object, doId: int, parentId: int, zoneId: int, optionalFields: list = []) -> None: + """ + Generate an object onto the State Server, specifying its ID and location. + You should use do.generateWithRequiredAndId(...) instead. This is not + meant to be called directly unless you really know what you are doing. + """ + + do.doId = doId + self.air.addDOToTables(do, location=(parentId, zoneId)) + do.sendGenerateWithRequired(self.air, parentId, zoneId, optionalFields) + + generateWithRequiredAndId = generate_with_required_and_id + + def get_zone_objects(self, parentId: int, zoneId: int, callback: object = None) -> None: + """ + Get all child objects in one or more zones from a single object. The parent will reply immediately with a GET_{ZONE,ZONES,CHILD}_COUNT_RESP message. + Each object will reply with a STATESERVER_OBJECT_ENTER_LOCATION message. + + Note: If a shard crashes the number of objects may not be correct, as such a client (for ADD_INTEREST) or AI/Uberdog (in the general case) should stop waiting after a reasonable timeout. + In some cases, it may be acceptable or even preferred to not wait for all responses to come in and just act on objects as they come in. + """ + + ctx = self.air.get_context() + if callback != None: + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(parentId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_GET_ZONE_OBJECTS) + dg.add_uint32(ctx) + dg.add_uint32(parentId) + dg.add_uint32(zoneId) + + self.air.send(dg) + + getZoneObjects = get_zone_objects + + def get_zones_objects(self, parentId: int, zones: list, callback: object = None) -> None: + """ + Get all child objects in one or more zones from a single object. The parent will reply immediately with a GET_{ZONE,ZONES,CHILD}_COUNT_RESP message. + Each object will reply with a STATESERVER_OBJECT_ENTER_LOCATION message. + + Note: If a shard crashes the number of objects may not be correct, as such a client (for ADD_INTEREST) or AI/Uberdog (in the general case) should stop waiting after a reasonable timeout. + In some cases, it may be acceptable or even preferred to not wait for all responses to come in and just act on objects as they come in. + """ + + ctx = self.air.get_context() + if callback != None: + self.__callbacks[ctx] = callback + + dg = PyDatagram() + dg.addServerHeader(parentId, self.air.ourChannel, msgtypes.STATESERVER_OBJECT_GET_ZONES_OBJECTS) + dg.add_uint32(ctx) + dg.add_uint32(parentId) + + dg.add_uint16(len(zones)) + for zone in zones: + dg.add_uint32(zone) + + self.air.send(dg) + + def send_activate(self, doId, parentId, zoneId, dclass=None, fields=None) -> None: + """ + Activate a DBSS object, given its doId, into the specified parentId/zoneId. + If both dclass and fields are specified, an ACTIVATE_WITH_DEFAULTS_OTHER + will be sent instead. In other words, the specified fields will be + auto-applied during the activation. + """ + + fieldPacker = direct.DCPacker() + fieldCount = 0 + if dclass and fields: + for k,v in fields.items(): + field = dclass.getFieldByName(k) + if not field: + self.notify.error('Activation request for %s object contains ' + 'invalid field named %s' % (dclass.getName(), k)) + + fieldPacker.rawPackUint16(field.getNumber()) + fieldPacker.beginPack(field) + field.packArgs(fieldPacker, v) + fieldPacker.endPack() + fieldCount += 1 + + dg = PyDatagram() + dg.addServerHeader(doId, self.ourChannel, msgtypes.DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS) + dg.addUint32(doId) + dg.addUint32(0) + dg.addUint32(0) + self.send(dg) + + # DEFAULTS_OTHER isn't implemented yet, so we chase it with a SET_FIELDS + dg = PyDatagram() + dg.addServerHeader(doId, self.ourChannel, msgtypes.STATESERVER_OBJECT_SET_FIELDS) + dg.addUint32(doId) + dg.addUint16(fieldCount) + dg.appendData(fieldPacker.getBytes()) + self.send(dg) + + # Now slide it into the zone we expect to see it in (so it + # generates onto us with all of the fields in place) + dg = PyDatagram() + dg.addServerHeader(doId, self.ourChannel, msgtypes.STATESERVER_OBJECT_SET_LOCATION) + dg.addUint32(parentId) + dg.addUint32(zoneId) + self.send(dg) + else: + dg = PyDatagram() + dg.addServerHeader(doId, self.ourChannel, msgtypes.DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS) + dg.addUint32(doId) + dg.addUint32(parentId) + dg.addUint32(zoneId) + self.send(dg) + + sendActivate = send_activate \ No newline at end of file diff --git a/panda3d_astron/internal.py b/panda3d_astron/internal.py new file mode 100644 index 0000000..ec14ebb --- /dev/null +++ b/panda3d_astron/internal.py @@ -0,0 +1,543 @@ +""" +This module contains the AstronInternalRepository class, which is the main class +for the Astron internal repository. This class is used to create AI servers and +UberDOG servers using Panda3D. It interfaces with an Astron server to manipulate +objects in the Astron cluster. It does not require any specific "gateway" into the +Astron network. Rather, it just connects directly to any Message Director. Hence, +it is an "internal" repository. +""" + +import os + +from direct.directnotify import DirectNotifyGlobal +from direct.distributed.PyDatagram import PyDatagram +from direct.distributed.MsgTypes import * +from direct.showbase import ShowBase # __builtin__.config +from direct.task.TaskManagerGlobal import * # taskMgr +from direct.distributed.ConnectionRepository import ConnectionRepository + +from panda3d import core as p3d +from panda3d_astron import msgtypes +from panda3d_astron.interfaces import clientagent, database +from panda3d_astron.interfaces import events, state +from panda3d_astron.interfaces import messenger as net_messenger +from panda3d_toolbox import runtime, utils + +class AstronInternalRepository(ConnectionRepository): + """ + This class is part of Panda3D's new MMO networking framework. + It interfaces with an Astron (https://github.com/Astron/Astron) server in + order to manipulate objects in the Astron cluster. It does not require any + specific "gateway" into the Astron network. Rather, it just connects directly + to any Message Director. Hence, it is an "internal" repository. + + This class is suitable for constructing your own AI Servers and UberDOG servers + using Panda3D. Objects with a "self.air" attribute are referring to an instance + of this class. + """ + + notify = DirectNotifyGlobal.directNotify.newCategory("repository") + + def __init__(self, baseChannel, maxChannels = 1000000, serverId = None, dcFileNames = None, + dcSuffix = 'AI', connectMethod = None, threadedNet = None): + """ + Initializes the Astron internal repository. + """ + + # If no connection method was define assumed we should use + # the native connection method. This is the most performant + # option for connecting to the Message Director. + if connectMethod is None: + connectMethod = self.CM_NATIVE + + # Iniitlaize our base connection repository + # instance with the provided connection method and + # threading options. + self.interfacesReady = False + ConnectionRepository.__init__(self, + connectMethod = connectMethod, + config = runtime.config, + hasOwnerView = False, + threadedNet = threadedNet) + + self.setClientDatagram(False) + self.dcSuffix = dcSuffix + + if hasattr(self, 'setVerbose'): + if self.config.GetBool('verbose-internalrepository'): + self.setVerbose(1) + + # The State Server we are configured to use for creating objects. + # If this is None, generating objects is not possible. + self.serverId = self.config.GetInt('air-stateserver', 0) or None + if serverId is not None: + self.serverId = serverId + self.notify.info('Using State Server %d for object creation' % self.serverId) + + # Setup our channel allocator and register our own channel for communicating + # our selves to the larger Astron cluster. + self.minChannel = baseChannel + self.maxChannel = self.config.GetInt('air-channel-allocation', maxChannels) + self.notify.info(f"Dynamic channel range [{self.minChannel}, {self.maxChannel}]. totaling {self.maxChannel - self.minChannel +1} slots.") + assert self.maxChannel >= self.minChannel + self.channelAllocator = p3d.UniqueIdAllocator(baseChannel, baseChannel + self.maxChannels - 1) + + self._registeredChannels = set() + self.ourChannel = self.allocateChannel() + + self.__context_counter = 0 + self.__message_counter = 0 + + # Initialize our interface for communicating with Astron + # server components. + self.database = database.AstronDatabaseInterface(self) + self.net_messenger = net_messenger.NetMessengerInterface(self) + self.state_server = state.StateServerInterface(self) + self.client_agent = clientagent.ClientAgentInterface(self) + self.events = events.EventLoggerInterface(self) + self.interfacesReady = True + + # Load the DC files if they were provided. + self.readDCFile(dcFileNames) + + @property + def our_channel(self) -> int: + """ + Gets the channel this AIR is operating on. + + Serves as a legacy bridge to the original ourChannel value + """ + + return self.ourChannel + + def __getattr__(self, key: str) -> object: + """ + Custom getattr method to allow for easy access to the interfaces. This also + serves as a legacy bridge from the old way of commanding the AIR to the new way. + """ + + # Attempt to retrieve the key on our own object first. If they key + # does not exist attempt to retrieve it from one of our interfaces. + try: + return object.__getattribute__(self, key) + except AttributeError: + # TODO: Should this be a flag we have to manange or should we + # assume if we've gotten here and the key matches any of our interface attribute names + # that the interfaces are not ready and we should raise an exception? + if not self.interfacesReady: + raise + + # Check if the key is a valid attribute of any of our interfaces. + # If it is then return the attribute. + if hasattr(self.database, key): + getattr(self.database, key) + elif hasattr(self.net_messenger, key): + return getattr(self.net_messenger, key) + elif hasattr(self.state_server, key): + return getattr(self.state_server, key) + elif hasattr(self.client_agent, key): + return getattr(self.client_agent, key) + elif hasattr(self.events, key): + return getattr(self.events, key) + except: + # An unexpected error occurred while attempting to retrieve the attribute. + # We should simply raise the error for normal handling. + raise + + def does_dc_suffix_match(self, suffix: str) -> bool: + """ + Returns whether the repository's DC suffix matches the provided suffix. + + This function will ignore case when comparing the suffixes. + """ + + lower_suffix = self.dcSuffix.lower() + lower_match_suffix = suffix.lower() + + return lower_suffix == lower_match_suffix + + doesDCSuffixMatch = does_dc_suffix_match + + def is_uberdog(self) -> bool: + """ + Returns whether the repository is an UberDOG server or not + """ + + return self.does_dc_suffix_match('UD') + + isUberDOG = is_uberdog + + def is_ai(self) -> bool: + """ + Returns whether the repository is an AI server or not + """ + + return self.does_dc_suffix_match('AI') + + isAI = is_ai + + def get_game_do_id(self) -> int: + """ + Gets the distributed id of the root game object as defined by + the legacy GameDoId variable. + """ + + return self.getGameDoId() + + def get_context(self) -> int: + """ + Get a new context ID for a callback. + """ + + self.__context_counter = (self.__context_counter + 1) & 0xFFFFFFFF + return self.__context_counter + + getContext = get_context + + def allocate_channel(self) -> None: + """ + Allocate an unused channel out of this AIR's configured channel space. + This is also used to allocate IDs for DistributedObjects, since those + occupy a channel. + """ + + return self.channelAllocator.allocate() + + allocateChannel = allocate_channel + + def deallocate_channel(self, channel: int) -> None: + """ + Return the previously-allocated channel back to the allocation pool. + """ + + self.channelAllocator.free(channel) + + deallocateChannel = deallocate_channel + + def get_location_channel(self, parentId: int, zoneId: int) -> int: + """ + Get a location channel for the specified parent and zone. This is used + to receive updates from the State Server about objects in a specific zone. + """ + + return (parentId << 32) | zoneId + + getLocationChannel = get_location_channel + + def register_for_channel(self, channel: int) -> None: + """ + Register for messages on a specific Message Director channel. + If the channel is already open by this AIR, nothing will happen. + """ + + if channel in self._registeredChannels: + return + self._registeredChannels.add(channel) + + dg = PyDatagram() + dg.addServerControlHeader(msgtypes.CONTROL_ADD_CHANNEL) + dg.addChannel(channel) + self.send(dg) + + registerForChannel = register_for_channel + + def register_for_location_channel(self, parentId: int, zoneId: int) -> None: + """ + Register for messages on a specific state server zone channel. + If the channel is already open by this AIR, nothing will happen. + """ + + channel = self.get_location_channel(parentId, zoneId) + self.registerForChannel(channel) + + registerForLocationChannel = register_for_location_channel + + def unregister_for_channel(self, channel: int) -> None: + """ + Unregister a channel subscription on the Message Director. The Message + Director will cease to relay messages to this AIR sent on the channel. + """ + + if channel not in self._registeredChannels: + return + self._registeredChannels.remove(channel) + + dg = PyDatagram() + dg.addServerControlHeader(msgtypes.CONTROL_REMOVE_CHANNEL) + dg.addChannel(channel) + self.send(dg) + + unregisterForChannel = unregister_for_channel + + def unregister_for_location_channel(self, parentId: int, zoneId: int) -> None: + """ + Unregister a location channel subscription on the Message Director. The Message + Director will cease to relay messages to this AIR sent on the location channel. + """ + + channel = self.get_location_channel(parentId, zoneId) + self.unregisterForChannel(channel) + + unregisterForLocationChannel = unregister_for_location_channel + + def subscribe_to_zone(self, zoneId: int, parentId: int = None, callback: object = None) -> None: + """ + Subscribes the server to a network zone and requests all objects in the zone. This is useful + for UberDOG servers to allow them to receive UD objects in a specific zone. The channel subscription + is followed up with a get_zone_objects request to ensure we know about all objects already in the zone. + """ + + # If our zone id came from an enum then retrieve the value of it. + if hasattr(zoneId, 'value'): + zoneId = zoneId.value + + # if we were not given a parent then assume we want to subscribe + # to the root global object's children. + if parentId is None: + parentId = self.getGameDoId() + + self.notify.info(f'Subscribing to network zone ({zoneId}) under parent {parentId}') + self.register_for_location_channel(self.getGameDoId(), zoneId) + + self.state_server.get_zone_objects( + parentId=parentId, + zoneId=zoneId, + callback=callback) + + subscribeToZone = subscribe_to_zone + + def add_post_remove(self, dg: object) -> None: + """ + Register a datagram with the Message Director that gets sent out if the + connection is ever lost. + This is useful for registering cleanup messages: If the Panda3D process + ever crashes unexpectedly, the Message Director will detect the socket + close and automatically process any post-remove datagrams. + """ + + dg2 = PyDatagram() + dg2.addServerControlHeader(msgtypes.CONTROL_ADD_POST_REMOVE) + dg2.add_uint64(self.ourChannel) + dg2.add_blob(dg.getMessage()) + + self.send(dg2) + + addPostRemove = add_post_remove + + def clear_post_remove(self) -> None: + """ + Clear all datagrams registered with addPostRemove. + This is useful if the Panda3D process is performing a clean exit. It may + clear the "emergency clean-up" post-remove messages and perform a normal + exit-time clean-up instead, depending on the specific design of the game. + """ + + dg = PyDatagram() + dg.addServerControlHeader(msgtypes.CONTROL_CLEAR_POST_REMOVES) + dg.add_uint64(self.ourChannel) + self.send(dg) + + clearPostRemove = clear_post_remove + + def get_account_id_from_channel(self, channel: int) -> int: + """ + Get the account ID of the client connected to the specified channel. + """ + + return (channel >> 32) & 0xFFFFFFFF + + getAccountIdFromChannel = get_account_id_from_channel + + def get_avatar_id_from_channel(self, channel: int) -> int: + """ + Get the avatar ID of the client connected to the specified channel. + """ + + return channel & 0xFFFFFFFF + + getAvatarIdFromChannel = get_avatar_id_from_channel + + def get_account_id_from_sender(self) -> int: + """ + Get the account ID of the sender of the current message. This only works if the + client agent set_client_account_id was called previously. + """ + + return self.get_account_id_from_channel(self.getMsgSender()) + + getAccountIdFromSender = get_account_id_from_sender + + def get_avatar_id_from_sender(self) -> int: + """ + Get the avatar ID of the sender of the current message. This only works if the + client agent set_client_id was called previously. + """ + + return self.get_avatar_id_from_channel(self.getMsgSender()) + + getAvatarIdFromSender = get_avatar_id_from_sender + + def register_net_messager_event(self, message: int) -> None: + """ + Registers a new event with the NetMessenger. This is useful for + broadcasting messenger events across the entire internal cluster. + """ + + self.net_messenger.register(self.__message_counter, message) + self.__message_counter += 1 + + def handle_datagram(self, di: object) -> None: + """ + Handle a datagram received from the Message Director. + """ + + msg_type = self.getMsgType() + if msgtypes.is_state_server_message(msg_type) or \ + msgtypes.is_database_state_server_message(msg_type): + self.state_server.handle_datagram(msg_type, di) + elif msgtypes.is_database_server_message(msg_type): + self.database.handle_datagram(msg_type, di) + elif msgtypes.is_client_agent_message(msg_type): + self.client_agent.handle_datagram(msg_type, di) + elif msgtypes.is_net_messenger_message(msg_type): + self.net_messenger.handle_datagram(msg_type, di) + else: + self.notify.warning('Received message with unknown MsgType=%d' % msg_type) + + handleDatagram = handle_datagram + + def send_update(self, do, fieldName, args): + """ + Send a field update for the given object. + You should use do.sendUpdate(...) instead. This is not meant to be + called directly unless you really know what you are doing. + """ + + self.send_update_to_channel(do, do.doId, fieldName, args) + + sendUpdate = send_update + + def send_update_to_channel(self, do, channelId, fieldName, args): + """ + Send an object field update to a specific channel. + This is useful for directing the update to a specific client or node, + rather than at the State Server managing the object. + You should use do.sendUpdateToChannel(...) instead. This is not meant + to be called directly unless you really know what you are doing. + """ + + dclass = do.dclass + field = dclass.getFieldByName(fieldName) + dg = field.aiFormatUpdate(do.doId, channelId, self.ourChannel, args) + self.send(dg) + + sendUpdateToChannel = send_update_to_channel + + def connect(self, host: str, port: int = 7199) -> None: + """ + Connect to a Message Director. The airConnected message is sent upon + success. + N.B. This overrides the base class's connect(). You cannot use the + ConnectionRepository connect() parameters. + """ + + url = p3d.URLSpec() + url.set_server(host) + url.set_port(port) + + self.notify.info('Now connecting to %s:%s...' % (host, port)) + ConnectionRepository.connect(self, [url], + successCallback=self.__connected, + failureCallback=self.__connect_failed, + failureArgs=[host, port]) + + def __connected(self) -> None: + """ + Handle a successful connection. + """ + + # Listen to our channel... + self.notify.info('Connected successfully.') + self.register_for_channel(self.ourChannel) + + # If we're configured with a State Server, register a post-remove to + # clean up whatever objects we own on this server should we unexpectedly + # fall over and die. + if self.serverId: + self.state_server.register_delete_ai_objects_post_remove(self.serverId) + + runtime.messenger.send('airConnected') + self.handle_connected() + + def __connect_failed(self, code: int, explanation: str, host: str, port: int) -> None: + """ + Handle a failed connection attempt. + """ + + self.notify.warning('Failed to connect! (code=%s; %r)' % (code, explanation)) + retryInterval = runtime.config.GetFloat('air-reconnect-delay', 5.0) + taskMgr.doMethodLater(retryInterval, self.connect, 'Reconnect delay', extraArgs=[host, port]) + + def handle_connected(self): + """ + Subclasses should override this if they wish to handle the connection + event. + """ + + def lost_connection(self) -> None: + """ + Handle a lost connection to the Message Director. + """ + + # This should be overridden by a subclass if unexpectedly losing connection + # is okay. + self.notify.error('Lost connection to gameserver!') + + lostConnection = lost_connection # Legacy carry-over + + def generate_global_object_if_configured(self, object_id: int, object_name: str, config_name: str = '', guarantee: bool = False) -> object: + """ + Generates a distributed object global if it was requested at startup. This is used for + generating certain global objects across different instances of the UberDOG server. + + When this method is called by an AI repository instance it is guaranteed to generate the object + to ensure the AI can properly communicate with whichever UberDOG server the object is on. + + An optional guarantee flag can be set to force the object to be generated regardless of the configuration. + """ + + # Verify the requested object exists in our dclass schema + dclass_name = f'{object_name}{self.dcSuffix}' + if not self.dclassesByName.get(dclass_name): + self.notify.warning(f'Could not find dclass for global object {object_name}') + return + + # If our object id came from an int enum then retrieve + # the value of it. + if hasattr(object_id, 'value'): + object_id = object_id.value + + # If the config name was not supplied then generate it from the object name. + # It should match the name of the object turned snake case and upper case. E.g. + # DistributedAccountManager -> DISTRIBUTED_ACCOUNT_MANAGER + if config_name == '': + config_name = utils.get_snake_case(object_name) + config_name = config_name.upper() + + # Check if the object's environment variable flag was set on startup, + # we are running in development mode, or this repository represents an AI server. If any + # of these conditions are met then we should generate the object. + should_generate = int(os.environ.get(config_name, '0')) or guarantee or self.is_ai() + if should_generate or runtime.__dev__: + self.notify.info(f'Generating global object {object_name} with id {object_id}') + return self.generate_global_object(object_id, object_name) + + return None + + # Exists as a legacy bridge to the original Panda3D generateGlobalObject method + def generate_global_object(self, doId: int, dcname: str, values: list = None) -> object: + """ + Generates a global object with the specified doId and dclass name. + """ + + return self.generateGlobalObject(doId, dcname, values) \ No newline at end of file diff --git a/panda3d_astron/msgtypes.py b/panda3d_astron/msgtypes.py index 22be59c..2d0fd9f 100644 --- a/panda3d_astron/msgtypes.py +++ b/panda3d_astron/msgtypes.py @@ -1,168 +1,344 @@ -"""MsgTypes module: contains distributed object message types""" +""" +MsgTypes module: contains distributed object message types +""" from direct.showbase.PythonUtil import invertDictLossless +# Client connection management messages. +CLIENT_HELLO = 1 +CLIENT_HELLO_RESP = 2 +CLIENT_DISCONNECT = 3 +CLIENT_EJECT = 4 +CLIENT_HEARTBEAT = 5 + +# These are custom messages added ontop of Astron for the purposes +# of extending the client protocol. They use CLIENTAGENT_SEND_DATAGRAM +# internal message to broadcast these to the client. +CLIENT_SYSTEM_MESSAGE = 6 + +# Client object management messages. +CLIENT_OBJECT_SET_FIELD = 120 +CLIENT_OBJECT_SET_FIELDS = 121 +CLIENT_OBJECT_LEAVING = 132 +CLIENT_OBJECT_LOCATION = 140 +CLIENT_ENTER_OBJECT_REQUIRED = 142 +CLIENT_ENTER_OBJECT_REQUIRED_OTHER = 143 +CLIENT_OBJECT_LEAVING_OWNER = 161 +CLIENT_ENTER_OBJECT_REQUIRED_OWNER = 172 +CLIENT_ENTER_OBJECT_REQUIRED_OTHER_OWNER = 173 + +# Client interest management messages. +CLIENT_ADD_INTEREST = 200 +CLIENT_ADD_INTEREST_MULTIPLE = 201 +CLIENT_REMOVE_INTEREST = 203 +CLIENT_DONE_INTEREST_RESP = 204 + +# These are sent internally inside the Astron cluster. +# Message Director control messages +CONTROL_CHANNEL = 1 +CONTROL_ADD_CHANNEL = 9000 +CONTROL_REMOVE_CHANNEL = 9001 +CONTROL_ADD_RANGE = 9002 +CONTROL_REMOVE_RANGE = 9003 +CONTROL_ADD_POST_REMOVE = 9010 +CONTROL_CLEAR_POST_REMOVES = 9011 + +# State Server control messages +STATESERVER_CREATE_OBJECT_WITH_REQUIRED = 2000 +STATESERVER_CREATE_OBJECT_WITH_REQUIRED_OTHER = 2001 + +STATESERVER_DELETE_AI_OBJECTS = 2009 +STATESERVER_OBJECT_GET_FIELD = 2010 +STATESERVER_OBJECT_GET_FIELD_RESP = 2011 +STATESERVER_OBJECT_GET_FIELDS = 2012 +STATESERVER_OBJECT_GET_FIELDS_RESP = 2013 +STATESERVER_OBJECT_GET_ALL = 2014 +STATESERVER_OBJECT_GET_ALL_RESP = 2015 + +STATESERVER_OBJECT_SET_FIELD = 2020 +STATESERVER_OBJECT_SET_FIELDS = 2021 +STATESERVER_OBJECT_DELETE_FIELD_RAM = 2030 +STATESERVER_OBJECT_DELETE_FIELDS_RAM = 2031 +STATESERVER_OBJECT_DELETE_RAM = 2032 + +STATESERVER_OBJECT_SET_LOCATION = 2040 +STATESERVER_OBJECT_CHANGING_LOCATION = 2041 +STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED = 2042 +STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED_OTHER = 2043 +STATESERVER_OBJECT_GET_LOCATION = 2044 +STATESERVER_OBJECT_GET_LOCATION_RESP = 2045 +STATESERVER_OBJECT_LOCATION_ACK = 2046 + +STATESERVER_OBJECT_SET_AI = 2050 +STATESERVER_OBJECT_CHANGING_AI = 2051 +STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED = 2052 +STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER = 2053 +STATESERVER_OBJECT_GET_AI = 2054 +STATESERVER_OBJECT_GET_AI_RESP = 2055 + +STATESERVER_OBJECT_SET_OWNER = 2060 +STATESERVER_OBJECT_CHANGING_OWNER = 2061 +STATESERVER_OBJECT_ENTER_OWNER_WITH_REQUIRED = 2062 +STATESERVER_OBJECT_ENTER_OWNER_WITH_REQUIRED_OTHER = 2063 +STATESERVER_OBJECT_GET_OWNER = 2064 +STATESERVER_OBJECT_GET_OWNER_RESP = 2065 +STATESERVER_OBJECT_ENTER_INTEREST_WITH_REQUIRED = 2066 + +STATESERVER_OBJECT_GET_ZONE_OBJECTS = 2100 +STATESERVER_OBJECT_GET_ZONES_OBJECTS = 2102 +STATESERVER_OBJECT_GET_CHILDREN = 2104 + +STATESERVER_OBJECT_GET_ZONE_COUNT = 2110 +STATESERVER_OBJECT_GET_ZONE_COUNT_RESP = 2111 +STATESERVER_OBJECT_GET_ZONES_COUNT = 2112 +STATESERVER_OBJECT_GET_ZONES_COUNT_RESP = 2113 +STATESERVER_OBJECT_GET_CHILD_COUNT = 2114 +STATESERVER_OBJECT_GET_CHILD_COUNT_RESP = 2115 + +STATESERVER_OBJECT_DELETE_ZONE = 2120 +STATESERVER_OBJECT_DELETE_ZONES = 2122 +STATESERVER_OBJECT_DELETE_CHILDREN = 2124 +STATESERVER_GET_ACTIVE_ZONES = 2125 +STATESERVER_GET_ACTIVE_ZONES_RESP = 2126 + +# DBSS-backed-object messages +DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS = 2200 +DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS_OTHER = 2201 +DBSS_OBJECT_GET_ACTIVATED = 2207 +DBSS_OBJECT_GET_ACTIVATED_RESP = 2208 +DBSS_OBJECT_DELETE_FIELD_DISK = 2230 +DBSS_OBJECT_DELETE_FIELDS_DISK = 2231 +DBSS_OBJECT_DELETE_DISK = 2232 + +# Database Server control messages +DBSERVER_CREATE_OBJECT = 3000 +DBSERVER_CREATE_OBJECT_RESP = 3001 +DBSERVER_OBJECT_GET_FIELD = 3010 +DBSERVER_OBJECT_GET_FIELD_RESP = 3011 +DBSERVER_OBJECT_GET_FIELDS = 3012 +DBSERVER_OBJECT_GET_FIELDS_RESP = 3013 +DBSERVER_OBJECT_GET_ALL = 3014 +DBSERVER_OBJECT_GET_ALL_RESP = 3015 +DBSERVER_OBJECT_SET_FIELD = 3020 +DBSERVER_OBJECT_SET_FIELDS = 3021 +DBSERVER_OBJECT_SET_FIELD_IF_EQUALS = 3022 +DBSERVER_OBJECT_SET_FIELD_IF_EQUALS_RESP = 3023 +DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS = 3024 +DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS_RESP = 3025 +DBSERVER_OBJECT_SET_FIELD_IF_EMPTY = 3026 +DBSERVER_OBJECT_SET_FIELD_IF_EMPTY_RESP = 3027 +DBSERVER_OBJECT_DELETE_FIELD = 3030 +DBSERVER_OBJECT_DELETE_FIELDS = 3031 +DBSERVER_OBJECT_DELETE = 3032 + +# Client Agent control messages +CLIENTAGENT_SET_STATE = 1000 +CLIENTAGENT_SET_CLIENT_ID = 1001 +CLIENTAGENT_SEND_DATAGRAM = 1002 +CLIENTAGENT_EJECT = 1004 +CLIENTAGENT_DROP = 1005 +CLIENTAGENT_GET_NETWORK_ADDRESS = 1006 +CLIENTAGENT_GET_NETWORK_ADDRESS_RESP = 1007 + +CLIENTAGENT_DECLARE_OBJECT = 1010 +CLIENTAGENT_UNDECLARE_OBJECT = 1011 +CLIENTAGENT_ADD_SESSION_OBJECT = 1012 +CLIENTAGENT_REMOVE_SESSION_OBJECT = 1013 +CLIENTAGENT_SET_FIELDS_SENDABLE = 1014 +CLIENTAGENT_GET_TLVS = 1015 +CLIENTAGENT_GET_TLVS_RESP = 1016 + +CLIENTAGENT_OPEN_CHANNEL = 1100 +CLIENTAGENT_CLOSE_CHANNEL = 1101 +CLIENTAGENT_ADD_POST_REMOVE = 1110 +CLIENTAGENT_CLEAR_POST_REMOVES = 1111 +CLIENTAGENT_ADD_INTEREST = 1200 +CLIENTAGENT_ADD_INTEREST_MULTIPLE = 1201 +CLIENTAGENT_REMOVE_INTEREST = 1203 +CLIENTAGENT_DONE_INTEREST_RESP = 1204 + MsgName2Id = { - 'CLIENT_HELLO': 1, - 'CLIENT_HELLO_RESP': 2, - - # Sent by the client when it's leaving. - 'CLIENT_DISCONNECT': 3, - - # Sent by the server when it is dropping the connection deliberately. - 'CLIENT_EJECT': 4, - - 'CLIENT_HEARTBEAT': 5, - - 'CLIENT_OBJECT_SET_FIELD': 120, - 'CLIENT_OBJECT_SET_FIELDS': 121, - 'CLIENT_OBJECT_LEAVING': 132, - 'CLIENT_OBJECT_LEAVING_OWNER': 161, - 'CLIENT_ENTER_OBJECT_REQUIRED': 142, - 'CLIENT_ENTER_OBJECT_REQUIRED_OTHER': 143, - 'CLIENT_ENTER_OBJECT_REQUIRED_OWNER': 172, - 'CLIENT_ENTER_OBJECT_REQUIRED_OTHER_OWNER': 173, - - 'CLIENT_DONE_INTEREST_RESP': 204, - - 'CLIENT_ADD_INTEREST': 200, - 'CLIENT_ADD_INTEREST_MULTIPLE': 201, - 'CLIENT_REMOVE_INTEREST': 203, - 'CLIENT_OBJECT_LOCATION': 140, - - - # These are sent internally inside the Astron cluster. - - # Message Director control messages: - 'CONTROL_CHANNEL': 1, - 'CONTROL_ADD_CHANNEL': 9000, - 'CONTROL_REMOVE_CHANNEL': 9001, - 'CONTROL_ADD_RANGE': 9002, - 'CONTROL_REMOVE_RANGE': 9003, - 'CONTROL_ADD_POST_REMOVE': 9010, - 'CONTROL_CLEAR_POST_REMOVES': 9011, - - # State Server control messages: - 'STATESERVER_CREATE_OBJECT_WITH_REQUIRED': 2000, - 'STATESERVER_CREATE_OBJECT_WITH_REQUIRED_OTHER': 2001, - 'STATESERVER_DELETE_AI_OBJECTS': 2009, - 'STATESERVER_OBJECT_GET_FIELD': 2010, - 'STATESERVER_OBJECT_GET_FIELD_RESP': 2011, - 'STATESERVER_OBJECT_GET_FIELDS': 2012, - 'STATESERVER_OBJECT_GET_FIELDS_RESP': 2013, - 'STATESERVER_OBJECT_GET_ALL': 2014, - 'STATESERVER_OBJECT_GET_ALL_RESP': 2015, - 'STATESERVER_OBJECT_SET_FIELD': 2020, - 'STATESERVER_OBJECT_SET_FIELDS': 2021, - 'STATESERVER_OBJECT_DELETE_FIELD_RAM': 2030, - 'STATESERVER_OBJECT_DELETE_FIELDS_RAM': 2031, - 'STATESERVER_OBJECT_DELETE_RAM': 2032, - 'STATESERVER_OBJECT_SET_LOCATION': 2040, - 'STATESERVER_OBJECT_CHANGING_LOCATION': 2041, - 'STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED': 2042, - 'STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED_OTHER': 2043, - 'STATESERVER_OBJECT_GET_LOCATION': 2044, - 'STATESERVER_OBJECT_GET_LOCATION_RESP': 2045, - 'STATESERVER_OBJECT_SET_AI': 2050, - 'STATESERVER_OBJECT_CHANGING_AI': 2051, - 'STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED': 2052, - 'STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER': 2053, - 'STATESERVER_OBJECT_GET_AI': 2054, - 'STATESERVER_OBJECT_GET_AI_RESP': 2055, - 'STATESERVER_OBJECT_SET_OWNER': 2060, - 'STATESERVER_OBJECT_CHANGING_OWNER': 2061, - 'STATESERVER_OBJECT_ENTER_OWNER_WITH_REQUIRED': 2062, - 'STATESERVER_OBJECT_ENTER_OWNER_WITH_REQUIRED_OTHER': 2063, - 'STATESERVER_OBJECT_GET_OWNER': 2064, - 'STATESERVER_OBJECT_GET_OWNER_RESP': 2065, - 'STATESERVER_OBJECT_GET_ZONE_OBJECTS': 2100, - 'STATESERVER_OBJECT_GET_ZONES_OBJECTS': 2102, - 'STATESERVER_OBJECT_GET_CHILDREN': 2104, - 'STATESERVER_OBJECT_GET_ZONE_COUNT': 2110, - 'STATESERVER_OBJECT_GET_ZONE_COUNT_RESP': 2111, - 'STATESERVER_OBJECT_GET_ZONES_COUNT': 2112, - 'STATESERVER_OBJECT_GET_ZONES_COUNT_RESP': 2113, - 'STATESERVER_OBJECT_GET_CHILD_COUNT': 2114, - 'STATESERVER_OBJECT_GET_CHILD_COUNT_RESP': 2115, - 'STATESERVER_OBJECT_DELETE_ZONE': 2120, - 'STATESERVER_OBJECT_DELETE_ZONES': 2122, - 'STATESERVER_OBJECT_DELETE_CHILDREN': 2124, - # DBSS-backed-object messages: - 'DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS': 2200, - 'DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS_OTHER': 2201, - 'DBSS_OBJECT_GET_ACTIVATED': 2207, - 'DBSS_OBJECT_GET_ACTIVATED_RESP': 2208, - 'DBSS_OBJECT_DELETE_FIELD_DISK': 2230, - 'DBSS_OBJECT_DELETE_FIELDS_DISK': 2231, - 'DBSS_OBJECT_DELETE_DISK': 2232, - - # Database Server control messages: - 'DBSERVER_CREATE_OBJECT': 3000, - 'DBSERVER_CREATE_OBJECT_RESP': 3001, - 'DBSERVER_OBJECT_GET_FIELD': 3010, - 'DBSERVER_OBJECT_GET_FIELD_RESP': 3011, - 'DBSERVER_OBJECT_GET_FIELDS': 3012, - 'DBSERVER_OBJECT_GET_FIELDS_RESP': 3013, - 'DBSERVER_OBJECT_GET_ALL': 3014, - 'DBSERVER_OBJECT_GET_ALL_RESP': 3015, - 'DBSERVER_OBJECT_SET_FIELD': 3020, - 'DBSERVER_OBJECT_SET_FIELDS': 3021, - 'DBSERVER_OBJECT_SET_FIELD_IF_EQUALS': 3022, - 'DBSERVER_OBJECT_SET_FIELD_IF_EQUALS_RESP': 3023, - 'DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS': 3024, - 'DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS_RESP': 3025, - 'DBSERVER_OBJECT_SET_FIELD_IF_EMPTY': 3026, - 'DBSERVER_OBJECT_SET_FIELD_IF_EMPTY_RESP': 3027, - 'DBSERVER_OBJECT_DELETE_FIELD': 3030, - 'DBSERVER_OBJECT_DELETE_FIELDS': 3031, - 'DBSERVER_OBJECT_DELETE': 3032, - - # Client Agent control messages: - 'CLIENTAGENT_SET_STATE': 1000, - 'CLIENTAGENT_SET_CLIENT_ID': 1001, - 'CLIENTAGENT_SEND_DATAGRAM': 1002, - 'CLIENTAGENT_EJECT': 1004, - 'CLIENTAGENT_DROP': 1005, - 'CLIENTAGENT_GET_NETWORK_ADDRESS': 1006, - 'CLIENTAGENT_GET_NETWORK_ADDRESS_RESP': 1007, - 'CLIENTAGENT_DECLARE_OBJECT': 1010, - 'CLIENTAGENT_UNDECLARE_OBJECT': 1011, - 'CLIENTAGENT_ADD_SESSION_OBJECT': 1012, - 'CLIENTAGENT_REMOVE_SESSION_OBJECT': 1013, - 'CLIENTAGENT_SET_FIELDS_SENDABLE': 1014, - 'CLIENTAGENT_OPEN_CHANNEL': 1100, - 'CLIENTAGENT_CLOSE_CHANNEL': 1101, - 'CLIENTAGENT_ADD_POST_REMOVE': 1110, - 'CLIENTAGENT_CLEAR_POST_REMOVES': 1111, - 'CLIENTAGENT_ADD_INTEREST': 1200, - 'CLIENTAGENT_ADD_INTEREST_MULTIPLE': 1201, - 'CLIENTAGENT_REMOVE_INTEREST': 1203, - 'CLIENTAGENT_DONE_INTEREST_RESP': 1204, - } + # Client messages + "CLIENT_HELLO": 1, + "CLIENT_HELLO_RESP": 2, + "CLIENT_DISCONNECT": 3, + "CLIENT_EJECT": 4, + "CLIENT_HEARTBEAT": 5, + "CLIENT_OBJECT_SET_FIELD": 120, + "CLIENT_OBJECT_SET_FIELDS": 121, + "CLIENT_OBJECT_LEAVING": 132, + "CLIENT_OBJECT_LEAVING_OWNER": 161, + "CLIENT_ENTER_OBJECT_REQUIRED": 142, + "CLIENT_ENTER_OBJECT_REQUIRED_OTHER": 143, + "CLIENT_ENTER_OBJECT_REQUIRED_OWNER": 172, + "CLIENT_ENTER_OBJECT_REQUIRED_OTHER_OWNER": 173, + "CLIENT_DONE_INTEREST_RESP": 204, + "CLIENT_ADD_INTEREST": 200, + "CLIENT_ADD_INTEREST_MULTIPLE": 201, + "CLIENT_REMOVE_INTEREST": 203, + "CLIENT_OBJECT_LOCATION": 140, + + # Message Director control messages + "CONTROL_ADD_CHANNEL": 9000, + "CONTROL_REMOVE_CHANNEL": 9001, + "CONTROL_ADD_RANGE": 9002, + "CONTROL_REMOVE_RANGE": 9003, + "CONTROL_ADD_POST_REMOVE": 9010, + "CONTROL_CLEAR_POST_REMOVES": 9011, + + # State Server control messages + "STATESERVER_CREATE_OBJECT_WITH_REQUIRED": 2000, + "STATESERVER_CREATE_OBJECT_WITH_REQUIRED_OTHER": 2001, + "STATESERVER_DELETE_AI_OBJECTS": 2009, + "STATESERVER_OBJECT_GET_FIELD": 2010, + "STATESERVER_OBJECT_GET_FIELD_RESP": 2011, + "STATESERVER_OBJECT_GET_FIELDS": 2012, + "STATESERVER_OBJECT_GET_FIELDS_RESP": 2013, + "STATESERVER_OBJECT_GET_ALL": 2014, + "STATESERVER_OBJECT_GET_ALL_RESP": 2015, + "STATESERVER_OBJECT_SET_FIELD": 2020, + "STATESERVER_OBJECT_SET_FIELDS": 2021, + "STATESERVER_OBJECT_DELETE_FIELD_RAM": 2030, + "STATESERVER_OBJECT_DELETE_FIELDS_RAM": 2031, + "STATESERVER_OBJECT_DELETE_RAM": 2032, + "STATESERVER_OBJECT_SET_LOCATION": 2040, + "STATESERVER_OBJECT_CHANGING_LOCATION": 2041, + "STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED": 2042, + "STATESERVER_OBJECT_ENTER_LOCATION_WITH_REQUIRED_OTHER": 2043, + "STATESERVER_OBJECT_GET_LOCATION": 2044, + "STATESERVER_OBJECT_GET_LOCATION_RESP": 2045, + "STATESERVER_OBJECT_SET_AI": 2050, + "STATESERVER_OBJECT_CHANGING_AI": 2051, + "STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED": 2052, + "STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER": 2053, + "STATESERVER_OBJECT_GET_AI": 2054, + "STATESERVER_OBJECT_GET_AI_RESP": 2055, + "STATESERVER_OBJECT_SET_OWNER": 2060, + "STATESERVER_OBJECT_CHANGING_OWNER": 2061, + "STATESERVER_OBJECT_ENTER_OWNER_WITH_REQUIRED": 2062, + "STATESERVER_OBJECT_ENTER_OWNER_WITH_REQUIRED_OTHER": 2063, + "STATESERVER_OBJECT_GET_OWNER": 2064, + "STATESERVER_OBJECT_GET_OWNER_RESP": 2065, + "STATESERVER_OBJECT_GET_ZONE_OBJECTS": 2100, + "STATESERVER_OBJECT_GET_ZONES_OBJECTS": 2102, + "STATESERVER_OBJECT_GET_CHILDREN": 2104, + "STATESERVER_OBJECT_GET_ZONE_COUNT": 2110, + "STATESERVER_OBJECT_GET_ZONE_COUNT_RESP": 2111, + "STATESERVER_OBJECT_GET_ZONES_COUNT": 2112, + "STATESERVER_OBJECT_GET_ZONES_COUNT_RESP": 2113, + "STATESERVER_OBJECT_GET_CHILD_COUNT": 2114, + "STATESERVER_OBJECT_GET_CHILD_COUNT_RESP": 2115, + "STATESERVER_OBJECT_DELETE_ZONE": 2120, + "STATESERVER_OBJECT_DELETE_ZONES": 2122, + "STATESERVER_OBJECT_DELETE_CHILDREN": 2124, + + # DBSS-backed-object messages + "DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS": 2200, + "DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS_OTHER": 2201, + "DBSS_OBJECT_GET_ACTIVATED": 2207, + "DBSS_OBJECT_GET_ACTIVATED_RESP": 2208, + "DBSS_OBJECT_DELETE_FIELD_DISK": 2230, + "DBSS_OBJECT_DELETE_FIELDS_DISK": 2231, + "DBSS_OBJECT_DELETE_DISK": 2232, + + # Database Server control messages + "DBSERVER_CREATE_OBJECT": 3000, + "DBSERVER_CREATE_OBJECT_RESP": 3001, + "DBSERVER_OBJECT_GET_FIELD": 3010, + "DBSERVER_OBJECT_GET_FIELD_RESP": 3011, + "DBSERVER_OBJECT_GET_FIELDS": 3012, + "DBSERVER_OBJECT_GET_FIELDS_RESP": 3013, + "DBSERVER_OBJECT_GET_ALL": 3014, + "DBSERVER_OBJECT_GET_ALL_RESP": 3015, + "DBSERVER_OBJECT_SET_FIELD": 3020, + "DBSERVER_OBJECT_SET_FIELDS": 3021, + "DBSERVER_OBJECT_SET_FIELD_IF_EQUALS": 3022, + "DBSERVER_OBJECT_SET_FIELD_IF_EQUALS_RESP": 3023, + "DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS": 3024, + "DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS_RESP": 3025, + "DBSERVER_OBJECT_SET_FIELD_IF_EMPTY": 3026, + "DBSERVER_OBJECT_SET_FIELD_IF_EMPTY_RESP": 3027, + "DBSERVER_OBJECT_DELETE_FIELD": 3030, + "DBSERVER_OBJECT_DELETE_FIELDS": 3031, + "DBSERVER_OBJECT_DELETE": 3032, + + # Client Agent control messages + "CLIENTAGENT_SET_STATE": 1000, + "CLIENTAGENT_SET_CLIENT_ID": 1001, + "CLIENTAGENT_SEND_DATAGRAM": 1002, + "CLIENTAGENT_EJECT": 1004, + "CLIENTAGENT_DROP": 1005, + "CLIENTAGENT_GET_NETWORK_ADDRESS": 1006, + "CLIENTAGENT_GET_NETWORK_ADDRESS_RESP": 1007, + "CLIENTAGENT_DECLARE_OBJECT": 1010, + "CLIENTAGENT_UNDECLARE_OBJECT": 1011, + "CLIENTAGENT_ADD_SESSION_OBJECT": 1012, + "CLIENTAGENT_REMOVE_SESSION_OBJECT": 1013, + "CLIENTAGENT_SET_FIELDS_SENDABLE": 1014, + "CLIENTAGENT_OPEN_CHANNEL": 1100, + "CLIENTAGENT_CLOSE_CHANNEL": 1101, + "CLIENTAGENT_ADD_POST_REMOVE": 1110, + "CLIENTAGENT_CLEAR_POST_REMOVES": 1111, + "CLIENTAGENT_ADD_INTEREST": 1200, + "CLIENTAGENT_ADD_INTEREST_MULTIPLE": 1201, + "CLIENTAGENT_REMOVE_INTEREST": 1203, + "CLIENTAGENT_DONE_INTEREST_RESP": 1204 +} # create id->name table for debugging MsgId2Names = invertDictLossless(MsgName2Id) -# put msg names in module scope, assigned to msg value -globals().update(MsgName2Id) +def is_client_message(msgType: int) -> bool: + """ + Returns whether or not the given message type is a Client message + """ + + return msgType >= CLIENT_HELLO and \ + msgType <= CLIENT_OBJECT_LOCATION + +def is_state_server_message(msgType: int) -> bool: + """ + Returns whether or not the given message type is a State Server message + """ + + return msgType >= STATESERVER_CREATE_OBJECT_WITH_REQUIRED and \ + msgType <= STATESERVER_OBJECT_DELETE_CHILDREN + +def is_database_state_server_message(msgType: int) -> bool: + """ + Returns whether or not the given message type is a Database State Server message + """ + + return msgType >= DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS and \ + msgType <= DBSS_OBJECT_DELETE_DISK + +def is_database_server_message(msgType: int) -> bool: + """ + Returns whether or not the given message type is a Database Server message + """ + + return msgType >= DBSERVER_CREATE_OBJECT and \ + msgType <= DBSERVER_OBJECT_DELETE + +def is_client_agent_message(msgType: int) -> bool: + """ + Returns whether or not the given message type is a Client Agent message + """ -# These messages are ignored when the client is headed to the quiet zone -QUIET_ZONE_IGNORED_LIST = [ + return msgType >= CLIENTAGENT_SET_STATE and \ + msgType <= CLIENTAGENT_DONE_INTEREST_RESP - # We mustn't ignore updates, because some updates for localToon - # are always important. - #CLIENT_OBJECT_UPDATE_FIELD, +def is_message_director_message(msgType: int) -> bool: + """ + Returns whether or not the given message type is a Message Director message + """ - # These are now handled. If it is a create for a class that is in the - # uber zone, we should create it. - #CLIENT_CREATE_OBJECT_REQUIRED, - #CLIENT_CREATE_OBJECT_REQUIRED_OTHER, + return msgType >= CONTROL_CHANNEL and \ + msgType <= CONTROL_CLEAR_POST_REMOVES - ] +def is_net_messenger_message(msgType: int) -> bool: + """ + Returns whether or not the given message type is a Net Messenger message + """ -# The following is a different set of numbers from above. -# These are the sub-message types for CLIENT_LOGIN_2. -CLIENT_LOGIN_2_GREEN = 1 # Disney's GoReg subscription token, not used. -CLIENT_LOGIN_2_PLAY_TOKEN = 2 # VR Studio PlayToken. -CLIENT_LOGIN_2_BLUE = 3 # The international GoReg token. -CLIENT_LOGIN_3_DISL_TOKEN = 4 # SSL encoded blob from DISL system. \ No newline at end of file + return msgType >= 20000 \ No newline at end of file diff --git a/panda3d_astron/repository.py b/panda3d_astron/repository.py deleted file mode 100644 index a1c82b0..0000000 --- a/panda3d_astron/repository.py +++ /dev/null @@ -1,1237 +0,0 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.ClientRepositoryBase import ClientRepositoryBase -from direct.distributed.PyDatagram import PyDatagram -from direct.distributed.MsgTypes import * -from direct.showbase import ShowBase # __builtin__.config -from direct.task.TaskManagerGlobal import * # taskMgr -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.ConnectionRepository import ConnectionRepository -from direct.distributed.PyDatagram import PyDatagram -from direct.distributed.PyDatagramIterator import PyDatagramIterator -from direct.distributed import DoInterestManager as interest_mgr - -from panda3d_astron import msgtypes -from panda3d_astron.database import AstronDatabaseInterface -from panda3d_astron.messenger import NetMessenger -from panda3d_astron.msgtypes import * - -import collections -from panda3d.direct import STUint16, STUint32, DCPacker -from panda3d.core import * - -# --------------------------------------------------------------------------------------------------------------------------------------------------------------------- # -# Panda3D ClientRepositoryBase implementation for implementing Astron clients - -class AstronClientRepository(ClientRepositoryBase): - """ - The Astron implementation of a clients repository for - communication with an Astron ClientAgent. - - This repo will emit events for: - * CLIENT_HELLO_RESP - * CLIENT_EJECT ( error_code, reason ) - * CLIENT_OBJECT_LEAVING ( do_id ) - * CLIENT_ADD_INTEREST ( context, interest_id, parent_id, zone_id ) - * CLIENT_ADD_INTEREST_MULTIPLE ( icontext, interest_id, parent_id, [zone_ids] ) - * CLIENT_REMOVE_INTEREST ( context, interest_id ) - * CLIENT_DONE_INTEREST_RESP ( context, interest_id ) - * LOST_CONNECTION () - """ - - notify = DirectNotifyGlobal.directNotify.newCategory("AstronClientRepository") - - # This is required by DoCollectionManager, even though it's not - # used by this implementation. - GameGlobalsId = 0 - - def __init__(self, *args, **kwargs): - ClientRepositoryBase.__init__(self, *args, **kwargs) - base.finalExitCallbacks.append(self.shutdown) - self.message_handlers = { - CLIENT_HELLO_RESP: self.handle_hello_resp, - CLIENT_EJECT: self.handle_eject, - CLIENT_ENTER_OBJECT_REQUIRED: self.handle_enter_object_required, - CLIENT_ENTER_OBJECT_REQUIRED_OTHER: self.handle_enter_object_required_other, - CLIENT_ENTER_OBJECT_REQUIRED_OWNER: self.handle_enter_object_Required_owner, - CLIENT_ENTER_OBJECT_REQUIRED_OTHER_OWNER: self.handle_enter_object_required_other_owner, - CLIENT_OBJECT_SET_FIELD: self.handle_update_field, - CLIENT_OBJECT_SET_FIELDS: self.handle_update_fields, - CLIENT_OBJECT_LEAVING: self.handle_object_leaving, - CLIENT_OBJECT_LOCATION: self.handleObjectLocation, - CLIENT_ADD_INTEREST: self.handle_add_interest, - CLIENT_ADD_INTEREST_MULTIPLE: self.handle_add_interest_multiple, - CLIENT_REMOVE_INTEREST: self.handle_remove_interest, - CLIENT_DONE_INTEREST_RESP: self.handle_interest_done_message, - } - - def handleDatagram(self, di: PyDatagramIterator) -> None: - """ - Handles incoming datagrams from an Astron ClientAgent instance. - """ - - msg_type = self.getMsgType() - message_handler = self.message_handlers.get(msg_type, None) - if message_handler is None: - self.notify.error('Received unknown message type: %d!' % msg_type) - return - - message_handler(di) - self.consider_heartbeat() - - handle_datagram = handleDatagram - - def consider_heartbeat(self) -> None: - """ - Sends a heartbeat message to the Astron client agent if one has not been sent recently. - """ - - super().considerHeartbeat() - - def handle_hello_resp(self, di: PyDatagramIterator) -> None: - """ - Handles the CLIENT_HELLO_RESP packet sent by the Client Agent to the client when the client's CLIENT_HELLO is accepted. - """ - - messenger.send("CLIENT_HELLO_RESP", []) - - def handle_eject(self, di: PyDatagramIterator) -> None: - """ - Handles the CLIENT_DISCONNECT sent by the client to the Client Agent to notify that it is going to close the connection. - """ - - error_code = di.get_uint16() - reason = di.get_string() - messenger.send("CLIENT_EJECT", [error_code, reason]) - - def handle_update_field(self, *args, **kwargs) -> None: - """ - """ - - super().handleUpdateField(*args, **kwargs) - - def handle_enter_object_required(self, di: PyDatagramIterator) -> None: - """ - """ - - do_id = di.get_uint32() - parent_id = di.get_uint32() - zone_id = di.get_uint32() - dclass_id = di.get_uint16() - dclass = self.dclassesByNumber[dclass_id] - self.generateWithRequiredFields(dclass, do_id, di, parent_id, zone_id) - - def handle_enter_object_required_other(self, di: PyDatagramIterator) -> None: - """ - """ - - do_id = di.get_uint32() - parent_id = di.get_uint32() - zone_id = di.get_uint32() - dclass_id = di.get_uint16() - dclass = self.dclassesByNumber[dclass_id] - self.generateWithRequiredOtherFields(dclass, do_id, di, parent_id, zone_id) - - def handle_enter_object_Required_owner(self, di: PyDatagramIterator) -> None: - """ - """ - - avatar_doId = di.get_uint32() - parent_id = di.get_uint32() - zone_id = di.get_uint32() - dclass_id = di.get_uint16() - dclass = self.dclassesByNumber[dclass_id] - self.generateWithRequiredFieldsOwner(dclass, avatar_doId, di) - - def handle_enter_object_required_other_owner(self, di: PyDatagramIterator) -> None: - """ - """ - - avatar_doId = di.get_uint32() - parent_id = di.get_uint32() - zone_id = di.get_uint32() - dclass_id = di.get_uint16() - dclass = self.dclassesByNumber[dclass_id] - self.generateWithRequiredOtherFieldsOwner(dclass, avatar_doId, di) - - def generateWithRequiredFieldsOwner(self, dclass: object, doId: int, di: PyDatagramIterator) -> None: - """ - """ - - if doId in self.doId2ownerView: - # ...it is in our dictionary. - # Just update it. - self.notify.error('duplicate owner generate for %s (%s)' % ( - doId, dclass.getName())) - distObj = self.doId2ownerView[doId] - assert distObj.dclass == dclass - distObj.generate() - distObj.updateRequiredFields(dclass, di) - # updateRequiredFields calls announceGenerate - elif self.cacheOwner.contains(doId): - # ...it is in the cache. - # Pull it out of the cache: - distObj = self.cacheOwner.retrieve(doId) - assert distObj.dclass == dclass - # put it in the dictionary: - self.doId2ownerView[doId] = distObj - # and update it. - distObj.generate() - distObj.updateRequiredFields(dclass, di) - # updateRequiredFields calls announceGenerate - else: - # ...it is not in the dictionary or the cache. - # Construct a new one - classDef = dclass.getOwnerClassDef() - if classDef == None: - self.notify.error("Could not create an undefined %s object. Have you created an owner view?" % (dclass.getName())) - distObj = classDef(self) - distObj.dclass = dclass - # Assign it an Id - distObj.doId = doId - # Put the new do in the dictionary - self.doId2ownerView[doId] = distObj - # Update the required fields - distObj.generateInit() # Only called when constructed - distObj.generate() - distObj.updateRequiredFields(dclass, di) - # updateRequiredFields calls announceGenerate - return distObj - - generate_with_required_Fields_owner = generateWithRequiredFieldsOwner - - def handle_update_fields(self, di): - """ - """ - - # Can't test this without the server actually sending it. - self.notify.error("CLIENT_OBJECT_SET_FIELDS not implemented!") - # # Here's some tentative code and notes: - # do_id = di.getUint32() - # field_count = di.getUint16() - # for i in range(0, field_count): - # field_id = di.getUint16() - # field = self.get_dc_file().get_field_by_index(field_id) - # # print(type(field)) - # # print(field) - # # FIXME: Get field type, unpack value, create and send message. - # # value = di.get?() - # # Assemble new message - - def handle_object_leaving(self, di): - """ - """ - - do_id = di.get_uint32() - dist_obj = self.doId2do.get(do_id) - dist_obj.delete() - self.deleteObject(do_id) - messenger.send("CLIENT_OBJECT_LEAVING", [do_id]) - - def handle_add_interest(self, di): - """ - """ - - context = di.get_uint32() - interest_id = di.get_uint16() - parent_id = di.get_uint32() - zone_id = di.get_uint32() - - messenger.send("CLIENT_ADD_INTEREST", [context, interest_id, parent_id, zone_id]) - self.add_internal_interest_handle(context, interest_id, parent_id, [zone_id]) - - def handle_add_interest_multiple(self, di): - """ - """ - - context = di.get_uint32() - interest_id = di.get_uint16() - parent_id = di.get_uint32() - zone_ids = [di.get_uint32() for i in range(0, di.get_uint16())] - - messenger.send("CLIENT_ADD_INTEREST_MULTIPLE", [context, interest_id, parent_id, zone_ids]) - self.add_internal_interest_handle(context, interest_id, parent_id, zone_ids) - - def add_internal_interest_handle(self, context, interest_id, parent_id, zone_ids) -> None: - """ - """ - - # make sure we've got parenting rules set in the DC - if parent_id not in (self.getGameDoId(),): - parent = self.getDo(parent_id) - if not parent: - self.notify.error('Attempting to add interest under unknown object %s' % parent_id) - else: - if not parent.hasParentingRules(): - self.notify.error('No setParentingRules defined in the DC for object %s (%s)' % (parent_id, parent.__class__.__name__)) - - interest_mgr.DoInterestManager._interests[interest_id] = interest_mgr.InterestState( - None, interest_mgr.InterestState.StateActive, context, None, parent_id, zone_ids, self._completeEventCount, True) - - def handle_interest_done_message(self, *args, **kwargs) -> None: - """ - """ - - super().handleInterestDoneMessage(*args, **kwargs) - - def handle_remove_interest(self, di): - """ - """ - - context = di.get_uint32() - interest_id = di.get_uint16() - messenger.send("CLIENT_REMOVE_INTEREST", [context, interest_id]) - - def deleteObject(self, doId): - """ - implementation copied from ClientRepository.py - Removes the object from the client's view of the world. This - should normally not be called directly except in the case of - error recovery, since the server will normally be responsible - for deleting and disabling objects as they go out of scope. - After this is called, future updates by server on this object - will be ignored (with a warning message). The object will - become valid again the next time the server sends a generate - message for this doId. - This is not a distributed message and does not delete the - object on the server or on any other client. - """ - - if doId in self.doId2do: - # If it is in the dictionary, remove it. - obj = self.doId2do[doId] - # Remove it from the dictionary - del self.doId2do[doId] - # Disable, announce, and delete the object itself... - # unless delayDelete is on... - obj.deleteOrDelay() - if self.isLocalId(doId): - self.freeDoId(doId) - elif self.cache.contains(doId): - # If it is in the cache, remove it. - self.cache.delete(doId) - if self.isLocalId(doId): - self.freeDoId(doId) - else: - # Otherwise, ignore it - self.notify.warning( - "Asked to delete non-existent DistObj " + str(doId)) - - delete_object = deleteObject - - def sendUpdate(self, distObj, fieldName, args): - """ - Sends a normal update for a single field. - """ - - dg = distObj.dclass.clientFormatUpdate( - fieldName, distObj.doId, args) - self.send(dg) - - send_update = sendUpdate - - def sendHello(self, version_string: str = None): - """ - Sends our CLIENT_HELLO protocol handshake packet to the game server. Uses the supplied argument version if present - otherwise default to the server-version PRC variable - """ - - if version_string == None: - version_string = ConfigVariableString('server-version', '') - - if version_string == None or version_string == '': - self.notify.error('No server version defined at the time of hello. Please set a "server-version" string in your panda runtime conrfiguration file') - return - - dg = PyDatagram() - dg.add_uint16(CLIENT_HELLO) - dg.add_uint32(self.get_dc_file().get_hash()) - dg.add_string(version_string) - self.send(dg) - - send_hello = sendHello - - def sendHeartbeat(self): - """ - """ - - datagram = PyDatagram() - datagram.addUint16(CLIENT_HEARTBEAT) - self.send(datagram) - - send_heartbeat = sendHeartbeat - - def sendAddInterest(self, context, interest_id, parent_id, zone_id): - """ - """ - - dg = PyDatagram() - dg.add_uint16(CLIENT_ADD_INTEREST) - dg.add_uint32(context) - dg.add_uint16(interest_id) - dg.add_uint32(parent_id) - dg.add_uint32(zone_id) - self.send(dg) - - send_add_interest = sendAddInterest - - def sendAddInterestMultiple(self, context, interest_id, parent_id, zone_ids): - """ - """ - - dg = PyDatagram() - dg.add_uint16(CLIENT_ADD_INTEREST_MULTIPLE) - dg.add_uint32(context) - dg.add_uint16(interest_id) - dg.add_uint32(parent_id) - dg.add_uint16(len(zone_ids)) - for zone_id in zone_ids: - dg.add_uint32(zone_id) - self.send(dg) - - send_add_interest_multiple = sendAddInterestMultiple - - def sendRemoveInterest(self, context, interest_id): - """ - """ - - dg = PyDatagram() - dg.add_uint16(CLIENT_REMOVE_INTEREST) - dg.add_uint32(context) - dg.add_uint16(interest_id) - self.send(dg) - - send_remove_interest = sendRemoveInterest - - def lostConnection(self): - """ - """ - - messenger.send("LOST_CONNECTION") - - def disconnect(self): - """ - This implicitly deletes all objects from the repository. - """ - - for do_id in self.doId2do.keys(): - self.deleteObject(do_id) - ClientRepositoryBase.disconnect(self) - -# --------------------------------------------------------------------------------------------------------------------------------------------------------------------- # -# Panda3D ConnectionRepository implementation for implementing Astron server instances - -class AstronInternalRepository(ConnectionRepository): - """ - This class is part of Panda3D's new MMO networking framework. - It interfaces with an Astron (https://github.com/Astron/Astron) server in - order to manipulate objects in the Astron cluster. It does not require any - specific "gateway" into the Astron network. Rather, it just connects directly - to any Message Director. Hence, it is an "internal" repository. - - This class is suitable for constructing your own AI Servers and UberDOG servers - using Panda3D. Objects with a "self.air" attribute are referring to an instance - of this class. - """ - - notify = DirectNotifyGlobal.directNotify.newCategory("AstronInternalRepository") - - def __init__(self, baseChannel, serverId=None, dcFileNames = None, - dcSuffix = 'AI', connectMethod = None, threadedNet = None): - if connectMethod is None: - connectMethod = self.CM_NATIVE - ConnectionRepository.__init__(self, connectMethod, config, hasOwnerView = False, threadedNet = threadedNet) - self.setClientDatagram(False) - self.dcSuffix = dcSuffix - if hasattr(self, 'setVerbose'): - if self.config.GetBool('verbose-internalrepository'): - self.setVerbose(1) - - # The State Server we are configured to use for creating objects. - #If this is None, generating objects is not possible. - self.serverId = self.config.GetInt('air-stateserver', 0) or None - if serverId is not None: - self.serverId = serverId - - maxChannels = self.config.GetInt('air-channel-allocation', 1000000) - self.channelAllocator = UniqueIdAllocator(baseChannel, baseChannel+maxChannels-1) - self._registeredChannels = set() - - self.__contextCounter = 0 - - self.netMessenger = NetMessenger(self) - - self.dbInterface = AstronDatabaseInterface(self) - self.__callbacks = {} - - self.ourChannel = self.allocateChannel() - - self.eventLogId = self.config.GetString('eventlog-id', 'AIR:%d' % self.ourChannel) - self.eventSocket = None - eventLogHost = self.config.GetString('eventlog-host', '') - if eventLogHost: - if ':' in eventLogHost: - host, port = eventLogHost.split(':', 1) - self.setEventLogHost(host, int(port)) - else: - self.setEventLogHost(eventLogHost) - - self.readDCFile(dcFileNames) - - def getContext(self): - self.__contextCounter = (self.__contextCounter + 1) & 0xFFFFFFFF - return self.__contextCounter - - def allocateChannel(self): - """ - Allocate an unused channel out of this AIR's configured channel space. - This is also used to allocate IDs for DistributedObjects, since those - occupy a channel. - """ - - return self.channelAllocator.allocate() - - def deallocateChannel(self, channel): - """ - Return the previously-allocated channel back to the allocation pool. - """ - - self.channelAllocator.free(channel) - - def registerForChannel(self, channel): - """ - Register for messages on a specific Message Director channel. - If the channel is already open by this AIR, nothing will happen. - """ - - if channel in self._registeredChannels: - return - self._registeredChannels.add(channel) - - dg = PyDatagram() - dg.addServerControlHeader(CONTROL_ADD_CHANNEL) - dg.addChannel(channel) - self.send(dg) - - def unregisterForChannel(self, channel): - """ - Unregister a channel subscription on the Message Director. The Message - Director will cease to relay messages to this AIR sent on the channel. - """ - - if channel not in self._registeredChannels: - return - self._registeredChannels.remove(channel) - - dg = PyDatagram() - dg.addServerControlHeader(CONTROL_REMOVE_CHANNEL) - dg.addChannel(channel) - self.send(dg) - - def addPostRemove(self, dg): - """ - Register a datagram with the Message Director that gets sent out if the - connection is ever lost. - This is useful for registering cleanup messages: If the Panda3D process - ever crashes unexpectedly, the Message Director will detect the socket - close and automatically process any post-remove datagrams. - """ - - dg2 = PyDatagram() - dg2.addServerControlHeader(CONTROL_ADD_POST_REMOVE) - dg2.addUint64(self.ourChannel) - dg2.addBlob(dg.getMessage()) - self.send(dg2) - - def clearPostRemove(self): - """ - Clear all datagrams registered with addPostRemove. - This is useful if the Panda3D process is performing a clean exit. It may - clear the "emergency clean-up" post-remove messages and perform a normal - exit-time clean-up instead, depending on the specific design of the game. - """ - - dg = PyDatagram() - dg.addServerControlHeader(CONTROL_CLEAR_POST_REMOVES) - dg.addUint64(self.ourChannel) - self.send(dg) - - def handleDatagram(self, di): - msgType = self.getMsgType() - - if msgType in (STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED, - STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER): - self.handleObjEntry(di, msgType == STATESERVER_OBJECT_ENTER_AI_WITH_REQUIRED_OTHER) - elif msgType in (STATESERVER_OBJECT_CHANGING_AI, - STATESERVER_OBJECT_DELETE_RAM): - self.handleObjExit(di) - elif msgType == STATESERVER_OBJECT_CHANGING_LOCATION: - self.handleObjLocation(di) - elif msgType in (DBSERVER_CREATE_OBJECT_RESP, - DBSERVER_OBJECT_GET_ALL_RESP, - DBSERVER_OBJECT_GET_FIELDS_RESP, - DBSERVER_OBJECT_GET_FIELD_RESP, - DBSERVER_OBJECT_SET_FIELD_IF_EQUALS_RESP, - DBSERVER_OBJECT_SET_FIELDS_IF_EQUALS_RESP): - self.dbInterface.handleDatagram(msgType, di) - elif msgType == DBSS_OBJECT_GET_ACTIVATED_RESP: - self.handleGetActivatedResp(di) - elif msgType == STATESERVER_OBJECT_GET_LOCATION_RESP: - self.handleGetLocationResp(di) - elif msgType == STATESERVER_OBJECT_GET_ALL_RESP: - self.handleGetObjectResp(di) - elif msgType == CLIENTAGENT_GET_NETWORK_ADDRESS_RESP: - self.handleGetNetworkAddressResp(di) - #elif msgType == CLIENTAGENT_DONE_INTEREST_RESP: - # self.handleClientAgentInterestDoneResp(di) - elif msgType >= 20000: - # These messages belong to the NetMessenger: - self.netMessenger.handle(msgType, di) - else: - self.notify.warning('Received message with unknown MsgType=%d' % msgType) - - def handleObjLocation(self, di): - doId = di.getUint32() - parentId = di.getUint32() - zoneId = di.getUint32() - - do = self.doId2do.get(doId) - - if not do: - self.notify.warning('Received location for unknown doId=%d!' % (doId)) - return - - do.setLocation(parentId, zoneId) - - def handleObjEntry(self, di, other): - doId = di.getUint32() - parentId = di.getUint32() - zoneId = di.getUint32() - classId = di.getUint16() - - if classId not in self.dclassesByNumber: - self.notify.warning('Received entry for unknown dclass=%d! (Object %d)' % (classId, doId)) - return - - if doId in self.doId2do: - return # We already know about this object; ignore the entry. - - dclass = self.dclassesByNumber[classId] - - do = dclass.getClassDef()(self) - do.dclass = dclass - do.doId = doId - # The DO came in off the server, so we do not unregister the channel when - # it dies: - do.doNotDeallocateChannel = True - self.addDOToTables(do, location=(parentId, zoneId)) - - # Now for generation: - do.generate() - if other: - do.updateAllRequiredOtherFields(dclass, di) - else: - do.updateAllRequiredFields(dclass, di) - - def handleObjExit(self, di): - doId = di.getUint32() - - if doId not in self.doId2do: - self.notify.warning('Received AI exit for unknown object %d' % (doId)) - return - - do = self.doId2do[doId] - self.removeDOFromTables(do) - do.delete() - do.sendDeleteEvent() - - def handleGetActivatedResp(self, di): - ctx = di.getUint32() - doId = di.getUint32() - activated = di.getUint8() - - if ctx not in self.__callbacks: - self.notify.warning('Received unexpected DBSS_OBJECT_GET_ACTIVATED_RESP (ctx: %d)' %ctx) - return - - try: - self.__callbacks[ctx](doId, activated) - finally: - del self.__callbacks[ctx] - - - def getActivated(self, doId, callback): - ctx = self.getContext() - self.__callbacks[ctx] = callback - - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, DBSS_OBJECT_GET_ACTIVATED) - dg.addUint32(ctx) - dg.addUint32(doId) - self.send(dg) - - def getLocation(self, doId, callback): - """ - Ask a DistributedObject where it is. - You should already be sure the object actually exists, otherwise the - callback will never be called. - Callback is called as: callback(doId, parentId, zoneId) - """ - - ctx = self.getContext() - self.__callbacks[ctx] = callback - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, STATESERVER_OBJECT_GET_LOCATION) - dg.addUint32(ctx) - self.send(dg) - - def handleGetLocationResp(self, di): - ctx = di.getUint32() - doId = di.getUint32() - parentId = di.getUint32() - zoneId = di.getUint32() - - if ctx not in self.__callbacks: - self.notify.warning('Received unexpected STATESERVER_OBJECT_GET_LOCATION_RESP (ctx: %d)' % ctx) - return - - try: - self.__callbacks[ctx](doId, parentId, zoneId) - finally: - del self.__callbacks[ctx] - - def getObject(self, doId, callback): - """ - Get the entire state of an object. - You should already be sure the object actually exists, otherwise the - callback will never be called. - Callback is called as: callback(doId, parentId, zoneId, dclass, fields) - """ - - ctx = self.getContext() - self.__callbacks[ctx] = callback - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, STATESERVER_OBJECT_GET_ALL) - dg.addUint32(ctx) - dg.addUint32(doId) - self.send(dg) - - def handleGetObjectResp(self, di): - ctx = di.getUint32() - doId = di.getUint32() - parentId = di.getUint32() - zoneId = di.getUint32() - classId = di.getUint16() - - if ctx not in self.__callbacks: - self.notify.warning('Received unexpected STATESERVER_OBJECT_GET_ALL_RESP (ctx: %d)' % ctx) - return - - if classId not in self.dclassesByNumber: - self.notify.warning('Received STATESERVER_OBJECT_GET_ALL_RESP for unknown dclass=%d! (Object %d)' % (classId, doId)) - return - - dclass = self.dclassesByNumber[classId] - - fields = {} - unpacker = DCPacker() - unpacker.setUnpackData(di.getRemainingBytes()) - - # Required: - for i in range(dclass.getNumInheritedFields()): - field = dclass.getInheritedField(i) - if not field.isRequired() or field.asMolecularField(): continue - unpacker.beginUnpack(field) - fields[field.getName()] = field.unpackArgs(unpacker) - unpacker.endUnpack() - - # Other: - other = unpacker.rawUnpackUint16() - for i in range(other): - field = dclass.getFieldByIndex(unpacker.rawUnpackUint16()) - unpacker.beginUnpack(field) - fields[field.getName()] = field.unpackArgs(unpacker) - unpacker.endUnpack() - - try: - self.__callbacks[ctx](doId, parentId, zoneId, dclass, fields) - finally: - del self.__callbacks[ctx] - - def getNetworkAddress(self, clientId, callback): - """ - Get the endpoints of a client connection. - You should already be sure the client actually exists, otherwise the - callback will never be called. - Callback is called as: callback(remoteIp, remotePort, localIp, localPort) - """ - - ctx = self.getContext() - self.__callbacks[ctx] = callback - dg = PyDatagram() - dg.addServerHeader(clientId, self.ourChannel, CLIENTAGENT_GET_NETWORK_ADDRESS) - dg.addUint32(ctx) - self.send(dg) - - def handleGetNetworkAddressResp(self, di): - ctx = di.getUint32() - remoteIp = di.getString() - remotePort = di.getUint16() - localIp = di.getString() - localPort = di.getUint16() - - if ctx not in self.__callbacks: - self.notify.warning('Received unexpected CLIENTAGENT_GET_NETWORK_ADDRESS_RESP (ctx: %d)' % ctx) - return - - try: - self.__callbacks[ctx](remoteIp, remotePort, localIp, localPort) - finally: - del self.__callbacks[ctx] - - def sendUpdate(self, do, fieldName, args): - """ - Send a field update for the given object. - You should use do.sendUpdate(...) instead. This is not meant to be - called directly unless you really know what you are doing. - """ - - self.sendUpdateToChannel(do, do.doId, fieldName, args) - - def sendUpdateToChannel(self, do, channelId, fieldName, args): - """ - Send an object field update to a specific channel. - This is useful for directing the update to a specific client or node, - rather than at the State Server managing the object. - You should use do.sendUpdateToChannel(...) instead. This is not meant - to be called directly unless you really know what you are doing. - """ - - dclass = do.dclass - field = dclass.getFieldByName(fieldName) - dg = field.aiFormatUpdate(do.doId, channelId, self.ourChannel, args) - self.send(dg) - - def sendActivate(self, doId, parentId, zoneId, dclass=None, fields=None): - """ - Activate a DBSS object, given its doId, into the specified parentId/zoneId. - If both dclass and fields are specified, an ACTIVATE_WITH_DEFAULTS_OTHER - will be sent instead. In other words, the specified fields will be - auto-applied during the activation. - """ - - fieldPacker = DCPacker() - fieldCount = 0 - if dclass and fields: - for k,v in fields.items(): - field = dclass.getFieldByName(k) - if not field: - self.notify.error('Activation request for %s object contains ' - 'invalid field named %s' % (dclass.getName(), k)) - - fieldPacker.rawPackUint16(field.getNumber()) - fieldPacker.beginPack(field) - field.packArgs(fieldPacker, v) - fieldPacker.endPack() - fieldCount += 1 - - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS) - dg.addUint32(doId) - dg.addUint32(0) - dg.addUint32(0) - self.send(dg) - # DEFAULTS_OTHER isn't implemented yet, so we chase it with a SET_FIELDS - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, STATESERVER_OBJECT_SET_FIELDS) - dg.addUint32(doId) - dg.addUint16(fieldCount) - dg.appendData(fieldPacker.getBytes()) - self.send(dg) - # Now slide it into the zone we expect to see it in (so it - # generates onto us with all of the fields in place) - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, STATESERVER_OBJECT_SET_LOCATION) - dg.addUint32(parentId) - dg.addUint32(zoneId) - self.send(dg) - else: - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, DBSS_OBJECT_ACTIVATE_WITH_DEFAULTS) - dg.addUint32(doId) - dg.addUint32(parentId) - dg.addUint32(zoneId) - self.send(dg) - - def sendSetLocation(self, do, parentId, zoneId): - dg = PyDatagram() - dg.addServerHeader(do.doId, self.ourChannel, STATESERVER_OBJECT_SET_LOCATION) - dg.addUint32(parentId) - dg.addUint32(zoneId) - self.send(dg) - - def generateWithRequired(self, do, parentId, zoneId, optionalFields=[]): - """ - Generate an object onto the State Server, choosing an ID from the pool. - You should use do.generateWithRequired(...) instead. This is not meant - to be called directly unless you really know what you are doing. - """ - - doId = self.allocateChannel() - self.generateWithRequiredAndId(do, doId, parentId, zoneId, optionalFields) - - def generateWithRequiredAndId(self, do, doId, parentId, zoneId, optionalFields=[]): - """ - Generate an object onto the State Server, specifying its ID and location. - You should use do.generateWithRequiredAndId(...) instead. This is not - meant to be called directly unless you really know what you are doing. - """ - - do.doId = doId - self.addDOToTables(do, location=(parentId, zoneId)) - do.sendGenerateWithRequired(self, parentId, zoneId, optionalFields) - - def requestDelete(self, do): - """ - Request the deletion of an object that already exists on the State Server. - You should use do.requestDelete() instead. This is not meant to be - called directly unless you really know what you are doing. - """ - - dg = PyDatagram() - dg.addServerHeader(do.doId, self.ourChannel, STATESERVER_OBJECT_DELETE_RAM) - dg.addUint32(do.doId) - self.send(dg) - - def connect(self, host, port=7199): - """ - Connect to a Message Director. The airConnected message is sent upon - success. - N.B. This overrides the base class's connect(). You cannot use the - ConnectionRepository connect() parameters. - """ - - url = URLSpec() - url.setServer(host) - url.setPort(port) - - self.notify.info('Now connecting to %s:%s...' % (host, port)) - ConnectionRepository.connect(self, [url], - successCallback=self.__connected, - failureCallback=self.__connectFailed, - failureArgs=[host, port]) - - def __connected(self): - self.notify.info('Connected successfully.') - - # Listen to our channel... - self.registerForChannel(self.ourChannel) - - # If we're configured with a State Server, register a post-remove to - # clean up whatever objects we own on this server should we unexpectedly - # fall over and die. - if self.serverId: - dg = PyDatagram() - dg.addServerHeader(self.serverId, self.ourChannel, STATESERVER_DELETE_AI_OBJECTS) - dg.addChannel(self.ourChannel) - self.addPostRemove(dg) - - messenger.send('airConnected') - self.handleConnected() - - def __connectFailed(self, code, explanation, host, port): - self.notify.warning('Failed to connect! (code=%s; %r)' % (code, explanation)) - - # Try again... - retryInterval = config.GetFloat('air-reconnect-delay', 5.0) - taskMgr.doMethodLater(retryInterval, self.connect, 'Reconnect delay', extraArgs=[host, port]) - - def handleConnected(self): - """ - Subclasses should override this if they wish to handle the connection - event. - """ - - def lostConnection(self): - # This should be overridden by a subclass if unexpectedly losing connection - # is okay. - self.notify.error('Lost connection to gameserver!') - - def setEventLogHost(self, host, port=7197): - """ - Set the target host for Event Logger messaging. This should be pointed - at the UDP IP:port that hosts the cluster's running Event Logger. - Providing a value of None or an empty string for 'host' will disable - event logging. - """ - - if not host: - self.eventSocket = None - return - - address = SocketAddress() - if not address.setHost(host, port): - self.notify.warning('Invalid Event Log host specified: %s:%s' % (host, port)) - self.eventSocket = None - else: - self.eventSocket = SocketUDPOutgoing() - self.eventSocket.InitToAddress(address) - - def writeServerEvent(self, logtype, *args, **kwargs): - """ - Write an event to the central Event Logger, if one is configured. - The purpose of the Event Logger is to keep a game-wide record of all - interesting in-game events that take place. Therefore, this function - should be used whenever such an interesting in-game event occurs. - """ - - if self.eventSocket is not None: - return # No event logger configured! - - log = collections.OrderedDict() - log['type'] = logtype - log['sender'] = self.eventLogId - - for i,v in enumerate(args): - # +1 because the logtype was _0, so we start at _1 - log['_%d' % (i+1)] = v - - log.update(kwargs) - - dg = PyDatagram() - msgpack_encode(dg, log) - self.eventSocket.Send(dg.getMessage()) - - def setAI(self, doId, aiChannel): - """ - Sets the AI of the specified DistributedObjectAI to be the specified channel. - Generally, you should not call this method, and instead call DistributedObjectAI.setAI. - """ - - dg = PyDatagram() - dg.addServerHeader(doId, aiChannel, STATESERVER_OBJECT_SET_AI) - dg.add_uint64(aiChannel) - self.send(dg) - - def eject(self, clientChannel, reasonCode, reason): - """ - Kicks the client residing at the specified clientChannel, using the specifed reasoning. - """ - - dg = PyDatagram() - dg.addServerHeader(clientChannel, self.ourChannel, CLIENTAGENT_EJECT) - dg.add_uint16(reasonCode) - dg.addString(reason) - self.send(dg) - - def setClientState(self, clientChannel, state): - """ - Sets the state of the client on the CA. - Useful for logging in and logging out, and for little else. - """ - - dg = PyDatagram() - dg.addServerHeader(clientChannel, self.ourChannel, CLIENTAGENT_SET_STATE) - dg.add_uint16(state) - self.send(dg) - - def setAllowClientSend(self, do, channelId, fieldNameList=[]): - """ - Overrides the security of a field(s) specified, allows an owner of a DistributedObject to send - the field(s) regardless if its marked ownsend/clsend. - """ - - dg = PyDatagram() - dg.addServerHeader(channelId, self.ourChannel, CLIENTAGENT_SET_FIELDS_SENDABLE) - fieldIds = [] - for fieldName in fieldNameList: - field = do.dclass.getFieldByName(fieldName) - - if not field: - continue - - fieldIds.append(field.getNumber()) - - dg.addUint32(do.doId) - dg.addUint16(len(fieldIds)) - for fieldId in fieldIds: - dg.addUint16(fieldId) - - self.send(dg) - - def clientAddSessionObject(self, clientChannel, doId): - """ - Declares the specified DistributedObject to be a "session object", - meaning that it is destroyed when the client disconnects. - Generally used for avatars owned by the client. - """ - - dg = PyDatagram() - dg.addServerHeader(clientChannel, self.ourChannel, CLIENTAGENT_ADD_SESSION_OBJECT) - dg.add_uint32(doId) - self.send(dg) - - def clientAddInterest(self, client_channel: int, interest_id: int, parent_id: int, zone_id: int, callback: object = None) -> None: - """ - Opens an interest on the behalf of the client. This, used in conjunction - with add_interest: visible (or preferably, disabled altogether), will mitigate - possible security risks. - """ - - dg = PyDatagram() - dg.addServerHeader(client_channel, self.ourChannel, CLIENTAGENT_ADD_INTEREST) - dg.add_uint16(interest_id) - dg.add_uint32(parent_id) - dg.add_uint32(zone_id) - self.send(dg) - - if callback != None: - ctx = (client_channel, interest_id) - self.__callbacks[ctx] = callback - - def client_add_interest_multiple(self, client_channel: int, interest_id: int, parent_id: int, zone_list: int, callback: object = None) -> None: - """ - """ - - dg = PyDatagram() - dg.addServerHeader(client_channel, self.ourChannel, CLIENTAGENT_ADD_INTEREST_MULTIPLE) - dg.addUint16(interest_id) - dg.addUint32(parent_id) - dg.addUint16(len(zone_list)) - for zoneId in zone_list: - dg.addUint32(zoneId) - - if callback != None: - ctx = (client_channel, interest_id) - self.__callbacks[ctx] = callback - - self.send(dg) - - def client_remove_interest(self, client_channel: int, interest_id: int, callback: object = None) -> None: - """ - """ - - dg = PyDatagram() - dg.addServerHeader(client_channel, self.ourChannel, CLIENTAGENT_REMOVE_INTEREST) - dg.addUint16(interest_id) - self.send(dg) - - if callback != None: - ctx = (client_channel, interest_id) - self.__callbacks[ctx] = callback - - def handle_client_agent_interest_done_resp(self, di: PyDatagramIterator) -> None: - """ - Sent by the ClientAgent to the caller of CLIENTAGENT_ADD_INTEREST to inform them that the interest operation has completed. - """ - - client_channel = di.getUint64() - interest_id = di.getUint16() - ctx = (client_channel, interest_id) - - if ctx not in self.__callbacks: - self.notify.warning('Received unexpected CLIENTAGENT_DONE_INTEREST_RESP (ctx: (%s, %s))' % ctx) - return - - try: - self.__callbacks[ctx](client_channel, interest_id) - finally: - del self.__callbacks[ctx] - - - def setOwner(self, doId: int, newOwner: int) -> None: - """ - Sets the owner of a DistributedObject. This will enable the new owner to send "ownsend" fields, - and will generate an OwnerView. - """ - - dg = PyDatagram() - dg.addServerHeader(doId, self.ourChannel, STATESERVER_OBJECT_SET_OWNER) - dg.add_uint64(newOwner) - self.send(dg) - - set_owner = setOwner - - # Snake case helpers - set_event_log_host = setEventLogHost - write_server_event = writeServerEvent - set_ai = setAI - set_client_state = setClientState - client_add_session_object = clientAddSessionObject - -# --------------------------------------------------------------------------------------------------------------------------------------------------------------------- # -# Helper utility functions for talking to the Astron EventLogger server implementation - -# Helper functions for logging output: -def msgpack_length(dg, length, fix, maxfix, tag8, tag16, tag32): - if length < maxfix: - dg.addUint8(fix + length) - elif tag8 is not None and length < 1<<8: - dg.addUint8(tag8) - dg.addUint8(length) - elif tag16 is not None and length < 1<<16: - dg.addUint8(tag16) - dg.addBeUint16(length) - elif tag32 is not None and length < 1<<32: - dg.addUint8(tag32) - dg.addBeUint32(length) - else: - raise ValueError('Value too big for MessagePack') - -def msgpack_encode(dg, element): - if element == None: - dg.addUint8(0xc0) - elif element is False: - dg.addUint8(0xc2) - elif element is True: - dg.addUint8(0xc3) - elif isinstance(element, int): - if -32 <= element < 128: - dg.addInt8(element) - elif 128 <= element < 256: - dg.addUint8(0xcc) - dg.addUint8(element) - elif 256 <= element < 65536: - dg.addUint8(0xcd) - dg.addBeUint16(element) - elif 65536 <= element < (1<<32): - dg.addUint8(0xce) - dg.addBeUint32(element) - elif (1<<32) <= element < (1<<64): - dg.addUint8(0xcf) - dg.addBeUint64(element) - elif -128 <= element < -32: - dg.addUint8(0xd0) - dg.addInt8(element) - elif -32768 <= element < -128: - dg.addUint8(0xd1) - dg.addBeInt16(element) - elif -1<<31 <= element < -32768: - dg.addUint8(0xd2) - dg.addBeInt32(element) - elif -1<<63 <= element < -1<<31: - dg.addUint8(0xd3) - dg.addBeInt64(element) - else: - raise ValueError('int out of range for msgpack: %d' % element) - elif isinstance(element, dict): - msgpack_length(dg, len(element), 0x80, 0x10, None, 0xde, 0xdf) - for k,v in list(element.items()): - msgpack_encode(dg, k) - msgpack_encode(dg, v) - elif isinstance(element, list): - msgpack_length(dg, len(element), 0x90, 0x10, None, 0xdc, 0xdd) - for v in element: - msgpack_encode(dg, v) - elif isinstance(element, str): - # 0xd9 is str 8 in all recent versions of the MsgPack spec, but somehow - # Logstash bundles a MsgPack implementation SO OLD that this isn't - # handled correctly so this function avoids it too - msgpack_length(dg, len(element), 0xa0, 0x20, None, 0xda, 0xdb) - dg.appendData(element.encode('utf-8')) - elif isinstance(element, float): - # Python does not distinguish between floats and doubles, so we send - # everything as a double in MsgPack: - dg.addUint8(0xcb) - dg.addBeFloat64(element) - else: - raise TypeError('Encountered non-MsgPack-packable value: %r' % element) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..79338ce --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +panda3d +panda3d-toolbox \ No newline at end of file diff --git a/setup.py b/setup.py index a8835fd..39731ea 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ Setup script for building the module into a package for publishing to PyPI. """ -from setuptools import setup +from setuptools import setup, find_namespace_packages import sys import os @@ -15,7 +15,14 @@ def get_version() -> str: minor = os.environ.get('MINOR', '0') patch = os.environ.get('PATCH', '0') - return f'{major}.{minor}.{patch}' + # Determine if this is a pre-release version + release_flag = os.environ.get('RELEASE', 'true').lower() + prerelease = not bool(release_flag) + + if not prerelease: + return f'{major}.{minor}.{patch}' + else: + return f'{major}.{minor}.{patch}.dev' def get_readme(filename: str = 'README.md') -> str: """ @@ -49,7 +56,7 @@ def main() -> int: # Define some constants module_name = 'panda3d_astron' - + # Run the setup setup( name=module_name, @@ -61,10 +68,8 @@ def main() -> int: author='Jordan Maxwell', maintainer='Jordan Maxwell', url=get_package_url(module_name), - packages=[module_name], - install_requires=[ - "panda3d" - ], + packages=find_namespace_packages(), + install_requires=get_requirements(), classifiers=[ 'Programming Language :: Python :: 3', ])