Rework a bit of things
This commit is contained in:
131
pymetaverse/bot.py
Normal file
131
pymetaverse/bot.py
Normal 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
54
pymetaverse/const.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
38
readme.md
38
readme.md
@@ -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())
|
||||
```
|
||||
Reference in New Issue
Block a user