Rework a bit of things

This commit is contained in:
Kyler Eastridge
2025-06-19 07:13:02 -04:00
parent 696c0bc649
commit e5a5e0aff7
7 changed files with 306 additions and 48 deletions

131
pymetaverse/bot.py Normal file
View File

@@ -0,0 +1,131 @@
import asyncio
import struct
import math
import random
from .eventtarget import EventTarget
from . import login
from . import viewer as Viewer
from .const import *
class SimpleBot(EventTarget):
def __init__(self):
super().__init__()
self.agent = Viewer.Agent()
self.agent.on("message", self.handleMessage)
async def handleSystemMessages(self, simulator, message):
# We only really care about the parent simulator here
if simulator == self.simulator:
pass
if message.name == "RegionHandshake":
# Send some stuff to make the simulator happy about our presence
msg = self.messageTemplate.getMessage("AgentThrottle")
msg.AgentData.AgentID = self.agent.agentId
msg.AgentData.SessionID = self.agent.sessionId
msg.AgentData.CircuitCode = self.agent.circuitCode
msg.Throttle.GenCounter = 0
msg.Throttle.Throttles = struct.pack("<7f",
#http://wiki.secondlife.com/wiki/AgentThrottle
150000, #Resend
170000, #Land
34000, #Wind
34000, #Cloud
446000, #Task
446000, #Texture
220000 #Asset
)
self.send(msg)
msg = self.messageTemplate.getMessage("AgentFOV")
msg.AgentData.AgentID = self.agent.agentId
msg.AgentData.SessionID = self.agent.sessionId
msg.AgentData.CircuitCode = self.agent.circuitCode
msg.FOVBlock.GenCounter = 0
msg.FOVBlock.VerticalAngle = 6.233185307179586
self.send(msg)
msg = self.messageTemplate.getMessage("AgentHeightWidth")
msg.AgentData.AgentID = self.agent.agentId
msg.AgentData.SessionID = self.agent.sessionId
msg.AgentData.CircuitCode = self.agent.circuitCode
msg.HeightWidthBlock.GenCounter = 0
msg.HeightWidthBlock.Height = 0xffff
msg.HeightWidthBlock.Width = 0xffff
self.send(msg)
@property
def simulator(self):
return self.agent.simulator
@property
def messageTemplate(self):
return self.agent.messageTemplate
async def handleMessage(self, simulator, message):
self.agentUpdate()
await self.handleSystemMessages(simulator, message)
await self.fire("message", simulator, message, name=message.name)
def send(self, message, reliable = False):
self.agent.send(message, reliable)
async def login(self, username, password):
loginHandle = await login.Login(username, password, isBot = True)
if loginHandle["login"] == False:
raise ValueError("Incorrect username or password")
await self.agent.login(loginHandle)
async def run(self):
await self.agent.run()
def logout(self):
self.agent.logout()
def say(self, channel, message, ctype = CHAT_TYPE_NORMAL):
if channel >= 0:
msg = self.messageTemplate.getMessage("ChatFromViewer")
msg.AgentData.AgentID = self.agent.agentId
msg.AgentData.SessionID = self.agent.sessionId
msg.ChatData.Message = message.encode() + b"\0"
msg.ChatData.Type = ctype
msg.ChatData.Channel = channel
self.send(msg)
else:
# Because ChatFromViewer.Message.Channel is a signed int, we have
# to use dialogs here
msg = self.messageTemplate.getMessage("ScriptDialogReply")
msg.AgentData.AgentID = self.agent.agentId
msg.AgentData.SessionID = self.agent.sessionId
msg.Data.ObjectID = self.agent.agentId
msg.Data.ChatChannel = channel
msg.Data.ButtonIndex = 0
msg.Data.ButtonLabel = message.encode() + b"\0"
self.send(msg)
def agentUpdate(self, controls = 0, forward = 0, state = 0, flags = 0):
angle_rad = math.radians(forward)
half_angle = angle_rad / 2
sin_half = math.sin(half_angle)
cos_half = math.cos(half_angle)
msg = self.messageTemplate.getMessage("AgentUpdate")
msg.AgentData.AgentID = self.agent.agentId
msg.AgentData.SessionID = self.agent.sessionId
msg.AgentData.BodyRotation = (0.0, 0.0, sin_half, cos_half)
msg.AgentData.HeadRotation = (0.0, 0.0, sin_half, cos_half)
msg.AgentData.State = state
msg.AgentData.CameraCenter = (0, 0, 0)
msg.AgentData.CameraAtAxis = (0, 0.999999, 0)
msg.AgentData.CameraLeftAxis = (0.999999, 0, 0)
msg.AgentData.CameraUpAxis = (0, 0, 0.999999)
msg.AgentData.Far = math.inf
msg.AgentData.ControlFlags = controls
msg.AgentData.Flags = flags
self.send(msg)

54
pymetaverse/const.py Normal file
View File

@@ -0,0 +1,54 @@
# llui/llchat.h
CHAT_TYPE_WHISPER = 0
CHAT_TYPE_NORMAL = 1
CHAT_TYPE_SHOUT = 2
CHAT_TYPE_START = 4
CHAT_TYPE_STOP = 5
CHAT_TYPE_DEBUG_MSG = 6
CHAT_TYPE_REGION = 7
CHAT_TYPE_OWNER = 8
CHAT_TYPE_DIRECT = 9
# newview/llviewermessage.cpp
AU_FLAGS_NONE = 0x00
AU_FLAGS_HIDETITLE = 0x01
AU_FLAGS_CLIENT_AUTOPILOT = 0x02
# llcommon/indra_constants.h
AGENT_CONTROL_AT_POS = 0x00000001
AGENT_CONTROL_AT_NEG = 0x00000002
AGENT_CONTROL_LEFT_POS = 0x00000004
AGENT_CONTROL_LEFT_NEG = 0x00000008
AGENT_CONTROL_UP_POS = 0x00000010
AGENT_CONTROL_UP_NEG = 0x00000020
AGENT_CONTROL_PITCH_POS = 0x00000040
AGENT_CONTROL_PITCH_NEG = 0x00000080
AGENT_CONTROL_YAW_POS = 0x00000100
AGENT_CONTROL_YAW_NEG = 0x00000200
AGENT_CONTROL_FAST_AT = 0x00000400
AGENT_CONTROL_FAST_LEFT = 0x00000800
AGENT_CONTROL_FAST_UP = 0x00001000
AGENT_CONTROL_FLY = 0x00002000
AGENT_CONTROL_STOP = 0x00004000
AGENT_CONTROL_FINISH_ANIM = 0x00008000
AGENT_CONTROL_STAND_UP = 0x00010000
AGENT_CONTROL_SIT_ON_GROUND = 0x00020000
AGENT_CONTROL_MOUSELOOK = 0x00040000
AGENT_CONTROL_NUDGE_AT_POS = 0x00080000
AGENT_CONTROL_NUDGE_AT_NEG = 0x00100000
AGENT_CONTROL_NUDGE_LEFT_POS = 0x00200000
AGENT_CONTROL_NUDGE_LEFT_NEG = 0x00400000
AGENT_CONTROL_NUDGE_UP_POS = 0x00800000
AGENT_CONTROL_NUDGE_UP_NEG = 0x01000000
AGENT_CONTROL_TURN_LEFT = 0x02000000
AGENT_CONTROL_TURN_RIGHT = 0x04000000
AGENT_CONTROL_AWAY = 0x08000000
AGENT_CONTROL_LBUTTON_DOWN = 0x10000000
AGENT_CONTROL_LBUTTON_UP = 0x20000000
AGENT_CONTROL_ML_LBUTTON_DOWN = 0x40000000
AGENT_CONTROL_ML_LBUTTON_UP = 0x80000000

View File

@@ -1,24 +1,37 @@
import asyncio
import inspect
class EventTarget:
def __init__(self):
self._listeners = {}
def on(self, event, func = None):
if not event in self._listeners:
self._listeners = {} # event -> list of (func, filters)
def on(self, event, func=None, **filters):
if event not in self._listeners:
self._listeners[event] = []
def _(func):
self._listeners[event].append(func)
return func
if func:
return _(func)
return _
def decorator(f):
self._listeners[event].append((f, filters))
return f
return decorator(func) if func else decorator
def off(self, event, func):
if event in self._listeners:
self._listeners[event].remove(func)
self._listeners[event] = [
(f, filt) for (f, filt) in self._listeners[event] if f != func
]
async def fire(self, event, *args, **kwargs):
listeners = self._listeners.get(event, [])
for func, filters in listeners:
if all(kwargs.get(k) == v for k, v in filters.items()):
if inspect.iscoroutinefunction(func):
await func(*args)
else:
func(*args)
def fire(self, event, *args, **kwargs):
if event in self._listeners:
for func in self._listeners[event]:
func(*args, **kwargs)
def fireSync(self, event, *args, **kwargs):
listeners = self._listeners.get(event, [])
for func, filters in listeners:
if all(kwargs.get(k) == v for k, v in filters.items()):
func(*args)

View File

@@ -30,13 +30,26 @@ class Agent(EventTarget):
if self.simulator:
self.simulator.send(msg, reliable)
def handleMessage(self, sim, msg):
self.fire("message", sim, msg)
@classmethod
async def fromLogin(cls, login):
self = cls()
async def handleSystemMessages(self, sim, msg):
if msg.name == "DisableSimulator":
if sim == self.simulator:
self.simulator = None
if sim in self.simulators:
self.simulators.remove(sim)
elif msg.name == "LogoutReply" or msg.name == "KickUser":
for simulator in self.simulators:
self.simulators.remove(simulator)
self.simulator = None
await self.fire("logout")
async def handleMessage(self, sim, msg):
await self.handleSystemMessages(sim, msg)
await self.fire("message", sim, msg)
async def login(self, login):
if login["login"] == False:
raise ValueError("Invalid login handle")
@@ -52,12 +65,21 @@ class Agent(EventTarget):
msg.AgentData.SessionID = self.sessionId
msg.AgentData.CircuitCode = self.circuitCode
self.send(msg, True)
return self
def logout(self):
msg = self.messageTemplate.getMessage("LogoutRequest")
msg.AgentData.AgentID = self.agentId
msg.AgentData.SessionID = self.sessionId
self.send(msg, True)
async def run(self):
while True:
await asyncio.sleep(5)
if self.simulator == None:
break
try:
await asyncio.sleep(0.1)
if self.simulator == None:
break
except asyncio.exceptions.CancelledError:
# Attempt to gracefully logout
self.logout()

View File

@@ -32,15 +32,30 @@ class Circuit(asyncio.Protocol, EventTarget):
if pkt.flags & pkt.FLAGS.ACK:
self.acknowledge(pkt.acks)
self.fire("message", addr, pkt.body)
asyncio.create_task(self.fire("message", addr, pkt.body))
def error_received(self, exc):
self.fire("error", exc)
asyncio.create_task(self.fire("error", exc))
def connection_lost(self, exc):
self.fire("close", exc)
if not self.transport:
return
self.transport = None
asyncio.create_task(self.fire("close", exc))
def close(self):
if not self.transport:
return
self.transport.close()
self.transport = None
asyncio.create_task(self.fire("close", None))
def send(self, message, reliable = False):
if not self.transport:
return
pkt = packet.Packet(self.nextSequence(), bytes(message), acks=self.acks)
if reliable:
pkt.reliable = True
@@ -49,6 +64,9 @@ class Circuit(asyncio.Protocol, EventTarget):
self.transport.sendto(pkt.toBytes())
def resend(self, distance = 100):
if not self.transport:
return
cutoff = self.sequence - distance
for pkt in self.unackd.values():
if pkt.sequence < cutoff:

View File

@@ -1,5 +1,6 @@
from .circuit import Circuit
from . import messages
from . import region
from .. import httpclient
from .. import llsd
from ..eventtarget import EventTarget
@@ -32,7 +33,7 @@ class Simulator(EventTarget):
msg.CircuitCode.ID = self.agent.agentId
self.send(msg, True)
def handleSystemMessages(self, msg):
async def handleSystemMessages(self, msg):
if msg.name == "PacketAck":
acks = []
for ack in msg.Packets:
@@ -53,20 +54,23 @@ class Simulator(EventTarget):
msg = self.messageTemplate.getMessage("RegionHandshakeReply")
msg.AgentData.AgentID = self.agent.agentId
msg.AgentData.SessionID = self.agent.sessionId
msg.RegionInfo.Flags = 2
msg.RegionInfo.Flags = 1
self.send(msg, True)
elif msg.name == "DisableSimulator":
self.circuit.close()
def handleMessage(self, addr, body):
async def handleMessage(self, addr, body):
# Reject unknown hosts as a security precaution
if addr != self.host:
return
msg = self.messageTemplate.loadMessage(body)
self.handleSystemMessages(msg)
await self.handleSystemMessages(msg)
# Don't break the whole script!
try:
self.fire("message", self, msg)
await self.fire("message", self, msg, name=msg.name)
except Exception as e:
traceback.print_exc()

View File

@@ -1,20 +1,36 @@
# Second Life viewer in python
```py
import asyncio
import json
import datetime
from pymetaverse import login
from pymetaverse.agent import Agent
from pymetaverse.bot import SimpleBot
from pymetaverse.const import *
bot = SimpleBot()
@bot.on("message", name="ChatFromSimulator")
def ChatFromSimulator(simulator, message):
# Ignore start / stop
if message.ChatData.ChatType in (4, 5):
return
sender = message.ChatData.FromName.rstrip(b"\0").decode()
text = message.ChatData.Message.rstrip(b"\0").decode()
if text == "logout":
bot.say(0, "Ok!")
bot.logout()
print("[{}] {}: {}".format(
datetime.datetime.now().strftime("%Y-%M-%d %H:%m:%S"),
sender,
text
))
async def main():
# Set argument isBot = False if human
loginHandle = await login.Login(username=("firstname", "lastname"), password="A secret to everyone")
agent = await Agent.fromLogin(loginHandle)
@agent.on("message")
def handleMessage(simulator, message):
print(simulator, message)
await agent.run()
await bot.login(("Magellan", "Linden"), "CrystalPrims")
await bot.run()
# Run everything
asyncio.run(main())
```