Initial commit
This commit is contained in:
201
.gitignore
vendored
Normal file
201
.gitignore
vendored
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[codz]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# 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.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# 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
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
|
# 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
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
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/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||||
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||||
|
# refer to https://docs.cursor.com/context/ignore-files
|
||||||
|
.cursorignore
|
||||||
|
.cursorindexingignore
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
20
license.md
Normal file
20
license.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
## ZLib
|
||||||
|
```
|
||||||
|
Copyright (c) 2024 Kyler "Félix" Eastridge
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
```
|
||||||
0
pymetaverse/__init__.py
Normal file
0
pymetaverse/__init__.py
Normal file
45
pymetaverse/agent.py
Normal file
45
pymetaverse/agent.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from .simulator import Simulator
|
||||||
|
from .eventtarget import EventTarget
|
||||||
|
from . import messages
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
class Agent:
|
||||||
|
def __init__(self):
|
||||||
|
self.simulator = None
|
||||||
|
self.simulators = []
|
||||||
|
self.messageTemplate = messages.getDefaultTemplate()
|
||||||
|
|
||||||
|
async def addSimulator(self, host, circuit, caps = None, parent = False):
|
||||||
|
sim = Simulator(self)
|
||||||
|
await sim.connect(host, circuit)
|
||||||
|
self.simulators.append(sim)
|
||||||
|
|
||||||
|
if caps:
|
||||||
|
await sim.fetchCapabilities(caps)
|
||||||
|
|
||||||
|
if parent:
|
||||||
|
self.simulator = sim
|
||||||
|
|
||||||
|
return sim
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def fromLogin(cls, login):
|
||||||
|
self = cls()
|
||||||
|
self.agentId = login["agent_id"]
|
||||||
|
self.sessionId = login["session_id"]
|
||||||
|
self.secureSessionId = login["secure_session_id"]
|
||||||
|
await self.addSimulator((login["sim_ip"], login["sim_port"]), login["circuit_code"], login["seed_capability"], True)
|
||||||
|
msg = self.messageTemplate.getMessage("CompleteAgentMovement")
|
||||||
|
msg.AgentData.AgentID = self.agentId
|
||||||
|
msg.AgentData.SessionID = self.sessionId
|
||||||
|
msg.AgentData.CircuitCode = login["circuit_code"]
|
||||||
|
self.simulator.send(msg, True)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
if self.simulator == None:
|
||||||
|
print("Simulator gone! :(")
|
||||||
|
break
|
||||||
|
|
||||||
64
pymetaverse/circuit.py
Normal file
64
pymetaverse/circuit.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import asyncio
|
||||||
|
from .eventtarget import EventTarget
|
||||||
|
from . import packet
|
||||||
|
|
||||||
|
class Circuit(asyncio.Protocol, EventTarget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.transport = None
|
||||||
|
self.sequence = 0
|
||||||
|
self.unackd = {}
|
||||||
|
self.acks = []
|
||||||
|
|
||||||
|
def nextSequence(self):
|
||||||
|
seq = self.sequence
|
||||||
|
self.sequence += 1
|
||||||
|
return seq
|
||||||
|
|
||||||
|
def acknowledge(self, sequences):
|
||||||
|
for ack in sequences:
|
||||||
|
if ack in self.unackd:
|
||||||
|
self.unackd.pop(ack)
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def datagram_received(self, data, addr):
|
||||||
|
pkt = packet.Packet.fromBytes(data)
|
||||||
|
if pkt.reliable:
|
||||||
|
self.acks.append(pkt.sequence)
|
||||||
|
|
||||||
|
# Has acks, acknowledge them!
|
||||||
|
if pkt.flags & pkt.FLAGS.ACK:
|
||||||
|
self.acknowledge(pkt.acks)
|
||||||
|
|
||||||
|
self.fire("message", addr, pkt.body)
|
||||||
|
|
||||||
|
def error_received(self, exc):
|
||||||
|
self.fire("error", exc)
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
self.fire("close", exc)
|
||||||
|
|
||||||
|
def send(self, message, reliable = False):
|
||||||
|
pkt = packet.Packet(self.nextSequence(), bytes(message), acks=self.acks)
|
||||||
|
if reliable:
|
||||||
|
pkt.reliable = True
|
||||||
|
self.unackd[pkt.sequence] = pkt
|
||||||
|
|
||||||
|
self.transport.sendto(pkt.toBytes())
|
||||||
|
|
||||||
|
def resend(self, distance = 100):
|
||||||
|
cutoff = self.sequence - distance
|
||||||
|
for pkt in self.unackd.values():
|
||||||
|
if pkt.sequence < cutoff:
|
||||||
|
pkt.resent = True
|
||||||
|
self.transport.sendto(pkt.toBytes())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, host, loop = None):
|
||||||
|
loop = loop or asyncio.get_running_loop()
|
||||||
|
transport, protocol = await loop.create_datagram_endpoint(
|
||||||
|
lambda: cls(),
|
||||||
|
remote_addr=host)
|
||||||
|
return protocol
|
||||||
24
pymetaverse/eventtarget.py
Normal file
24
pymetaverse/eventtarget.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
class EventTarget:
|
||||||
|
def __init__(self):
|
||||||
|
self._listeners = {}
|
||||||
|
|
||||||
|
def on(self, event, func = None):
|
||||||
|
if not event in self._listeners:
|
||||||
|
self._listeners[event] = []
|
||||||
|
|
||||||
|
def _(func):
|
||||||
|
self._listeners[event].append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
if func:
|
||||||
|
return _(func)
|
||||||
|
return _
|
||||||
|
|
||||||
|
def off(self, event, func):
|
||||||
|
if event in self._listeners:
|
||||||
|
self._listeners[event].remove(func)
|
||||||
|
|
||||||
|
def fire(self, event, *args, **kwargs):
|
||||||
|
if event in self._listeners:
|
||||||
|
for func in self._listeners[event]:
|
||||||
|
func(*args, **kwargs)
|
||||||
61
pymetaverse/httpclient.py
Normal file
61
pymetaverse/httpclient.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import aiohttp
|
||||||
|
|
||||||
|
class HttpResponse:
|
||||||
|
def __init__(self, handle):
|
||||||
|
self._handle = handle
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
self._handle.close()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
return self._handle.status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
return self._handle.headers
|
||||||
|
|
||||||
|
async def read(self):
|
||||||
|
return await self._handle.read()
|
||||||
|
|
||||||
|
class HttpClient:
|
||||||
|
def __init__(self):
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self._session = aiohttp.ClientSession()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
await self._session.close()
|
||||||
|
|
||||||
|
async def get(self, url, **kwargs):
|
||||||
|
response = await self._session.get(url, **kwargs)
|
||||||
|
return HttpResponse(response)
|
||||||
|
|
||||||
|
async def post(self, url, **kwargs):
|
||||||
|
response = await self._session.post(url, **kwargs)
|
||||||
|
return HttpResponse(response)
|
||||||
|
|
||||||
|
async def put(self, url, **kwargs):
|
||||||
|
response = await self._session.put(url, **kwargs)
|
||||||
|
return HttpResponse(response)
|
||||||
|
|
||||||
|
async def delete(self, url, **kwargs):
|
||||||
|
response = await self._session.delete(url, **kwargs)
|
||||||
|
return HttpResponse(response)
|
||||||
|
|
||||||
|
async def head(self, url, **kwargs):
|
||||||
|
response = await self._session.head(url, **kwargs)
|
||||||
|
return HttpResponse(response)
|
||||||
|
|
||||||
|
async def options(self, url, **kwargs):
|
||||||
|
response = await self._session.options(url, **kwargs)
|
||||||
|
return HttpResponse(response)
|
||||||
|
|
||||||
|
async def patch(self, url, **kwargs):
|
||||||
|
response = await self._session.patch(url, **kwargs)
|
||||||
|
return HttpResponse(response)
|
||||||
380
pymetaverse/llsd.py
Normal file
380
pymetaverse/llsd.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Name: llsd.py
|
||||||
|
Purpose: Parse and serialize LLSD objects
|
||||||
|
|
||||||
|
Copyright (c) 2021 Kyler Eastridge
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
import datetime
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import struct
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
class URI(str):
|
||||||
|
def __repr__(self):
|
||||||
|
return "URI({})".format(super().__repr__())
|
||||||
|
|
||||||
|
#Encoders
|
||||||
|
def llsdEncodeXml(input, destination, *args, optimize = False, encoding = "base64", **kwargs):
|
||||||
|
t = type(input)
|
||||||
|
if input == None:
|
||||||
|
elm = ET.SubElement(destination, "undef")
|
||||||
|
elif t == bool:
|
||||||
|
elm = ET.SubElement(destination, "boolean")
|
||||||
|
if input:
|
||||||
|
elm.text = "true"
|
||||||
|
elif not optimize:
|
||||||
|
elm.text = "false"
|
||||||
|
elif t == int:
|
||||||
|
elm = ET.SubElement(destination, "integer")
|
||||||
|
if input != 0 or not optimize:
|
||||||
|
elm.text = str(input)
|
||||||
|
elif t == float:
|
||||||
|
elm = ET.SubElement(destination, "real")
|
||||||
|
if input != 0 or not optimize:
|
||||||
|
elm.text = str(input)
|
||||||
|
elif t == uuid.UUID:
|
||||||
|
elm = ET.SubElement(destination, "uuid")
|
||||||
|
if input.bytes != b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" or not optimize:
|
||||||
|
elm.text = str(input)
|
||||||
|
elif t == str:
|
||||||
|
elm = ET.SubElement(destination, "string")
|
||||||
|
if input != "" or not optimize:
|
||||||
|
elm.text = input
|
||||||
|
elif t == bytes:
|
||||||
|
encoder = encoding
|
||||||
|
elm = ET.SubElement(destination, "binary")
|
||||||
|
if input != b"" or not optimize:
|
||||||
|
if encoder == "base64":
|
||||||
|
elm.text = base64.b64encode(input).decode()
|
||||||
|
elif encoder == "base85":
|
||||||
|
elm.text = base64.b85encode(input).decode()
|
||||||
|
elif encoder == "base16":
|
||||||
|
elm.text = base64.b16encode(input).decode()
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown binary encoding {}!".format(encoder))
|
||||||
|
elif t == datetime.datetime:
|
||||||
|
elm = ET.SubElement(destination, "date")
|
||||||
|
elm.text = input.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||||
|
elif t == URI:
|
||||||
|
elm = ET.SubElement(destination, "uri")
|
||||||
|
if input != "" or not optimize:
|
||||||
|
elm.text = input
|
||||||
|
elif t == dict:
|
||||||
|
root = ET.SubElement(destination, "map")
|
||||||
|
for key in input:
|
||||||
|
if type(key) != str:
|
||||||
|
raise ValueError("Dictionary keys must be type str, not {}!".format(type(key)))
|
||||||
|
elm = ET.SubElement(root, "key")
|
||||||
|
elm.text = key
|
||||||
|
llsdEncodeXml(input[key], root, *args, **kwargs)
|
||||||
|
elif t == list:
|
||||||
|
root = ET.SubElement(destination, "array")
|
||||||
|
for value in input:
|
||||||
|
llsdEncodeXml(value, root, *args, **kwargs)
|
||||||
|
|
||||||
|
def llsdEncode(input, *args, format = "xml", **kwargs):
|
||||||
|
if format == "xml":
|
||||||
|
root = ET.Element("llsd")
|
||||||
|
if "optimize" not in kwargs:
|
||||||
|
kwargs["optimize"] = True
|
||||||
|
llsdEncodeXml(input, root, *args, **kwargs)
|
||||||
|
xml = ET.ElementTree(root)
|
||||||
|
f = io.BytesIO()
|
||||||
|
xml.write(f, encoding='UTF-8', xml_declaration=True)
|
||||||
|
return f.getvalue()
|
||||||
|
|
||||||
|
#Decoders
|
||||||
|
def parseISODate(input):
|
||||||
|
try:
|
||||||
|
if input[-1] == "Z":
|
||||||
|
input = input[:-1]
|
||||||
|
date, time = input.split("T", 2)
|
||||||
|
year, month, day = date.split("-", 3)
|
||||||
|
hour, minute, second = time.split(":", 3)
|
||||||
|
if "." in second:
|
||||||
|
second, microsecond = second.split(".", 2)
|
||||||
|
else:
|
||||||
|
microsecond = 0
|
||||||
|
return datetime.datetime(*[int(i) for i in [year, month, day, hour, minute, second, microsecond]])
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("Invalid timestamp '{}'!".format(input))
|
||||||
|
|
||||||
|
def llsdDecodeXml(input):
|
||||||
|
if input.tag == "undef":
|
||||||
|
return None
|
||||||
|
elif input.tag == "boolean":
|
||||||
|
value = input.text
|
||||||
|
if value == None:
|
||||||
|
return False
|
||||||
|
value = value.lower()
|
||||||
|
if value in ["1", "true"]:
|
||||||
|
return True
|
||||||
|
elif value in ["", "0", "false"]:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise ValueError("Unexpected value '{}' for boolean!".format(value))
|
||||||
|
elif input.tag == "integer":
|
||||||
|
if input.text == None:
|
||||||
|
return 0
|
||||||
|
return int(input.text)
|
||||||
|
elif input.tag == "real":
|
||||||
|
if input.text == None:
|
||||||
|
return 0
|
||||||
|
return float(input.text)
|
||||||
|
elif input.tag == "uuid":
|
||||||
|
if input.text == None:
|
||||||
|
return uuid.UUID(bytes=b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")
|
||||||
|
return uuid.UUID(input.text)
|
||||||
|
elif input.tag == "string":
|
||||||
|
if input.text == None:
|
||||||
|
return ""
|
||||||
|
return input.text
|
||||||
|
elif input.tag == "binary":
|
||||||
|
if input.text == None:
|
||||||
|
return b""
|
||||||
|
encoding = input.attrib.get("encoding", "base64").lower()
|
||||||
|
if encoding == "base64":
|
||||||
|
return base64.b64decode(input.text)
|
||||||
|
elif encoding == "base85":
|
||||||
|
return base64.b85decode(input.text)
|
||||||
|
elif encoding == "base16":
|
||||||
|
return base64.b16decode(input.text)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown encoding {} for binary element!".format(encoding))
|
||||||
|
elif input.tag == "date":
|
||||||
|
if input.text == None:
|
||||||
|
return datetime.datetime.fromtimestamp(0)
|
||||||
|
return parseISODate(input.text)
|
||||||
|
elif input.tag == "uri":
|
||||||
|
if input.text == None:
|
||||||
|
return URI("")
|
||||||
|
return URI(input.text)
|
||||||
|
elif input.tag == "map":
|
||||||
|
result = {}
|
||||||
|
for i in range(0, len(input), 2):
|
||||||
|
if input[i].tag != "key":
|
||||||
|
raise ValueError("Unexpected {} element in map, expected key!".format(input[i].tag))
|
||||||
|
result[input[i].text] = llsdDecodeXml(input[i+1])
|
||||||
|
return result
|
||||||
|
elif input.tag == "array":
|
||||||
|
result = [None]*len(input)
|
||||||
|
for i in range(0, len(input)):
|
||||||
|
result[i] = llsdDecodeXml(input[i])
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise ValueError("Unexpected {} element in LLSD!".format(input.tag))
|
||||||
|
|
||||||
|
def llsdDecode(input, *args, format = None, maxHeaderLength = 128, **kwargs):
|
||||||
|
if format == None:
|
||||||
|
isBytes = type(input) == bytes
|
||||||
|
i = 0
|
||||||
|
l = len(input)
|
||||||
|
while i < l and i < maxHeaderLength:
|
||||||
|
if isBytes:
|
||||||
|
c = chr(input[i])
|
||||||
|
else:
|
||||||
|
c = input[i]
|
||||||
|
if c == '"' or c == "'":
|
||||||
|
quoteChar = c
|
||||||
|
while i < l and i < maxHeaderLength:
|
||||||
|
i += 1
|
||||||
|
if isBytes:
|
||||||
|
c = chr(input[i])
|
||||||
|
else:
|
||||||
|
c = input[i]
|
||||||
|
if c == quoteChar:
|
||||||
|
break
|
||||||
|
elif c == "\\":
|
||||||
|
#Assuming the file is valid, no unicode should be in the
|
||||||
|
#header
|
||||||
|
i += 1
|
||||||
|
i += 1
|
||||||
|
i += 1
|
||||||
|
if c == ">":
|
||||||
|
break
|
||||||
|
header = input[2:i-2].strip().lower()
|
||||||
|
if header == "llsd/notation":
|
||||||
|
format = "notation"
|
||||||
|
elif header == "llsd/binary":
|
||||||
|
format = "binary"
|
||||||
|
else:
|
||||||
|
tmp = header[0:3]
|
||||||
|
if type(tmp) == bytes:
|
||||||
|
tmp = tmp.decode()
|
||||||
|
if tmp == "xml":
|
||||||
|
format = "xml"
|
||||||
|
else:
|
||||||
|
raise ValueError("Unable to detect serialization format!")
|
||||||
|
|
||||||
|
if format == "xml":
|
||||||
|
input = ET.fromstring(input)
|
||||||
|
if input.tag != "llsd":
|
||||||
|
raise ValueError("Unexpected tag {} in LLSD+XML!".format(input.tag))
|
||||||
|
return llsdDecodeXml(input[0])
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown serialization format {}!".format(format))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
source_test = {
|
||||||
|
"undef": [None],
|
||||||
|
"boolean": [True, False],
|
||||||
|
"integer": [289343, -3, 0],
|
||||||
|
"real": [-0.28334, 2983287453.3848387, 0.0],
|
||||||
|
"uuid": [
|
||||||
|
uuid.UUID("d7f4aeca-88f1-42a1-b385-b9db18abb255"),
|
||||||
|
uuid.UUID("00000000-0000-0000-0000-000000000000")
|
||||||
|
],
|
||||||
|
"string": [
|
||||||
|
"The quick brown fox jumped over the lazy dog.",
|
||||||
|
"540943c1-7142-4fdd-996f-fc90ed5dd3fa",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"binary": [
|
||||||
|
b"The quick brown fox jumped over the lazy dog."
|
||||||
|
],
|
||||||
|
"date": [
|
||||||
|
datetime.datetime.now()
|
||||||
|
],
|
||||||
|
"uri": [
|
||||||
|
URI("http://sim956.agni.lindenlab.com:12035/runtime/agents")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
llsd_xml_test = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<llsd>
|
||||||
|
<map>
|
||||||
|
<key>undef</key>
|
||||||
|
<array>
|
||||||
|
<undef />
|
||||||
|
</array>
|
||||||
|
<key>boolean</key>
|
||||||
|
<array>
|
||||||
|
<!-- true -->
|
||||||
|
<boolean>1</boolean>
|
||||||
|
<boolean>true</boolean>
|
||||||
|
|
||||||
|
<!-- false -->
|
||||||
|
<boolean>0</boolean>
|
||||||
|
<boolean>false</boolean>
|
||||||
|
<boolean />
|
||||||
|
</array>
|
||||||
|
<key>integer</key>
|
||||||
|
<array>
|
||||||
|
<integer>289343</integer>
|
||||||
|
<integer>-3</integer>
|
||||||
|
<integer /> <!-- zero -->
|
||||||
|
</array>
|
||||||
|
<key>real</key>
|
||||||
|
<array>
|
||||||
|
<real>-0.28334</real>
|
||||||
|
<real>2983287453.3848387</real>
|
||||||
|
<real /> <!-- exactly zero -->
|
||||||
|
</array>
|
||||||
|
<key>uuid</key>
|
||||||
|
<array>
|
||||||
|
<uuid>d7f4aeca-88f1-42a1-b385-b9db18abb255</uuid>
|
||||||
|
<uuid /> <!-- null uuid '00000000-0000-0000-0000-000000000000' -->
|
||||||
|
</array>
|
||||||
|
<key>string</key>
|
||||||
|
<array>
|
||||||
|
<string>The quick brown fox jumped over the lazy dog.</string>
|
||||||
|
<string>540943c1-7142-4fdd-996f-fc90ed5dd3fa</string>
|
||||||
|
<string /> <!-- empty string -->
|
||||||
|
</array>
|
||||||
|
<key>binary</key>
|
||||||
|
<array>
|
||||||
|
<binary encoding="base64">cmFuZG9t</binary> <!-- base 64 encoded binary data -->
|
||||||
|
<binary>dGhlIHF1aWNrIGJyb3duIGZveA==</binary> <!-- base 64 encoded binary data is default -->
|
||||||
|
<binary encoding="base85">YISXJWn>_4c4cxPbZBJ</binary>
|
||||||
|
<binary encoding="base16">6C617A7920646F67</binary>
|
||||||
|
<binary /> <!-- empty binary blob -->
|
||||||
|
</array>
|
||||||
|
<key>date</key>
|
||||||
|
<array>
|
||||||
|
<date>2006-02-01T14:29:53.43Z</date>
|
||||||
|
<date /> <!-- epoch -->
|
||||||
|
</array>
|
||||||
|
<key>uri</key>
|
||||||
|
<array>
|
||||||
|
<uri>http://sim956.agni.lindenlab.com:12035/runtime/agents</uri>
|
||||||
|
<uri /> <!-- an empty link -->
|
||||||
|
</array>
|
||||||
|
</map>
|
||||||
|
</llsd>"""
|
||||||
|
|
||||||
|
llsd_notation_test = """<?llsd/notation?>
|
||||||
|
{
|
||||||
|
'undef':
|
||||||
|
[
|
||||||
|
!
|
||||||
|
],
|
||||||
|
'boolean':
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
t,
|
||||||
|
T,
|
||||||
|
true,
|
||||||
|
TRUE,
|
||||||
|
0
|
||||||
|
f,
|
||||||
|
F,
|
||||||
|
false,
|
||||||
|
FALSE
|
||||||
|
],
|
||||||
|
'integer':
|
||||||
|
[
|
||||||
|
i289343,
|
||||||
|
i-3
|
||||||
|
],
|
||||||
|
'real':
|
||||||
|
[
|
||||||
|
r-0.28334,
|
||||||
|
r2983287453.3848387
|
||||||
|
]
|
||||||
|
'uuid':
|
||||||
|
[
|
||||||
|
ud7f4aeca-88f1-42a1-b385-b9db18abb255
|
||||||
|
]
|
||||||
|
'string';
|
||||||
|
'The quick brown fox jumped over the lazy dog.'
|
||||||
|
"540943c1-7142-4fdd-996f-fc90ed5dd3fa",
|
||||||
|
s(10)'0123456789',
|
||||||
|
s(10)"0123456789"
|
||||||
|
]
|
||||||
|
'binary':
|
||||||
|
[
|
||||||
|
b64"cmFuZG9t",
|
||||||
|
b64'dGhlIHF1aWNrIGJyb3duIGZveA==',
|
||||||
|
b85"YISXJWn>_4c4cxPbZBJ",
|
||||||
|
b16'6C617A7920646F67',
|
||||||
|
b(10)'0123456789'
|
||||||
|
]
|
||||||
|
'date':
|
||||||
|
[
|
||||||
|
d"2006-02-01T14:29:53.43Z"
|
||||||
|
]
|
||||||
|
'uri':
|
||||||
|
[
|
||||||
|
l"http://sim956.agni.lindenlab.com:12035/runtime/agents"
|
||||||
|
]
|
||||||
|
}"""
|
||||||
168
pymetaverse/login.py
Executable file
168
pymetaverse/login.py
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
import uuid #For Mac addresses
|
||||||
|
from . import httpclient
|
||||||
|
from . import llsd
|
||||||
|
|
||||||
|
def getMacAddress():
|
||||||
|
mac = uuid.getnode()
|
||||||
|
return ':'.join(("%012X" % mac)[i:i+2] for i in range(0, 12, 2))
|
||||||
|
|
||||||
|
def getPlatform():
|
||||||
|
if sys.platform == "linux" or sys.platform == "linux2":
|
||||||
|
return "Lnx"
|
||||||
|
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
return "Mac"
|
||||||
|
|
||||||
|
elif sys.platform == "win32":
|
||||||
|
return "Win"
|
||||||
|
|
||||||
|
return "Unk"
|
||||||
|
|
||||||
|
#Convenience name
|
||||||
|
OPTIONS_NONE = []
|
||||||
|
|
||||||
|
#Enough to get us most capability out of the grid without restrictions
|
||||||
|
OPTIONS_MINIMAL = [
|
||||||
|
"adult_compliant",
|
||||||
|
]
|
||||||
|
|
||||||
|
#Normal stuff
|
||||||
|
OPTIONS_MOST = [
|
||||||
|
"inventory-root",
|
||||||
|
|
||||||
|
"inventory-lib-root",
|
||||||
|
"inventory-lib-owner",
|
||||||
|
|
||||||
|
"display_names",
|
||||||
|
"adult_compliant",
|
||||||
|
|
||||||
|
"advanced-mode",
|
||||||
|
|
||||||
|
"max_groups",
|
||||||
|
"max-agent-groups",
|
||||||
|
"map-server-url",
|
||||||
|
"login-flags",
|
||||||
|
]
|
||||||
|
|
||||||
|
#This may take longer to log in
|
||||||
|
OPTIONS_FULL = [
|
||||||
|
"inventory-root",
|
||||||
|
"inventory-skeleton",
|
||||||
|
"inventory-meat",
|
||||||
|
"inventory-skel-targets",
|
||||||
|
|
||||||
|
"inventory-lib-root",
|
||||||
|
"inventory-lib-owner",
|
||||||
|
"inventory-skel-lib",
|
||||||
|
"inventory-meat-lib",
|
||||||
|
|
||||||
|
"initial-outfit",
|
||||||
|
"gestures",
|
||||||
|
"display_names",
|
||||||
|
"event_categories",
|
||||||
|
"event_notifications",
|
||||||
|
"classified_categories",
|
||||||
|
"adult_compliant",
|
||||||
|
"buddy-list",
|
||||||
|
"newuser-config",
|
||||||
|
"ui-config",
|
||||||
|
|
||||||
|
"advanced-mode",
|
||||||
|
|
||||||
|
"max_groups",
|
||||||
|
"max-agent-groups",
|
||||||
|
"map-server-url",
|
||||||
|
"voice-config",
|
||||||
|
"tutorial_setting",
|
||||||
|
"login-flags",
|
||||||
|
"global-textures",
|
||||||
|
#"god-connect", #lol no
|
||||||
|
]
|
||||||
|
|
||||||
|
#WARNING: Not actually async yet! But make it a async request to allow it to be
|
||||||
|
#done so in the future!
|
||||||
|
async def Login(username, password,
|
||||||
|
start = "last",
|
||||||
|
options = None,
|
||||||
|
grid = "https://login.agni.lindenlab.com/cgi-bin/login.cgi",
|
||||||
|
isBot = True
|
||||||
|
):
|
||||||
|
|
||||||
|
if len(username) == 1:
|
||||||
|
username = (username[0], "resident")
|
||||||
|
|
||||||
|
#WARNING:
|
||||||
|
# Falsifying this is a violation of the Terms of Service
|
||||||
|
mac = getMacAddress()
|
||||||
|
|
||||||
|
platform = getPlatform()
|
||||||
|
|
||||||
|
#WARNING:
|
||||||
|
# Deviating from this format MAY be a violation of the Terms of Service
|
||||||
|
id0 = hashlib.md5("{}:{}:{}".format(
|
||||||
|
platform,
|
||||||
|
mac,
|
||||||
|
sys.version
|
||||||
|
).encode("latin")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
if options == None:
|
||||||
|
options = OPTIONS_MOST
|
||||||
|
|
||||||
|
requestBody = llsd.llsdEncode({
|
||||||
|
#Credentials
|
||||||
|
"first": username[0],
|
||||||
|
"last": username[1],
|
||||||
|
"passwd": "$1$" + hashlib.md5(password.encode("latin")).hexdigest(),
|
||||||
|
#"web_login_key": "",
|
||||||
|
|
||||||
|
#OS information
|
||||||
|
"platform": platform,
|
||||||
|
"platform_version": sys.version,
|
||||||
|
|
||||||
|
#Viewer information
|
||||||
|
"channel": "pymetaverse",
|
||||||
|
"version": "Testing", #TODO: Change this to metaverse.__VERSION__
|
||||||
|
#"major": 0,
|
||||||
|
#"minor": 0,
|
||||||
|
#"patch": 0,
|
||||||
|
#"build": 0,
|
||||||
|
|
||||||
|
#Machine information
|
||||||
|
"mac": mac, #WARNING: Falsifying this is a violation of the Terms of Service
|
||||||
|
"id0": id0, #WARNING: Falsifying this is a violation of the Terms of Service
|
||||||
|
#"viewer_digest": "",
|
||||||
|
|
||||||
|
#Ignore messages for now
|
||||||
|
"skipoptional": True,
|
||||||
|
"agree_to_tos": True,
|
||||||
|
"read_critical": True,
|
||||||
|
|
||||||
|
#Viewer options
|
||||||
|
"extended_errors": True,
|
||||||
|
"options": options,
|
||||||
|
"agent_flags": 2 if isBot else 0, #Bitmask, we are a bot, so set bit 2 to true,
|
||||||
|
"start": start,
|
||||||
|
#"functions": "", #No idea what this does
|
||||||
|
|
||||||
|
#Login error tracking
|
||||||
|
"last_exec_event": 0,
|
||||||
|
#"last_exec_froze": False,
|
||||||
|
#"last_exec_duration": 0,
|
||||||
|
|
||||||
|
#For proxied connections apparently:
|
||||||
|
#"service_proxy_ip": "",
|
||||||
|
|
||||||
|
"token": "",
|
||||||
|
"mfa_hash": ""
|
||||||
|
})
|
||||||
|
|
||||||
|
async with httpclient.HttpClient() as session:
|
||||||
|
async with await session.post(grid, data = requestBody, headers = {
|
||||||
|
"Content-Type": "application/llsd+xml"
|
||||||
|
}) as response:
|
||||||
|
return llsd.llsdDecode(await response.read())
|
||||||
|
|
||||||
9219
pymetaverse/message_template.msg
Executable file
9219
pymetaverse/message_template.msg
Executable file
File diff suppressed because it is too large
Load Diff
1
pymetaverse/message_template.msg.sha1
Executable file
1
pymetaverse/message_template.msg.sha1
Executable file
@@ -0,0 +1 @@
|
|||||||
|
1a9a3717fde5d0fb3d5f688a1a3dab7fcc2aa308
|
||||||
732
pymetaverse/messages.py
Normal file
732
pymetaverse/messages.py
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from collections import OrderedDict
|
||||||
|
from enum import Enum, auto
|
||||||
|
import ipaddress
|
||||||
|
import uuid
|
||||||
|
import struct
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
|
# These are shared in various places around the code
|
||||||
|
sUInt32 = struct.Struct(">I")
|
||||||
|
sUInt16 = struct.Struct(">H")
|
||||||
|
sUInt8 = struct.Struct(">B")
|
||||||
|
|
||||||
|
def ZeroEncode(buf):
|
||||||
|
output = io.BytesIO()
|
||||||
|
count = None
|
||||||
|
i = 0
|
||||||
|
l = len(buf)
|
||||||
|
count = 0
|
||||||
|
while i < l:
|
||||||
|
if buf[i] == 0:
|
||||||
|
count = 0
|
||||||
|
while i < l and buf[i] == 0:
|
||||||
|
count += 1
|
||||||
|
i += 1
|
||||||
|
if count == 255:
|
||||||
|
output.write(bytes([0, 0xFF]))
|
||||||
|
count = 0
|
||||||
|
if count != 0:
|
||||||
|
output.write(bytes([0, count]))
|
||||||
|
if i < l:
|
||||||
|
output.write(bytes([buf[i]]))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
def ZeroDecode(buf):
|
||||||
|
output = io.BytesIO()
|
||||||
|
count = None
|
||||||
|
i = 0
|
||||||
|
l = len(buf)
|
||||||
|
while i < l:
|
||||||
|
if buf[i] == 0:
|
||||||
|
i += 1
|
||||||
|
output.write(bytes(buf[i]))
|
||||||
|
else:
|
||||||
|
output.write(bytes([buf[i]]))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
class Block:
|
||||||
|
class TYPE(Enum):
|
||||||
|
NULL = auto()
|
||||||
|
FIXED = auto()
|
||||||
|
VARIABLE = auto()
|
||||||
|
U8 = auto()
|
||||||
|
U16 = auto()
|
||||||
|
U32 = auto()
|
||||||
|
U64 = auto()
|
||||||
|
S8 = auto()
|
||||||
|
S16 = auto()
|
||||||
|
S32 = auto()
|
||||||
|
S64 = auto()
|
||||||
|
F32 = auto()
|
||||||
|
F64 = auto()
|
||||||
|
LLVECTOR3 = auto()
|
||||||
|
LLVECTOR3D = auto()
|
||||||
|
LLVECTOR4 = auto()
|
||||||
|
LLQUATERNION = auto()
|
||||||
|
LLUUID = auto()
|
||||||
|
BOOL = auto()
|
||||||
|
IPADDR = auto()
|
||||||
|
IPPORT = auto()
|
||||||
|
|
||||||
|
#Unused
|
||||||
|
U16VEC3 = auto()
|
||||||
|
U16QUAT = auto()
|
||||||
|
S16ARRAY = auto()
|
||||||
|
|
||||||
|
# LL couldn't decide which endianness to use. The different endianness
|
||||||
|
# is intentional.
|
||||||
|
sVariable1 = struct.Struct(">B")
|
||||||
|
sVariable2 = struct.Struct(">H")
|
||||||
|
sU8 = struct.Struct("<B")
|
||||||
|
sU16 = struct.Struct("<H")
|
||||||
|
sU32 = struct.Struct("<I")
|
||||||
|
sU64 = struct.Struct("<Q")
|
||||||
|
sS8 = struct.Struct("<b")
|
||||||
|
sS16 = struct.Struct("<h")
|
||||||
|
sS32 = struct.Struct("<i")
|
||||||
|
sS64 = struct.Struct("<q")
|
||||||
|
sF32 = struct.Struct("<f")
|
||||||
|
sF64 = struct.Struct("<d")
|
||||||
|
sLLVector3 = struct.Struct("<fff")
|
||||||
|
sLLVector3d = struct.Struct("<ddd")
|
||||||
|
sLLVector4 = struct.Struct("<ffff")
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__setattr__('name', name)
|
||||||
|
super().__setattr__('parameters', OrderedDict())
|
||||||
|
super().__setattr__('values', {})
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__name__} {self.name}>"
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
if name in self.parameters:
|
||||||
|
return self.values.get(name) # Fix incorrect variable name
|
||||||
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
if name in self.parameters:
|
||||||
|
self.values[name] = value # Fix incorrect variable name
|
||||||
|
else:
|
||||||
|
super().__setattr__(name, value) # Prevent infinite recursion
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
res = io.BytesIO()
|
||||||
|
self.toStream(res)
|
||||||
|
return res.getvalue()
|
||||||
|
|
||||||
|
def toStream(self, handle):
|
||||||
|
for name, (type, size) in self.parameters.items():
|
||||||
|
if type == self.TYPE.NULL:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif type == self.TYPE.FIXED:
|
||||||
|
data = self.values.get(name, b"")[:size]
|
||||||
|
handle.write(data.ljust(size, b'\x00'))
|
||||||
|
|
||||||
|
elif type == self.TYPE.VARIABLE:
|
||||||
|
if size == 1:
|
||||||
|
data = self.values.get(name, b"")[:255]
|
||||||
|
handle.write(self.sVariable1.pack(len(data)))
|
||||||
|
handle.write(data)
|
||||||
|
|
||||||
|
elif size == 2:
|
||||||
|
data = self.values.get(name, b"")[:65535]
|
||||||
|
handle.write(self.sVariable2.pack(len(data)))
|
||||||
|
handle.write(data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid variable size {}".format(size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U8:
|
||||||
|
handle.write(self.sU8.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U16:
|
||||||
|
handle.write(self.sU16.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U32:
|
||||||
|
handle.write(self.sU32.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U64:
|
||||||
|
handle.write(self.sU64.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S8:
|
||||||
|
handle.write(self.sS8.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S16:
|
||||||
|
handle.write(self.sS16.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S32:
|
||||||
|
handle.write(self.sS32.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S64:
|
||||||
|
handle.write(self.sS64.pack(int(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.F32:
|
||||||
|
handle.write(self.sF32.pack(float(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.F64:
|
||||||
|
handle.write(self.sF64.pack(float(self.values.get(name, 0) or 0)))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLVECTOR3:
|
||||||
|
vec = self.values.get(name, (0,0,0)) or (0,0,0)
|
||||||
|
handle.write(self.sLLVector3.pack(vec[0], vec[1], vec[2]))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLVECTOR3D:
|
||||||
|
vec = self.values.get(name, (0,0,0)) or (0,0,0)
|
||||||
|
handle.write(self.sLLVector3d.pack(vec[0], vec[1], vec[2]))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLVECTOR4:
|
||||||
|
vec = self.values.get(name, (0,0,0,0)) or (0,0,0,0)
|
||||||
|
handle.write(self.sLLVector4.pack(vec[0], vec[1], vec[2], vec[3]))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLQUATERNION:
|
||||||
|
vec = self.values.get(name, (0,0,0)) or (0,0,0)
|
||||||
|
# NOTE: Quaternions are transmitted as vectors. The W component
|
||||||
|
# is missing and is just generated on the fly.
|
||||||
|
handle.write(self.sLLVector3.pack(vec[0], vec[1], vec[2]))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLUUID:
|
||||||
|
handle.write(uuid.UUID(self.values.get(name, "00000000-0000-0000-0000-000000000000") or "00000000-0000-0000-0000-000000000000").bytes)
|
||||||
|
|
||||||
|
elif type == self.TYPE.BOOL:
|
||||||
|
handle.write(b"\1" if bool(self.values.get(name, False) or False) else "\0")
|
||||||
|
|
||||||
|
# NOTE: IPADDR AND IPPORT USE THE BIG ENDIAN sUInt32 AND sUInt16
|
||||||
|
# THESE ARE NOT FROM THE MESSAGE CLASS, THEY ARE FROM THE GLOBAL SCOPE!
|
||||||
|
# IT IS INTENTIONAL!
|
||||||
|
elif type == self.TYPE.IPADDR:
|
||||||
|
handle.write(sUInt32.pack(int(ipaddress.IPv4Address(self.values.get(name, "0.0.0.0") or "0.0.0.0"))))
|
||||||
|
|
||||||
|
elif type == self.TYPE.IPPORT:
|
||||||
|
handle.write(sUInt16.pack(int(self.values.get(name, 0) or 0)&0xFFFF))
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown type {}".format(type))
|
||||||
|
|
||||||
|
def fromStream(self, handle):
|
||||||
|
for name, (type, size) in self.parameters.items():
|
||||||
|
if type == self.TYPE.NULL:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif type == self.TYPE.FIXED:
|
||||||
|
data = handle.read(size)
|
||||||
|
|
||||||
|
elif type == self.TYPE.VARIABLE:
|
||||||
|
if size == 1:
|
||||||
|
data = handle.read(self.sVariable1.unpack(handle.read(self.sVariable1.size))[0])
|
||||||
|
|
||||||
|
elif size == 2:
|
||||||
|
data = handle.read(self.sVariable2.unpack(handle.read(self.sVariable2.size))[0])
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Invalid variable size {}".format(size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U8:
|
||||||
|
data, = self.sU8.unpack(handle.read(self.sU8.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U16:
|
||||||
|
data, = self.sU16.unpack(handle.read(self.sU16.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U32:
|
||||||
|
data, = self.sU32.unpack(handle.read(self.sU32.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.U64:
|
||||||
|
data, = self.sU64.unpack(handle.read(self.sU64.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S8:
|
||||||
|
data, = self.sS8.unpack(handle.read(self.sS8.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S16:
|
||||||
|
data, = self.sS16.unpack(handle.read(self.sS16.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S32:
|
||||||
|
data, = self.sS32.unpack(handle.read(self.sS32.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.S64:
|
||||||
|
data, = self.sS64.unpack(handle.read(self.sS64.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.F32:
|
||||||
|
data, = self.sF32.unpack(handle.read(self.sF32.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.F64:
|
||||||
|
data, = self.sF64.unpack(handle.read(self.sF64.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLVECTOR3:
|
||||||
|
data = self.sLLVector3.unpack(handle.read(self.sLLVector3.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLVECTOR3D:
|
||||||
|
data = self.sLLVector3D.unpack(handle.read(self.sLLVector3D.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLVECTOR4:
|
||||||
|
data = self.sLLVector4.unpack(handle.read(self.sLLVector4.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLQUATERNION:
|
||||||
|
# NOTE: Quaternions are transmitted as vectors. The W component
|
||||||
|
# is missing and is just generated on the fly.
|
||||||
|
data = self.sLLVector3.unpack(handle.read(self.sLLVector3.size))
|
||||||
|
|
||||||
|
elif type == self.TYPE.LLUUID:
|
||||||
|
data = uuid.UUID(bytes=handle.read(16))
|
||||||
|
|
||||||
|
elif type == self.TYPE.BOOL:
|
||||||
|
data = handle.read(1)[0] != 0
|
||||||
|
|
||||||
|
# NOTE: IPADDR AND IPPORT USE THE BIG ENDIAN sUInt32 AND sUInt16
|
||||||
|
# THESE ARE NOT FROM THE MESSAGE CLASS, THEY ARE FROM THE GLOBAL SCOPE!
|
||||||
|
# IT IS INTENTIONAL!
|
||||||
|
elif type == self.TYPE.IPADDR:
|
||||||
|
data = ipaddress.IPv4Address(sUInt32.unpack(handle.read(sUInt32.size)[0]))
|
||||||
|
|
||||||
|
elif type == self.TYPE.IPPORT:
|
||||||
|
data, = sUInt16.unpack(handle.read(sUInt16.size))
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown type {}".format(type))
|
||||||
|
|
||||||
|
self.values[name] = data
|
||||||
|
|
||||||
|
def registerParameter(self, name, type, size):
|
||||||
|
self.parameters[name] = (type, size)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
block = Block(self.name)
|
||||||
|
block.parameters = self.parameters
|
||||||
|
|
||||||
|
return block
|
||||||
|
|
||||||
|
|
||||||
|
class BlockArray(Block):
|
||||||
|
def __init__(self, name, count = None):
|
||||||
|
super().__init__(name)
|
||||||
|
self.count = count
|
||||||
|
self.blocks = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__name__} {self.name}[{self.count or len(self.blocks)}]>"
|
||||||
|
|
||||||
|
def __getitem__(self, i):
|
||||||
|
if self.count != None and i > self.count:
|
||||||
|
raise IndexError("block index out of range")
|
||||||
|
|
||||||
|
for _ in range(len(self.blocks), i + 1):
|
||||||
|
self.blocks.append(Block(self.name))
|
||||||
|
self.blocks[-1].parameters = self.parameters
|
||||||
|
|
||||||
|
return self.blocks[i]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.count if self.count is not None else len(self.blocks)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for i in range(len(self)):
|
||||||
|
yield self[i]
|
||||||
|
|
||||||
|
def toStream(self, handle):
|
||||||
|
if self.count == None:
|
||||||
|
handle.write(sUInt8.pack(self.count))
|
||||||
|
|
||||||
|
for i in range(self.count or len(self.blocks)):
|
||||||
|
self[i].toStream(handle)
|
||||||
|
|
||||||
|
def fromStream(self, handle):
|
||||||
|
count = self.count
|
||||||
|
if count == None:
|
||||||
|
count, = sUInt8.unpack(handle.read(sUInt8.size))
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
self[i].fromStream(handle)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
block = BlockArray(self.name, self.count)
|
||||||
|
block.parameters = self.parameters
|
||||||
|
|
||||||
|
return block
|
||||||
|
|
||||||
|
class Message:
|
||||||
|
class FREQUENCY(Enum):
|
||||||
|
NULL = 0
|
||||||
|
HIGH = 1
|
||||||
|
MEDIUM = 2
|
||||||
|
LOW = 4
|
||||||
|
|
||||||
|
class TRUST(Enum):
|
||||||
|
TRUST = auto()
|
||||||
|
NOTRUST = auto()
|
||||||
|
|
||||||
|
class ENCODING(Enum):
|
||||||
|
UNENCODED = auto()
|
||||||
|
ZEROCODED = auto()
|
||||||
|
|
||||||
|
class DEPRECATION(Enum):
|
||||||
|
NOT = 0
|
||||||
|
UDPDEPRECATED = 1
|
||||||
|
UDPBLACKLISTED = 2
|
||||||
|
DEPRECATED = 3
|
||||||
|
|
||||||
|
def __init__(self, name, frequency, id, trust = None, encoding = None, deprecation = None):
|
||||||
|
self.name = name
|
||||||
|
self.frequency = frequency
|
||||||
|
self.id = id
|
||||||
|
self.trust = trust or self.TRUST.TRUST
|
||||||
|
self.encoding = encoding or self.ENCODING.UNENCODED
|
||||||
|
self.blocks = OrderedDict()
|
||||||
|
self.deprecation = deprecation or self.DEPRECATION.NOT
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__name__} {self.name} {self.frequency} {self.id} {self.trust} {self.encoding}>"
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return self.blocks[name]
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
buf = io.BytesIO()
|
||||||
|
self.toStream(buf, True)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
def toStream(self, handle, writeID = False):
|
||||||
|
if writeID:
|
||||||
|
if self.frequency == self.FREQUENCY.LOW:
|
||||||
|
handle.write(sUInt32.pack(self.id))
|
||||||
|
elif self.frequency == self.FREQUENCY.MEDIUM:
|
||||||
|
handle.write(sUInt16.pack(self.id))
|
||||||
|
elif self.frequency == self.FREQUENCY.HIGH:
|
||||||
|
handle.write(sUInt8.pack(self.id))
|
||||||
|
|
||||||
|
for block in self.blocks.values():
|
||||||
|
block.toStream(handle)
|
||||||
|
|
||||||
|
def load(self, handle, readID = False):
|
||||||
|
if readID:
|
||||||
|
# This doesn't do anything.
|
||||||
|
# Perhaps we could verify the ID?
|
||||||
|
if self.frequency == self.FREQUENCY.LOW:
|
||||||
|
handle.read(sUInt32.size)
|
||||||
|
elif self.frequency == self.FREQUENCY.MEDIUM:
|
||||||
|
handle.read(sUInt16.size)
|
||||||
|
elif self.frequency == self.FREQUENCY.HIGH:
|
||||||
|
handle.read(sUInt8.size)
|
||||||
|
|
||||||
|
for block in self.blocks.values():
|
||||||
|
block.fromStream(handle)
|
||||||
|
|
||||||
|
def loads(self, data, verifyID = True):
|
||||||
|
handle = io.BytesIO(data)
|
||||||
|
self.load(handle, verifyID)
|
||||||
|
|
||||||
|
def registerBlock(self, block):
|
||||||
|
if block.name in self.blocks:
|
||||||
|
raise Exception("Block {} already registered in message {}".format(block.name, self.name))
|
||||||
|
self.blocks[block.name] = block
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
msg = Message(self.name, self.frequency, self.id, self.trust, self.encoding)
|
||||||
|
for block in self.blocks.values():
|
||||||
|
msg.registerBlock(block.copy())
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
class MessageTemplate:
|
||||||
|
def __init__(self):
|
||||||
|
self.messages = {}
|
||||||
|
self.ids = {}
|
||||||
|
|
||||||
|
def registerMessage(self, message):
|
||||||
|
self.messages[message.name] = message
|
||||||
|
self.messages[message.id] = message
|
||||||
|
|
||||||
|
def getMessage(self, name):
|
||||||
|
return self.messages[name].copy()
|
||||||
|
|
||||||
|
def loadMessage(self, message):
|
||||||
|
if message[0] == 0xFF:
|
||||||
|
if message[1] == 0xFF:
|
||||||
|
mid, = sUInt32.unpack_from(message[0:4])
|
||||||
|
|
||||||
|
else:
|
||||||
|
mid, = sUInt16.unpack_from(message[0:2])
|
||||||
|
|
||||||
|
else:
|
||||||
|
mid = message[0]
|
||||||
|
|
||||||
|
msg = self.getMessage(mid)
|
||||||
|
msg.loads(message)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, handle):
|
||||||
|
templates = parseTemplateAbstract(handle.read())
|
||||||
|
return cls.loadAst(templates)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def loadAst(cls, templates):
|
||||||
|
self = cls()
|
||||||
|
if not templates:
|
||||||
|
raise ValueError("Empty template specified!")
|
||||||
|
|
||||||
|
if templates.pop(0) != "version":
|
||||||
|
raise Exception("Expected version as first parameter to message template!")
|
||||||
|
|
||||||
|
tVersion = float(templates.pop(0))
|
||||||
|
|
||||||
|
counters = {"High": 0, "Medium": 0, "Low": 0, "Fixed": 0}
|
||||||
|
|
||||||
|
for template in templates:
|
||||||
|
if type(template) != list:
|
||||||
|
raise Exception("Expected {} in template abstract, got {}!".format(type(list), type(template)))
|
||||||
|
|
||||||
|
mName = template.pop(0)
|
||||||
|
if type(mName) != str:
|
||||||
|
raise Exception("Expected name to be string")
|
||||||
|
|
||||||
|
mFrequency = template.pop(0)
|
||||||
|
if type(mFrequency) != str:
|
||||||
|
raise Exception("Expected frequency to be string")
|
||||||
|
|
||||||
|
if mFrequency not in counters.keys():
|
||||||
|
raise Exception("Unknown frequency type {}".format(mFrequency))
|
||||||
|
|
||||||
|
mID = 0
|
||||||
|
|
||||||
|
# Version 2.0 and onward requires explicit message IDs
|
||||||
|
counters[mFrequency] += 1
|
||||||
|
if mFrequency == "Fixed" or tVersion >= 2.0:
|
||||||
|
mID = template.pop(0)
|
||||||
|
if type(mID) != str:
|
||||||
|
raise Exception("Expected ID to be string")
|
||||||
|
|
||||||
|
if mID.startswith("0x"):
|
||||||
|
mID = int(mID, 16)
|
||||||
|
else:
|
||||||
|
mID = int(mID)
|
||||||
|
|
||||||
|
else:
|
||||||
|
mID = counters[mFrequency]
|
||||||
|
|
||||||
|
mTrust = template.pop(0)
|
||||||
|
if type(mTrust) != str:
|
||||||
|
raise Exception("Expected Trust to be string")
|
||||||
|
|
||||||
|
if mTrust not in ("Trusted", "NotTrusted"):
|
||||||
|
raise Exception("Unknown trust type {}".format(mTrust))
|
||||||
|
|
||||||
|
mEncoding = "Unencoded"
|
||||||
|
|
||||||
|
if tVersion >= 2.0:
|
||||||
|
mEncoding = template.pop(0)
|
||||||
|
if type(mEncoding) != str:
|
||||||
|
raise Exception("Expected Encoding to be string")
|
||||||
|
|
||||||
|
if mEncoding not in ("Unencoded", "Zerocoded"):
|
||||||
|
raise Exception("Unknown encoding type {}".format(mEncoding))
|
||||||
|
|
||||||
|
# --- Start message construction ---
|
||||||
|
|
||||||
|
# Idk, this is how message.cpp does it
|
||||||
|
if mFrequency == "Fixed":
|
||||||
|
mFrequency = Message.FREQUENCY.LOW
|
||||||
|
|
||||||
|
elif mFrequency == "Low":
|
||||||
|
if mID > 0xFFFF:
|
||||||
|
raise Exception("Too many Low frequency messages")
|
||||||
|
|
||||||
|
mFrequency = Message.FREQUENCY.LOW
|
||||||
|
mID = (0xFFFF << 16) | mID
|
||||||
|
|
||||||
|
elif mFrequency == "Medium":
|
||||||
|
if mID > 0xFF:
|
||||||
|
raise Exception("Too many Medium frequency messages")
|
||||||
|
|
||||||
|
mFrequency = Message.FREQUENCY.MEDIUM
|
||||||
|
mID = (0xFF << 8) | mID
|
||||||
|
|
||||||
|
elif mFrequency == "High":
|
||||||
|
mFrequency = Message.FREQUENCY.HIGH
|
||||||
|
if mID > 0xFF:
|
||||||
|
raise Exception("Too many high frequency messages")
|
||||||
|
|
||||||
|
|
||||||
|
if mTrust == "Trusted":
|
||||||
|
mTrust = Message.TRUST.TRUST
|
||||||
|
|
||||||
|
elif mTrust == "NotTrusted":
|
||||||
|
mTrust = Message.TRUST.NOTRUST
|
||||||
|
|
||||||
|
|
||||||
|
if mEncoding == "Unencoded":
|
||||||
|
mEncoding = Message.ENCODING.UNENCODED
|
||||||
|
|
||||||
|
elif mEncoding == "Zerocoded":
|
||||||
|
mEncoding = Message.ENCODING.ZEROCODED
|
||||||
|
|
||||||
|
mDeprecation = Message.DEPRECATION.NOT
|
||||||
|
|
||||||
|
if len(template):
|
||||||
|
if template[0] == "UDPDeprecated":
|
||||||
|
mDeprecation = Message.DEPRECATION.UDPDEPRECATED
|
||||||
|
template.pop(0)
|
||||||
|
elif template[0] == "UDPBlackListed":
|
||||||
|
mDeprecation = Message.DEPRECATION.UDPBLACKLISTED
|
||||||
|
template.pop(0)
|
||||||
|
elif template[0] == "Deprecated":
|
||||||
|
mDeprecation = Message.DEPRECATION.DEPRECATED
|
||||||
|
template.pop(0)
|
||||||
|
|
||||||
|
message = Message(mName, mFrequency, mID, mTrust, mEncoding, mDeprecation)
|
||||||
|
|
||||||
|
# --- End message construction ---
|
||||||
|
|
||||||
|
# All that should remain now is blocks!
|
||||||
|
for block in template:
|
||||||
|
if len(block) < 2:
|
||||||
|
raise Exception("Block must contain at least a name and quantity")
|
||||||
|
|
||||||
|
if type(block) != list:
|
||||||
|
raise Exception("Expected block to be a list")
|
||||||
|
|
||||||
|
bName = block.pop(0)
|
||||||
|
if type(bName) != str:
|
||||||
|
raise Exception("Expected block name to be string")
|
||||||
|
|
||||||
|
bQuantity = block.pop(0)
|
||||||
|
if type(bQuantity) != str:
|
||||||
|
raise Exception("Expected quantity name to be string")
|
||||||
|
|
||||||
|
bCount = None
|
||||||
|
if bQuantity == "Multiple":
|
||||||
|
if len(block) < 1:
|
||||||
|
raise Exception("Multiple quantity specified without count")
|
||||||
|
|
||||||
|
bCount = block.pop(0)
|
||||||
|
if type(bQuantity) != str:
|
||||||
|
raise Exception("Expected block count to be string")
|
||||||
|
|
||||||
|
bCount = int(bCount)
|
||||||
|
|
||||||
|
# --- Start block construction ---
|
||||||
|
|
||||||
|
mBlock = None
|
||||||
|
if bQuantity in ("Multiple", "Variable"):
|
||||||
|
mBlock = BlockArray(bName, bCount)
|
||||||
|
elif bQuantity == "Single":
|
||||||
|
mBlock = Block(bName)
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown quantity {}".format(bQuantity))
|
||||||
|
|
||||||
|
# --- End block construction ---
|
||||||
|
|
||||||
|
# All that should remain now is parameters!
|
||||||
|
for parameter in block:
|
||||||
|
if len(parameter) < 2:
|
||||||
|
raise Exception("Parameter must contain at least a name and type")
|
||||||
|
|
||||||
|
if type(parameter) != list:
|
||||||
|
raise Exception("Expected parameter to be a list")
|
||||||
|
|
||||||
|
pName = parameter.pop(0)
|
||||||
|
if type(pName) != str:
|
||||||
|
raise Exception("Expected parameter name to be string")
|
||||||
|
|
||||||
|
pType = parameter.pop(0)
|
||||||
|
if type(pType) != str:
|
||||||
|
raise Exception("Expected parameter type to be string")
|
||||||
|
|
||||||
|
pSize = None
|
||||||
|
if pType in ("Fixed", "Variable"):
|
||||||
|
if len(parameter) < 1:
|
||||||
|
raise Exception("Parameter specified Fixed or Variable without size")
|
||||||
|
|
||||||
|
pSize = parameter.pop(0)
|
||||||
|
if type(pType) != str:
|
||||||
|
raise Exception("Expected parameter size to be string")
|
||||||
|
pSize = int(pSize)
|
||||||
|
|
||||||
|
pType = mBlock.TYPE[pType.upper()]
|
||||||
|
|
||||||
|
# --- Start parameter construction ---
|
||||||
|
mBlock.registerParameter(pName, pType, pSize)
|
||||||
|
# --- End parameter construction ---
|
||||||
|
|
||||||
|
|
||||||
|
message.registerBlock(mBlock)
|
||||||
|
|
||||||
|
self.registerMessage(message)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def parseTemplateAbstract(text):
|
||||||
|
parsed = []
|
||||||
|
stack = [parsed]
|
||||||
|
strbuf = ""
|
||||||
|
comment = 0
|
||||||
|
for c in text:
|
||||||
|
if c == "/":
|
||||||
|
comment += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif comment >= 2:
|
||||||
|
if c == "\n":
|
||||||
|
comment = 0
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif comment == 1:
|
||||||
|
raise Exception("Unexpected /")
|
||||||
|
|
||||||
|
if c in (" ", "\t", "{", "}", "\n"):
|
||||||
|
if strbuf != "":
|
||||||
|
stack[-1].append(strbuf)
|
||||||
|
strbuf = ""
|
||||||
|
|
||||||
|
if c == "{":
|
||||||
|
stack.append([])
|
||||||
|
|
||||||
|
elif c == "}":
|
||||||
|
tmp = stack.pop()
|
||||||
|
stack[-1].append(tmp)
|
||||||
|
|
||||||
|
elif not c in (" ", "\t", "\n"):
|
||||||
|
strbuf += c
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
__templateCache = None
|
||||||
|
|
||||||
|
def getDefaultTemplate():
|
||||||
|
global __templateCache
|
||||||
|
if not __templateCache:
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
template_path = os.path.join(script_dir, "message_template.msg")
|
||||||
|
with open(template_path, "r") as f:
|
||||||
|
__templateCache = MessageTemplate.load(f)
|
||||||
|
return __templateCache
|
||||||
|
|
||||||
|
def unitTest():
|
||||||
|
with open("message_template.msg", "r") as f:
|
||||||
|
template = MessageTemplate.load(f)
|
||||||
|
msg = template.getMessage("TestMessage")
|
||||||
|
msg.TestBlock1.Test1 = 4
|
||||||
|
|
||||||
|
for i in range(0, 4):
|
||||||
|
msg.NeighborBlock[i].Test0 = i * 3
|
||||||
|
msg.NeighborBlock[i].Test1 = i * 3 + 1
|
||||||
|
msg.NeighborBlock[i].Test2 = i * 3 + 2
|
||||||
|
print(msg)
|
||||||
|
print(ZeroEncode(bytes(msg)))
|
||||||
|
print(ZeroDecode(ZeroEncode(bytes(msg))))
|
||||||
|
print(bytes(msg) == ZeroDecode(ZeroEncode(bytes(msg))))
|
||||||
|
msg2 = template.getMessage("TestMessage")
|
||||||
|
msg2.loads(bytes(msg))
|
||||||
|
print(msg2.TestBlock1.Test1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unitTest()
|
||||||
181
pymetaverse/packet.py
Normal file
181
pymetaverse/packet.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import struct
|
||||||
|
import io
|
||||||
|
|
||||||
|
# https://wiki.secondlife.com/wiki/Packet_Layout
|
||||||
|
"""
|
||||||
|
+-+-+-+-+----+--------+--------+--------+--------+--------+-----...-----+
|
||||||
|
|Z|R|R|A| | | | Extra |
|
||||||
|
|E|E|E|C| | Sequence number (4 bytes) | Extra | Header |
|
||||||
|
|R|L|S|K| | | (byte) | (N bytes) |
|
||||||
|
+-+-+-+-+----+--------+--------+--------+--------+--------+-----...-----+
|
||||||
|
"""
|
||||||
|
|
||||||
|
def zeroEncode(buf):
|
||||||
|
output = io.BytesIO()
|
||||||
|
count = None
|
||||||
|
i = 0
|
||||||
|
l = len(buf)
|
||||||
|
count = 0
|
||||||
|
while i < l:
|
||||||
|
if buf[i] == 0:
|
||||||
|
count = 0
|
||||||
|
while i < l and buf[i] == 0:
|
||||||
|
count += 1
|
||||||
|
i += 1
|
||||||
|
if count == 255:
|
||||||
|
output.write(bytes([0, 0xFF]))
|
||||||
|
count = 0
|
||||||
|
if count != 0:
|
||||||
|
output.write(bytes([0, count]))
|
||||||
|
if i < l:
|
||||||
|
output.write(bytes([buf[i]]))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
def zeroDecode(buf):
|
||||||
|
output = io.BytesIO()
|
||||||
|
count = None
|
||||||
|
i = 0
|
||||||
|
l = len(buf)
|
||||||
|
while i < l:
|
||||||
|
if buf[i] == 0:
|
||||||
|
i += 1
|
||||||
|
output.write(bytes(buf[i]))
|
||||||
|
else:
|
||||||
|
output.write(bytes([buf[i]]))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
class Packet:
|
||||||
|
MTU = 1400
|
||||||
|
|
||||||
|
sPacketHeader = struct.Struct(">BIB")
|
||||||
|
sPacketAcks = struct.Struct(">I")
|
||||||
|
|
||||||
|
class FLAGS:
|
||||||
|
ZEROCODE = 0x80
|
||||||
|
RELIABLE = 0x40
|
||||||
|
RESENT = 0x20
|
||||||
|
ACK = 0x10
|
||||||
|
|
||||||
|
def __init__(self, seq, body = None, flags = None, acks = None, extra = None):
|
||||||
|
self.sequence = seq
|
||||||
|
self.flags = flags or 0
|
||||||
|
self.extra = extra or b""
|
||||||
|
self.body = body or b""
|
||||||
|
self.acks = acks or []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reliable(self):
|
||||||
|
return bool(self.flags & self.FLAGS.RELIABLE)
|
||||||
|
|
||||||
|
@reliable.setter
|
||||||
|
def reliable(self, value):
|
||||||
|
if value:
|
||||||
|
self.flags |= self.FLAGS.RELIABLE
|
||||||
|
else:
|
||||||
|
self.flags &= ~self.FLAGS.RELIABLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resent(self):
|
||||||
|
return bool(self.flags & self.FLAGS.RESENT)
|
||||||
|
|
||||||
|
@resent.setter
|
||||||
|
def resent(self, value):
|
||||||
|
if value:
|
||||||
|
self.flags |= self.FLAGS.RESENT
|
||||||
|
else:
|
||||||
|
self.flags &= ~self.FLAGS.RESENT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def zerocode(self):
|
||||||
|
return bool(self.flags & self.FLAGS.ZEROCODE)
|
||||||
|
|
||||||
|
@zerocode.setter
|
||||||
|
def zerocode(self, value):
|
||||||
|
if value:
|
||||||
|
self.flags |= self.FLAGS.ZEROCODE
|
||||||
|
else:
|
||||||
|
self.flags &= ~self.FLAGS.ZEROCODE
|
||||||
|
|
||||||
|
def toBytes(self):
|
||||||
|
output = io.BytesIO()
|
||||||
|
flags = self.flags
|
||||||
|
if len(self.acks) > 0:
|
||||||
|
flags = flags | self.FLAGS.ACK
|
||||||
|
|
||||||
|
output.write(self.sPacketHeader.pack(flags, self.sequence, len(self.extra)))
|
||||||
|
output.write(self.extra)
|
||||||
|
|
||||||
|
if flags & self.FLAGS.ZEROCODE:
|
||||||
|
output.write(zeroEncode(self.body))
|
||||||
|
else:
|
||||||
|
output.write(self.body)
|
||||||
|
|
||||||
|
if flags & self.FLAGS.ACK:
|
||||||
|
count = 0
|
||||||
|
while len(self.acks):
|
||||||
|
size = output.tell()
|
||||||
|
if size - 5 >= self.MTU:
|
||||||
|
break
|
||||||
|
|
||||||
|
if count >= 255:
|
||||||
|
break
|
||||||
|
|
||||||
|
output.write(self.sPacketAcks.pack(self.acks.pop(0)))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
output.write(bytes([count]))
|
||||||
|
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return self.toByte()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromBytes(cls, data):
|
||||||
|
return cls.fromStream(io.BytesIO(data))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromStream(cls, f):
|
||||||
|
flags, seq, extra = cls.sPacketHeader.unpack_from(f.read(cls.sPacketHeader.size))
|
||||||
|
|
||||||
|
if extra > 0:
|
||||||
|
extra = f.read(extra)
|
||||||
|
else:
|
||||||
|
extra = b""
|
||||||
|
|
||||||
|
bodyStart = f.tell()
|
||||||
|
|
||||||
|
body = None
|
||||||
|
acks = []
|
||||||
|
if flags & cls.FLAGS.ACK:
|
||||||
|
f.seek(-1, io.SEEK_END)
|
||||||
|
ackCount, = f.read(1)
|
||||||
|
|
||||||
|
f.seek(-(ackCount * cls.sPacketAcks.size) - 1, io.SEEK_END)
|
||||||
|
|
||||||
|
bodyEnd = f.tell()
|
||||||
|
|
||||||
|
acks = [None] * ackCount
|
||||||
|
for i in range(ackCount):
|
||||||
|
acks[i], = cls.sPacketAcks.unpack(f.read(cls.sPacketAcks.size))
|
||||||
|
|
||||||
|
f.seek(bodyStart)
|
||||||
|
body = f.read(bodyEnd - bodyStart)
|
||||||
|
|
||||||
|
else:
|
||||||
|
body = f.read()
|
||||||
|
|
||||||
|
if flags & cls.FLAGS.ZEROCODE:
|
||||||
|
body = zeroDecode(body)
|
||||||
|
|
||||||
|
return cls(seq, body, flags = flags, acks = acks, extra = extra)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromBytes(cls, data):
|
||||||
|
return cls.fromStream(io.BytesIO(data))
|
||||||
|
|
||||||
0
pymetaverse/region.py
Normal file
0
pymetaverse/region.py
Normal file
62
pymetaverse/simulator.py
Normal file
62
pymetaverse/simulator.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from .circuit import Circuit
|
||||||
|
from . import messages
|
||||||
|
from . import httpclient
|
||||||
|
from . import llsd
|
||||||
|
from .eventtarget import EventTarget
|
||||||
|
|
||||||
|
class Simulator(EventTarget):
|
||||||
|
def __init__(self, agent):
|
||||||
|
super().__init__()
|
||||||
|
self.agent = agent
|
||||||
|
self.host = None
|
||||||
|
self.circuit = None
|
||||||
|
self.region = None
|
||||||
|
self.caps = {}
|
||||||
|
self.messageTemplate = messages.getDefaultTemplate()
|
||||||
|
|
||||||
|
async def connect(self, host, circuitCode):
|
||||||
|
self.host = host
|
||||||
|
self.circuit = await Circuit.create(host)
|
||||||
|
self.circuit.on("message", self.handleMessage)
|
||||||
|
msg = self.messageTemplate.getMessage("UseCircuitCode")
|
||||||
|
msg.CircuitCode.Code = circuitCode
|
||||||
|
msg.CircuitCode.SessionID = self.agent.sessionId
|
||||||
|
msg.CircuitCode.ID = self.agent.agentId
|
||||||
|
self.circuit.send(msg, True)
|
||||||
|
|
||||||
|
def send(self, msg, reliable = False):
|
||||||
|
self.circuit.send(msg, reliable)
|
||||||
|
|
||||||
|
def handleSystemMessages(self, msg):
|
||||||
|
if msg.name == "PacketAck":
|
||||||
|
acks = []
|
||||||
|
for ack in msg.Packets:
|
||||||
|
acks.append(ack.ID)
|
||||||
|
|
||||||
|
self.circuit.acknowledge(acks)
|
||||||
|
|
||||||
|
elif msg.name == "StartPingCheck":
|
||||||
|
msg = self.messageTemplate.getMessage("CompletePingCheck")
|
||||||
|
msg.PingID.PingID = msg.PingID.PingID
|
||||||
|
self.send(msg)
|
||||||
|
|
||||||
|
elif msg.name == "RegionHandshake":
|
||||||
|
msg = self.messageTemplate.getMessage("RegionHandshakeReply")
|
||||||
|
msg.AgentData.AgentID = self.agent.agentId
|
||||||
|
msg.AgentData.SessionID = self.agent.sessionId
|
||||||
|
msg.RegionInfo.Flags = 2
|
||||||
|
self.send(msg, True)
|
||||||
|
|
||||||
|
def handleMessage(self, addr, body):
|
||||||
|
# Reject unknown hosts as a security precaution
|
||||||
|
if addr != self.host:
|
||||||
|
print("REJECT")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = self.messageTemplate.loadMessage(body)
|
||||||
|
self.handleSystemMessages(msg)
|
||||||
|
self.fire("message", msg)
|
||||||
|
|
||||||
|
async def fetchCapabilities(self, seed):
|
||||||
|
pass
|
||||||
|
|
||||||
Reference in New Issue
Block a user