Initial commit

This commit is contained in:
Kyler Eastridge
2025-06-18 00:54:18 -04:00
commit dd67e5096c
15 changed files with 11158 additions and 0 deletions

201
.gitignore vendored Normal file
View 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
View 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
View File

45
pymetaverse/agent.py Normal file
View 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
View 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

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
1a9a3717fde5d0fb3d5f688a1a3dab7fcc2aa308

732
pymetaverse/messages.py Normal file
View 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
View 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
View File

62
pymetaverse/simulator.py Normal file
View 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