Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
220a02543e | ||
|
|
8ac47c2397 | ||
|
|
d384978322 | ||
|
|
f02a479834 | ||
|
|
b5e8b36173 | ||
|
|
08a39f4df7 | ||
|
|
61ec51beec | ||
|
|
9adbdcdcc8 | ||
|
|
e7b05f72ca | ||
|
|
75f2f363a4 | ||
|
|
cc1bb9ac1d |
@@ -340,6 +340,15 @@ It can be launched at any time by typing `/524 spawn_repl` in chat.
|
||||
|
||||

|
||||
|
||||
The REPL is fully async aware and allows awaiting events without blocking:
|
||||
|
||||
```python
|
||||
>>> from hippolyzer.lib.client.object_manager import ObjectUpdateType
|
||||
>>> evt = await session.objects.events.wait_for((ObjectUpdateType.OBJECT_UPDATE,), timeout=2.0)
|
||||
>>> evt.updated
|
||||
{'Position'}
|
||||
```
|
||||
|
||||
## Potential Changes
|
||||
|
||||
* AISv3 wrapper?
|
||||
|
||||
158
addon_examples/demo_autoattacher.py
Normal file
158
addon_examples/demo_autoattacher.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
Detect receipt of a marketplace order for a demo, and auto-attach the most appropriate object
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from typing import List, Tuple, Dict, Optional, Sequence
|
||||
|
||||
from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.base.message.message import Message, Block
|
||||
from hippolyzer.lib.base.templates import InventoryType, Permissions, FolderType
|
||||
from hippolyzer.lib.proxy.addon_utils import BaseAddon, show_message
|
||||
from hippolyzer.lib.proxy.region import ProxiedRegion
|
||||
from hippolyzer.lib.proxy.sessions import Session
|
||||
|
||||
|
||||
MARKETPLACE_TRANSACTION_ID = UUID('ffffffff-ffff-ffff-ffff-ffffffffffff')
|
||||
|
||||
|
||||
class DemoAutoAttacher(BaseAddon):
|
||||
def handle_eq_event(self, session: Session, region: ProxiedRegion, event: dict):
|
||||
if event["message"] != "BulkUpdateInventory":
|
||||
return
|
||||
# Check that this update even possibly came from the marketplace
|
||||
if event["body"]["AgentData"][0]["TransactionID"] != MARKETPLACE_TRANSACTION_ID:
|
||||
return
|
||||
# Make sure that the transaction targeted our real received items folder
|
||||
folders = event["body"]["FolderData"]
|
||||
received_folder = folders[0]
|
||||
if received_folder["Name"] != "Received Items":
|
||||
return
|
||||
skel = session.login_data['inventory-skeleton']
|
||||
actual_received = [x for x in skel if x['type_default'] == FolderType.INBOX]
|
||||
assert actual_received
|
||||
if UUID(actual_received[0]['folder_id']) != received_folder["FolderID"]:
|
||||
show_message(f"Strange received folder ID spoofing? {folders!r}")
|
||||
return
|
||||
|
||||
if not re.match(r".*\bdemo\b.*", folders[1]["Name"], flags=re.I):
|
||||
return
|
||||
# Alright, so we have a demo... thing from the marketplace. What now?
|
||||
items = event["body"]["ItemData"]
|
||||
object_items = [x for x in items if x["InvType"] == InventoryType.OBJECT]
|
||||
if not object_items:
|
||||
return
|
||||
self._schedule_task(self._attach_best_object(session, region, object_items))
|
||||
|
||||
async def _attach_best_object(self, session: Session, region: ProxiedRegion, object_items: List[Dict]):
|
||||
own_body_type = await self._guess_own_body(session, region)
|
||||
show_message(f"Trying to find demo for {own_body_type}")
|
||||
guess_patterns = self.BODY_CLOTHING_PATTERNS.get(own_body_type)
|
||||
to_attach = []
|
||||
if own_body_type and guess_patterns:
|
||||
matching_items = self._get_matching_items(object_items, guess_patterns)
|
||||
if matching_items:
|
||||
# Only take the first one
|
||||
to_attach.append(matching_items[0])
|
||||
if not to_attach:
|
||||
# Don't know what body's being used or couldn't figure out what item
|
||||
# would work best with our body. Just attach the first object in the folder.
|
||||
to_attach.append(object_items[0])
|
||||
|
||||
# Also attach whatever HUDs, maybe we need them.
|
||||
for hud in self._get_matching_items(object_items, ("hud",)):
|
||||
if hud not in to_attach:
|
||||
to_attach.append(hud)
|
||||
|
||||
region.circuit.send(Message(
|
||||
'RezMultipleAttachmentsFromInv',
|
||||
Block('AgentData', AgentID=session.agent_id, SessionID=session.id),
|
||||
Block('HeaderData', CompoundMsgID=UUID.random(), TotalObjects=len(to_attach), FirstDetachAll=0),
|
||||
*[Block(
|
||||
'ObjectData',
|
||||
ItemID=o["ItemID"],
|
||||
OwnerID=session.agent_id,
|
||||
# 128 = "add", uses whatever attachmentpt was defined on the object
|
||||
AttachmentPt=128,
|
||||
ItemFlags_=(),
|
||||
GroupMask_=(),
|
||||
EveryoneMask_=(),
|
||||
NextOwnerMask_=(Permissions.COPY | Permissions.MOVE),
|
||||
Name=o["Name"],
|
||||
Description=o["Description"],
|
||||
) for o in to_attach]
|
||||
))
|
||||
|
||||
def _get_matching_items(self, items: List[dict], patterns: Sequence[str]):
|
||||
# Loop over patterns to search for our body type, in order of preference
|
||||
matched = []
|
||||
for guess_pattern in patterns:
|
||||
# Check each item for that pattern
|
||||
for item in items:
|
||||
if re.match(rf".*\b{guess_pattern}\b.*", item["Name"], re.I):
|
||||
matched.append(item)
|
||||
return matched
|
||||
|
||||
# We scan the agent's attached objects to guess what kind of body they use
|
||||
BODY_PREFIXES = {
|
||||
"-Belleza- Jake ": "jake",
|
||||
"-Belleza- Freya ": "freya",
|
||||
"-Belleza- Isis ": "isis",
|
||||
"-Belleza- Venus ": "venus",
|
||||
"[Signature] Gianni Body": "gianni",
|
||||
"[Signature] Geralt Body": "geralt",
|
||||
"Maitreya Mesh Body - Lara": "maitreya",
|
||||
"Slink Physique Hourglass Petite": "hg_petite",
|
||||
"Slink Physique Mesh Body Hourglass": "hourglass",
|
||||
"Slink Physique Original Petite": "phys_petite",
|
||||
"Slink Physique Mesh Body Original": "physique",
|
||||
"[BODY] Legacy (f)": "legacy_f",
|
||||
"[BODY] Legacy (m)": "legacy_m",
|
||||
"[Signature] Alice Body": "sig_alice",
|
||||
"Slink Physique MALE Mesh Body": "slink_male",
|
||||
"AESTHETIC - [Mesh Body]": "aesthetic",
|
||||
}
|
||||
|
||||
# Different bodies' clothes have different naming conventions according to different merchants.
|
||||
# These are common naming patterns we use to choose objects to attach, in order of preference.
|
||||
BODY_CLOTHING_PATTERNS: Dict[str, Tuple[str, ...]] = {
|
||||
"jake": ("jake", "belleza"),
|
||||
"freya": ("freya", "belleza"),
|
||||
"isis": ("isis", "belleza"),
|
||||
"venus": ("venus", "belleza"),
|
||||
"gianni": ("gianni", "signature", "sig"),
|
||||
"geralt": ("geralt", "signature", "sig"),
|
||||
"hg_petite": ("hourglass petite", "hg petite", "hourglass", "hg", "slink"),
|
||||
"hourglass": ("hourglass", "hg", "slink"),
|
||||
"phys_petite": ("physique petite", "phys petite", "physique", "phys", "slink"),
|
||||
"physique": ("physique", "phys", "slink"),
|
||||
"legacy_f": ("legacy",),
|
||||
"legacy_m": ("legacy",),
|
||||
"sig_alice": ("alice", "signature"),
|
||||
"slink_male": ("physique", "slink"),
|
||||
"aesthetic": ("aesthetic",),
|
||||
}
|
||||
|
||||
async def _guess_own_body(self, session: Session, region: ProxiedRegion) -> Optional[str]:
|
||||
agent_obj = region.objects.lookup_fullid(session.agent_id)
|
||||
if not agent_obj:
|
||||
return None
|
||||
# We probably won't know the names for all of our attachments, request them.
|
||||
# Could be obviated by looking at the COF, not worth it for this.
|
||||
try:
|
||||
await asyncio.wait(region.objects.request_object_properties(agent_obj.Children), timeout=0.5)
|
||||
except asyncio.TimeoutError:
|
||||
# We expect that we just won't ever receive some property requests, that's fine
|
||||
pass
|
||||
|
||||
for prefix, body_type in self.BODY_PREFIXES.items():
|
||||
for obj in agent_obj.Children:
|
||||
if not obj.Name:
|
||||
continue
|
||||
if obj.Name.startswith(prefix):
|
||||
return body_type
|
||||
return None
|
||||
|
||||
|
||||
addons = [DemoAutoAttacher()]
|
||||
100
addon_examples/get_task_inventory_cap.py
Normal file
100
addon_examples/get_task_inventory_cap.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Loading task inventory doesn't actually need to be slow.
|
||||
|
||||
By using a cap instead of the slow xfer path and sending the LLSD inventory
|
||||
model we get 15x speedups even when mocking things behind the scenes by using
|
||||
a hacked up version of xfer. See turbo_object_inventory.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import asgiref.wsgi
|
||||
from typing import *
|
||||
|
||||
from flask import Flask, Response, request
|
||||
|
||||
from hippolyzer.lib.base import llsd
|
||||
from hippolyzer.lib.base.datatypes import UUID
|
||||
from hippolyzer.lib.base.inventory import InventoryModel
|
||||
from hippolyzer.lib.base.message.message import Message, Block
|
||||
from hippolyzer.lib.base.templates import XferFilePath
|
||||
from hippolyzer.lib.proxy import addon_ctx
|
||||
from hippolyzer.lib.proxy.webapp_cap_addon import WebAppCapAddon
|
||||
|
||||
app = Flask("GetTaskInventoryCapApp")
|
||||
|
||||
|
||||
@app.route('/', methods=["POST"])
|
||||
async def get_task_inventory():
|
||||
# Should always have the current region, the cap handler is bound to one.
|
||||
# Just need to pull it from the `addon_ctx` module's global.
|
||||
region = addon_ctx.region.get()
|
||||
session = addon_ctx.session.get()
|
||||
obj_id = UUID(request.args["task_id"])
|
||||
obj = region.objects.lookup_fullid(obj_id)
|
||||
if not obj:
|
||||
return Response(f"Couldn't find {obj_id}", status=404, mimetype="text/plain")
|
||||
request_msg = Message(
|
||||
'RequestTaskInventory',
|
||||
Block('AgentData', AgentID=session.agent_id, SessionID=session.id),
|
||||
Block('InventoryData', LocalID=obj.LocalID),
|
||||
)
|
||||
# Keep around a dict of chunks we saw previously in case we have to restart
|
||||
# an Xfer due to missing chunks. We don't expect chunks to change across Xfers
|
||||
# so this can be used to recover from dropped SendXferPackets in subsequent attempts
|
||||
existing_chunks: Dict[int, bytes] = {}
|
||||
for _ in range(3):
|
||||
# Any previous requests will have triggered a delete of the inventory file
|
||||
# by marking it complete on the server-side. Re-send our RequestTaskInventory
|
||||
# To make sure there's a fresh copy.
|
||||
region.circuit.send(request_msg.take())
|
||||
inv_message = await region.message_handler.wait_for(
|
||||
('ReplyTaskInventory',),
|
||||
predicate=lambda x: x["InventoryData"]["TaskID"] == obj.FullID,
|
||||
timeout=5.0,
|
||||
)
|
||||
# No task inventory, send the reply as-is
|
||||
file_name = inv_message["InventoryData"]["Filename"]
|
||||
if not file_name:
|
||||
return Response("", status=204)
|
||||
|
||||
if inv_message["InventoryData"]["Serial"] == int(request.args.get("last_serial", None)):
|
||||
# Nothing has changed since the version of the inventory they say they have, say so.
|
||||
return Response("", status=304)
|
||||
|
||||
xfer = region.xfer_manager.request(
|
||||
file_name=file_name,
|
||||
file_path=XferFilePath.CACHE,
|
||||
turbo=True,
|
||||
)
|
||||
xfer.chunks.update(existing_chunks)
|
||||
try:
|
||||
await xfer
|
||||
except asyncio.TimeoutError:
|
||||
# We likely failed the request due to missing chunks, store
|
||||
# the chunks that we _did_ get for the next attempt.
|
||||
existing_chunks.update(xfer.chunks)
|
||||
continue
|
||||
|
||||
inv_model = InventoryModel.from_str(xfer.reassemble_chunks().decode("utf8"))
|
||||
|
||||
return Response(
|
||||
llsd.format_notation({
|
||||
"inventory": inv_model.to_llsd(),
|
||||
"inv_serial": inv_message["InventoryData"]["Serial"],
|
||||
}),
|
||||
headers={"Content-Type": "application/llsd+notation"},
|
||||
)
|
||||
raise asyncio.TimeoutError("Failed to get inventory after 3 tries")
|
||||
|
||||
|
||||
class GetTaskInventoryCapExampleAddon(WebAppCapAddon):
|
||||
# A cap URL with this name will be tied to each region when
|
||||
# the sim is first connected to. The URL will be returned to the
|
||||
# viewer in the Seed if the viewer requests it by name.
|
||||
CAP_NAME = "GetTaskInventoryExample"
|
||||
# Any asgi app should be fine.
|
||||
APP = asgiref.wsgi.WsgiToAsgi(app)
|
||||
|
||||
|
||||
addons = [GetTaskInventoryCapExampleAddon()]
|
||||
@@ -14,8 +14,9 @@ from PySide6.QtGui import QImage
|
||||
from hippolyzer.lib.base.datatypes import UUID, Vector3, Quaternion
|
||||
from hippolyzer.lib.base.helpers import to_chunks
|
||||
from hippolyzer.lib.base.message.message import Block, Message
|
||||
from hippolyzer.lib.base.templates import ObjectUpdateFlags, PCode, MCode, MultipleObjectUpdateFlags, TextureEntryCollection
|
||||
from hippolyzer.lib.client.object_manager import ObjectEvent, UpdateType
|
||||
from hippolyzer.lib.base.templates import ObjectUpdateFlags, PCode, MCode, MultipleObjectUpdateFlags, \
|
||||
TextureEntryCollection, JUST_CREATED_FLAGS
|
||||
from hippolyzer.lib.client.object_manager import ObjectEvent, ObjectUpdateType
|
||||
from hippolyzer.lib.proxy.addon_utils import BaseAddon
|
||||
from hippolyzer.lib.proxy.addons import AddonManager
|
||||
from hippolyzer.lib.proxy.commands import handle_command
|
||||
@@ -24,7 +25,6 @@ from hippolyzer.lib.proxy.region import ProxiedRegion
|
||||
from hippolyzer.lib.proxy.sessions import Session
|
||||
|
||||
|
||||
JUST_CREATED_FLAGS = (ObjectUpdateFlags.CREATE_SELECTED | ObjectUpdateFlags.OBJECT_YOU_OWNER)
|
||||
PRIM_SCALE = 0.2
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class PixelArtistAddon(BaseAddon):
|
||||
# Watch for any newly created prims, this is basically what the viewer does to find
|
||||
# prims that it just created with the build tool.
|
||||
with session.objects.events.subscribe_async(
|
||||
(UpdateType.OBJECT_UPDATE,),
|
||||
(ObjectUpdateType.OBJECT_UPDATE,),
|
||||
predicate=lambda e: e.object.UpdateFlags & JUST_CREATED_FLAGS and "LocalID" in e.updated
|
||||
) as get_events:
|
||||
# Create a pool of prims to use for building the pixel art
|
||||
|
||||
@@ -113,7 +113,8 @@ class MessageHandler(Generic[_T, _K]):
|
||||
|
||||
async def _canceller():
|
||||
await asyncio.sleep(timeout)
|
||||
fut.set_exception(asyncio.exceptions.TimeoutError("Timed out waiting for packet"))
|
||||
if not fut.done():
|
||||
fut.set_exception(asyncio.exceptions.TimeoutError("Timed out waiting for packet"))
|
||||
for n in notifiers:
|
||||
n.unsubscribe(_handler)
|
||||
|
||||
@@ -126,7 +127,8 @@ class MessageHandler(Generic[_T, _K]):
|
||||
# Whatever was awaiting this future now owns this message
|
||||
if take:
|
||||
message = message.take()
|
||||
fut.set_result(message)
|
||||
if not fut.done():
|
||||
fut.set_result(message)
|
||||
# Make sure to unregister this handler for all message types
|
||||
for n in notifiers:
|
||||
n.unsubscribe(_handler)
|
||||
|
||||
@@ -146,6 +146,50 @@ class InventoryType(IntEnum):
|
||||
}.get(lower, lower)
|
||||
|
||||
|
||||
class FolderType(IntEnum):
|
||||
TEXTURE = 0
|
||||
SOUND = 1
|
||||
CALLINGCARD = 2
|
||||
LANDMARK = 3
|
||||
CLOTHING = 5
|
||||
OBJECT = 6
|
||||
NOTECARD = 7
|
||||
# We'd really like to change this to 9 since AT_CATEGORY is 8,
|
||||
# but "My Inventory" has been type 8 for a long time.
|
||||
ROOT_INVENTORY = 8
|
||||
LSL_TEXT = 10
|
||||
BODYPART = 13
|
||||
TRASH = 14
|
||||
SNAPSHOT_CATEGORY = 15
|
||||
LOST_AND_FOUND = 16
|
||||
ANIMATION = 20
|
||||
GESTURE = 21
|
||||
FAVORITE = 23
|
||||
ENSEMBLE_START = 26
|
||||
ENSEMBLE_END = 45
|
||||
# This range is reserved for special clothing folder types.
|
||||
CURRENT_OUTFIT = 46
|
||||
OUTFIT = 47
|
||||
MY_OUTFITS = 48
|
||||
MESH = 49
|
||||
# "received items" for MP
|
||||
INBOX = 50
|
||||
OUTBOX = 51
|
||||
BASIC_ROOT = 52
|
||||
MARKETPLACE_LISTINGS = 53
|
||||
MARKETPLACE_STOCK = 54
|
||||
# Note: We actually *never* create folders with that type. This is used for icon override only.
|
||||
MARKETPLACE_VERSION = 55
|
||||
SETTINGS = 56
|
||||
# Firestorm folders, may not actually exist
|
||||
FIRESTORM = 57
|
||||
PHOENIX = 58
|
||||
RLV = 59
|
||||
# Opensim folders
|
||||
MY_SUITCASE = 100
|
||||
NONE = -1
|
||||
|
||||
|
||||
@se.enum_field_serializer("AgentIsNowWearing", "WearableData", "WearableType")
|
||||
@se.enum_field_serializer("AgentWearablesUpdate", "WearableData", "WearableType")
|
||||
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "WearableType")
|
||||
@@ -179,6 +223,7 @@ def _register_permissions_flags(message_name, block_name):
|
||||
|
||||
@se.flag_field_serializer("ObjectPermissions", "ObjectData", "Mask")
|
||||
@_register_permissions_flags("ObjectProperties", "ObjectData")
|
||||
@_register_permissions_flags("ObjectPropertiesFamily", "ObjectData")
|
||||
@_register_permissions_flags("UpdateCreateInventoryItem", "InventoryData")
|
||||
@_register_permissions_flags("UpdateTaskInventory", "InventoryData")
|
||||
@_register_permissions_flags("CreateInventoryItem", "InventoryBlock")
|
||||
@@ -203,11 +248,74 @@ class Permissions(IntFlag):
|
||||
RESERVED = 1 << 31
|
||||
|
||||
|
||||
@se.enum_field_serializer("ObjectSaleInfo", "ObjectData", "SaleType")
|
||||
@se.enum_field_serializer("ObjectProperties", "ObjectData", "SaleType")
|
||||
@se.enum_field_serializer("ObjectPropertiesFamily", "ObjectData", "SaleType")
|
||||
@se.enum_field_serializer("ObjectBuy", "ObjectData", "SaleType")
|
||||
@se.enum_field_serializer("RezScript", "InventoryBlock", "SaleType")
|
||||
@se.enum_field_serializer("RezObject", "InventoryData", "SaleType")
|
||||
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "SaleType")
|
||||
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "SaleType")
|
||||
class SaleInfo(IntEnum):
|
||||
NOT = 0
|
||||
ORIGINAL = 1
|
||||
COPY = 2
|
||||
CONTENTS = 3
|
||||
|
||||
|
||||
@se.flag_field_serializer("ParcelInfoReply", "Data", "Flags")
|
||||
class ParcelInfoFlags(IntFlag):
|
||||
MATURE = 1 << 0
|
||||
# You should never see adult without mature
|
||||
ADULT = 1 << 1
|
||||
GROUP_OWNED = 1 << 2
|
||||
|
||||
|
||||
@se.flag_field_serializer("MapItemRequest", "AgentData", "Flags")
|
||||
@se.flag_field_serializer("MapNameRequest", "AgentData", "Flags")
|
||||
@se.flag_field_serializer("MapBlockRequest", "AgentData", "Flags")
|
||||
@se.flag_field_serializer("MapItemReply", "AgentData", "Flags")
|
||||
@se.flag_field_serializer("MapNameReply", "AgentData", "Flags")
|
||||
@se.flag_field_serializer("MapBlockReply", "AgentData", "Flags")
|
||||
class MapImageFlags(IntFlag):
|
||||
# No clue, honestly. I guess there's potentially different image types you could request.
|
||||
LAYER = 1 << 1
|
||||
|
||||
|
||||
@se.enum_field_serializer("MapBlockReply", "Data", "Access")
|
||||
class SimAccess(IntEnum):
|
||||
# Treated as 'unknown', usually ends up being SIM_ACCESS_PG
|
||||
MIN = 0
|
||||
PG = 13
|
||||
MATURE = 21
|
||||
ADULT = 42
|
||||
DOWN = 254
|
||||
|
||||
|
||||
@se.enum_field_serializer("MapItemRequest", "RequestData", "ItemType")
|
||||
@se.enum_field_serializer("MapItemReply", "RequestData", "ItemType")
|
||||
class MapItemType(IntEnum):
|
||||
# Treated as 'unknown', usually ends up being SIM_ACCESS_PG
|
||||
TELEHUB = 0x01
|
||||
PG_EVENT = 0x02
|
||||
MATURE_EVENT = 0x03
|
||||
# No longer supported, 2009-03-02 KLW
|
||||
DEPRECATED_POPULAR = 0x04
|
||||
DEPRECATED_AGENT_COUNT = 0x05
|
||||
AGENT_LOCATIONS = 0x06
|
||||
LAND_FOR_SALE = 0x07
|
||||
CLASSIFIED = 0x08
|
||||
ADULT_EVENT = 0x09
|
||||
LAND_FOR_SALE_ADULT = 0x0a
|
||||
|
||||
|
||||
@se.flag_field_serializer("RezObject", "RezData", "ItemFlags")
|
||||
@se.flag_field_serializer("RezMultipleAttachmentsFromInv", "ObjectData", "ItemFlags")
|
||||
@se.flag_field_serializer("RezObject", "InventoryData", "Flags")
|
||||
@se.flag_field_serializer("RezScript", "InventoryBlock", "Flags")
|
||||
@se.flag_field_serializer("UpdateCreateInventoryItem", "InventoryData", "Flags")
|
||||
@se.flag_field_serializer("UpdateTaskInventory", "InventoryData", "Flags")
|
||||
@se.flag_field_serializer("ChangeInventoryItemFlags", "InventoryData", "Flags")
|
||||
class InventoryItemFlags(IntFlag):
|
||||
# The asset has only one reference in the system. If the
|
||||
# inventory item is deleted, or the assetid updated, then we
|
||||
@@ -234,7 +342,8 @@ class InventoryItemFlags(IntFlag):
|
||||
OBJECT_HAS_MULTIPLE_ITEMS = 0x200000
|
||||
|
||||
@property
|
||||
def attachment_point(self):
|
||||
def subtype(self):
|
||||
"""Subtype of the given item type, could be an attachment point or setting type, etc."""
|
||||
return self & 0xFF
|
||||
|
||||
|
||||
@@ -763,6 +872,7 @@ class MCode(IntEnum):
|
||||
@se.flag_field_serializer("ObjectUpdateCompressed", "ObjectData", "UpdateFlags")
|
||||
@se.flag_field_serializer("ObjectUpdateCached", "ObjectData", "UpdateFlags")
|
||||
@se.flag_field_serializer("ObjectAdd", "ObjectData", "AddFlags")
|
||||
@se.flag_field_serializer("ObjectDuplicate", "SharedData", "DuplicateFlags")
|
||||
class ObjectUpdateFlags(IntFlag):
|
||||
USE_PHYSICS = 1 << 0
|
||||
CREATE_SELECTED = 1 << 1
|
||||
@@ -798,6 +908,9 @@ class ObjectUpdateFlags(IntFlag):
|
||||
ZLIB_COMPRESSED_REPRECATED = 1 << 31
|
||||
|
||||
|
||||
JUST_CREATED_FLAGS = (ObjectUpdateFlags.CREATE_SELECTED | ObjectUpdateFlags.OBJECT_YOU_OWNER)
|
||||
|
||||
|
||||
class AttachmentStateAdapter(se.Adapter):
|
||||
# Encoded attachment point ID for attached objects
|
||||
# nibbles are swapped around because old attachment nums only used to live
|
||||
@@ -842,6 +955,15 @@ class ObjectStateSerializer(se.AdapterSubfieldSerializer):
|
||||
ORIG_INLINE = True
|
||||
|
||||
|
||||
@se.subfield_serializer("ObjectUpdate", "RegionData", "TimeDilation")
|
||||
@se.subfield_serializer("ObjectUpdateCompressed", "RegionData", "TimeDilation")
|
||||
@se.subfield_serializer("ObjectUpdateCached", "RegionData", "TimeDilation")
|
||||
@se.subfield_serializer("ImprovedTerseObjectUpdate", "RegionData", "TimeDilation")
|
||||
class TimeDilationSerializer(se.AdapterSubfieldSerializer):
|
||||
ADAPTER = se.QuantizedFloat(se.U16, 0.0, 1.0, False)
|
||||
ORIG_INLINE = True
|
||||
|
||||
|
||||
@se.subfield_serializer("ImprovedTerseObjectUpdate", "ObjectData", "Data")
|
||||
class ImprovedTerseObjectUpdateDataSerializer(se.SimpleSubfieldSerializer):
|
||||
TEMPLATE = se.Template({
|
||||
@@ -867,9 +989,9 @@ class ShineLevel(IntEnum):
|
||||
@dataclasses.dataclass(unsafe_hash=True)
|
||||
class BasicMaterials:
|
||||
# Meaning is technically implementation-dependent, these are in LL data files
|
||||
Bump: int = se.bitfield_field(bits=5)
|
||||
FullBright: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
|
||||
Shiny: int = se.bitfield_field(bits=2, adapter=se.IntEnum(ShineLevel))
|
||||
Bump: int = se.bitfield_field(bits=5, default=0)
|
||||
FullBright: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter(), default=False)
|
||||
Shiny: int = se.bitfield_field(bits=2, adapter=se.IntEnum(ShineLevel), default=0)
|
||||
|
||||
|
||||
BUMP_SHINY_FULLBRIGHT = se.BitfieldDataclass(BasicMaterials, se.U8)
|
||||
@@ -885,10 +1007,10 @@ class TexGen(IntEnum):
|
||||
|
||||
@dataclasses.dataclass(unsafe_hash=True)
|
||||
class MediaFlags:
|
||||
WebPage: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
|
||||
TexGen: "TexGen" = se.bitfield_field(bits=2, adapter=se.IntEnum(TexGen))
|
||||
WebPage: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter(), default=False)
|
||||
TexGen: "TexGen" = se.bitfield_field(bits=2, adapter=se.IntEnum(TexGen), default=TexGen.DEFAULT)
|
||||
# Probably unused but show it just in case
|
||||
_Unused: int = se.bitfield_field(bits=5)
|
||||
_Unused: int = se.bitfield_field(bits=5, default=0)
|
||||
|
||||
|
||||
# Not shifted so enum definitions can match indra
|
||||
@@ -1073,9 +1195,9 @@ class TextureEntry:
|
||||
OffsetsT: float = 0.0
|
||||
# In radians
|
||||
Rotation: float = 0.0
|
||||
MediaFlags: Optional[MediaFlags] = None
|
||||
BasicMaterials: Optional[BasicMaterials] = None
|
||||
Glow: int = 0
|
||||
MediaFlags: "MediaFlags" = dataclasses.field(default_factory=MediaFlags)
|
||||
BasicMaterials: "BasicMaterials" = dataclasses.field(default_factory=BasicMaterials)
|
||||
Glow: float = 0.0
|
||||
Materials: UUID = UUID.ZERO
|
||||
|
||||
def st_to_uv(self, st_coord: Vector3) -> Vector3:
|
||||
@@ -1110,14 +1232,10 @@ class TextureEntryCollection:
|
||||
OffsetsT: Dict[_TE_FIELD_KEY, float] = _te_field(TE_S16_COORD, default=0.0)
|
||||
Rotation: Dict[_TE_FIELD_KEY, float] = _te_field(PackedTERotation(), default=0.0)
|
||||
BasicMaterials: Dict[_TE_FIELD_KEY, "BasicMaterials"] = _te_field(
|
||||
BUMP_SHINY_FULLBRIGHT, default_factory=lambda: BasicMaterials(Bump=0, FullBright=False, Shiny=0),
|
||||
BUMP_SHINY_FULLBRIGHT, default_factory=BasicMaterials,
|
||||
)
|
||||
MediaFlags: Dict[_TE_FIELD_KEY, "MediaFlags"] = _te_field(
|
||||
MEDIA_FLAGS,
|
||||
default_factory=lambda: MediaFlags(WebPage=False, TexGen=TexGen.DEFAULT, _Unused=0),
|
||||
)
|
||||
# TODO: dequantize
|
||||
Glow: Dict[_TE_FIELD_KEY, int] = _te_field(se.U8, default=0)
|
||||
MediaFlags: Dict[_TE_FIELD_KEY, "MediaFlags"] = _te_field(MEDIA_FLAGS, default_factory=MediaFlags)
|
||||
Glow: Dict[_TE_FIELD_KEY, float] = _te_field(se.QuantizedFloat(se.U8, 0.0, 1.0), default=0.0)
|
||||
Materials: Dict[_TE_FIELD_KEY, UUID] = _te_field(se.UUID, optional=True, default=UUID.ZERO)
|
||||
|
||||
def unwrap(self):
|
||||
@@ -1660,6 +1778,7 @@ class DeRezObjectDestination(IntEnum):
|
||||
@se.flag_field_serializer("SimStats", "RegionInfo", "RegionFlagsExtended")
|
||||
@se.flag_field_serializer("RegionInfo", "RegionInfo", "RegionFlags")
|
||||
@se.flag_field_serializer("RegionInfo", "RegionInfo3", "RegionFlagsExtended")
|
||||
@se.flag_field_serializer("MapBlockReply", "Data", "RegionFlags")
|
||||
class RegionFlags(IntFlag):
|
||||
ALLOW_DAMAGE = 1 << 0
|
||||
ALLOW_LANDMARK = 1 << 1
|
||||
@@ -1728,6 +1847,131 @@ class TeleportFlags(IntFlag):
|
||||
WITHIN_REGION = 1 << 17
|
||||
|
||||
|
||||
@se.flag_field_serializer("AvatarPropertiesReply", "PropertiesData", "Flags")
|
||||
class AvatarPropertiesFlags(IntFlag):
|
||||
ALLOW_PUBLISH = 1 << 0 # whether profile is externally visible or not
|
||||
MATURE_PUBLISH = 1 << 1 # profile is "mature"
|
||||
IDENTIFIED = 1 << 2 # whether avatar has provided payment info
|
||||
TRANSACTED = 1 << 3 # whether avatar has actively used payment info
|
||||
ONLINE = 1 << 4 # the online status of this avatar, if known.
|
||||
AGEVERIFIED = 1 << 5 # whether avatar has been age-verified
|
||||
|
||||
|
||||
@se.flag_field_serializer("AvatarGroupsReply", "GroupData", "GroupPowers")
|
||||
@se.flag_field_serializer("AvatarGroupDataUpdate", "GroupData", "GroupPowers")
|
||||
@se.flag_field_serializer("AvatarDataUpdate", "AgentDataData", "GroupPowers")
|
||||
class GroupPowerFlags(IntFlag):
|
||||
MEMBER_INVITE = 1 << 1 # Invite member
|
||||
MEMBER_EJECT = 1 << 2 # Eject member from group
|
||||
MEMBER_OPTIONS = 1 << 3 # Toggle "Open enrollment" and change "Signup Fee"
|
||||
MEMBER_VISIBLE_IN_DIR = 1 << 47
|
||||
|
||||
# Roles
|
||||
ROLE_CREATE = 1 << 4 # Create new roles
|
||||
ROLE_DELETE = 1 << 5 # Delete roles
|
||||
ROLE_PROPERTIES = 1 << 6 # Change Role Names, Titles, and Descriptions (Of roles the user is in, only, or any role in group?)
|
||||
ROLE_ASSIGN_MEMBER_LIMITED = 1 << 7 # Assign Member to a Role that the assigner is in
|
||||
ROLE_ASSIGN_MEMBER = 1 << 8 # Assign Member to Role
|
||||
ROLE_REMOVE_MEMBER = 1 << 9 # Remove Member from Role
|
||||
ROLE_CHANGE_ACTIONS = 1 << 10 # Change actions a role can perform
|
||||
|
||||
# Group Identity
|
||||
GROUP_CHANGE_IDENTITY = 1 << 11 # Charter, insignia, 'Show In Group List', 'Publish on the web', 'Mature', all 'Show Member In Group Profile' checkboxes
|
||||
|
||||
# Parcel Management
|
||||
LAND_DEED = 1 << 12 # Deed Land and Buy Land for Group
|
||||
LAND_RELEASE = 1 << 13 # Release Land (to Gov. Linden)
|
||||
LAND_SET_SALE_INFO = 1 << 14 # Set for sale info (Toggle "For Sale", Set Price, Set Target, Toggle "Sell objects with the land")
|
||||
LAND_DIVIDE_JOIN = 1 << 15 # Divide and Join Parcels
|
||||
|
||||
# Parcel Identity
|
||||
LAND_FIND_PLACES = 1 << 17 # Toggle "Show in Find Places" and Set Category.
|
||||
LAND_CHANGE_IDENTITY = 1 << 18 # Change Parcel Identity: Parcel Name, Parcel Description, Snapshot, 'Publish on the web', and 'Mature' checkbox
|
||||
LAND_SET_LANDING_POINT = 1 << 19 # Set Landing Point
|
||||
|
||||
# Parcel Settings
|
||||
LAND_CHANGE_MEDIA = 1 << 20 # Change Media Settings
|
||||
LAND_EDIT = 1 << 21 # Toggle Edit Land
|
||||
LAND_OPTIONS = 1 << 22 # Toggle Set Home Point, Fly, Outside Scripts, Create/Edit Objects, Landmark, and Damage checkboxes
|
||||
|
||||
# Parcel Powers
|
||||
LAND_ALLOW_EDIT_LAND = 1 << 23 # Bypass Edit Land Restriction
|
||||
LAND_ALLOW_FLY = 1 << 24 # Bypass Fly Restriction
|
||||
LAND_ALLOW_CREATE = 1 << 25 # Bypass Create/Edit Objects Restriction
|
||||
LAND_ALLOW_LANDMARK = 1 << 26 # Bypass Landmark Restriction
|
||||
LAND_ALLOW_SET_HOME = 1 << 28 # Bypass Set Home Point Restriction
|
||||
LAND_ALLOW_HOLD_EVENT = 1 << 41 # Allowed to hold events on group-owned land
|
||||
LAND_ALLOW_ENVIRONMENT = 1 << 46 # Allowed to change the environment
|
||||
|
||||
# Parcel Access
|
||||
LAND_MANAGE_ALLOWED = 1 << 29 # Manage Allowed List
|
||||
LAND_MANAGE_BANNED = 1 << 30 # Manage Banned List
|
||||
LAND_MANAGE_PASSES = 1 << 31 # Change Sell Pass Settings
|
||||
LAND_ADMIN = 1 << 32 # Eject and Freeze Users on the land
|
||||
|
||||
# Parcel Content
|
||||
LAND_RETURN_GROUP_SET = 1 << 33 # Return objects on parcel that are set to group
|
||||
LAND_RETURN_NON_GROUP = 1 << 34 # Return objects on parcel that are not set to group
|
||||
LAND_RETURN_GROUP_OWNED = 1 << 48 # Return objects on parcel that are owned by the group
|
||||
|
||||
LAND_GARDENING = 1 << 35 # Parcel Gardening - plant and move linden trees
|
||||
|
||||
# Object Management
|
||||
OBJECT_DEED = 1 << 36 # Deed Object
|
||||
OBJECT_MANIPULATE = 1 << 38 # Manipulate Group Owned Objects (Move, Copy, Mod)
|
||||
OBJECT_SET_SALE = 1 << 39 # Set Group Owned Object for Sale
|
||||
|
||||
# Accounting
|
||||
ACCOUNTING_ACCOUNTABLE = 1 << 40 # Pay Group Liabilities and Receive Group Dividends
|
||||
|
||||
# Notices
|
||||
NOTICES_SEND = 1 << 42 # Send Notices
|
||||
NOTICES_RECEIVE = 1 << 43 # Receive Notices and View Notice History
|
||||
|
||||
# Proposals
|
||||
# TODO: _DEPRECATED suffix as part of vote removal - DEV-24856:
|
||||
PROPOSAL_START = 1 << 44 # Start Proposal
|
||||
# TODO: _DEPRECATED suffix as part of vote removal - DEV-24856:
|
||||
PROPOSAL_VOTE = 1 << 45 # Vote on Proposal
|
||||
|
||||
# Group chat moderation related
|
||||
SESSION_JOIN = 1 << 16 # can join session
|
||||
SESSION_VOICE = 1 << 27 # can hear/talk
|
||||
SESSION_MODERATOR = 1 << 37 # can mute people's session
|
||||
|
||||
EXPERIENCE_ADMIN = 1 << 49 # has admin rights to any experiences owned by this group
|
||||
EXPERIENCE_CREATOR = 1 << 50 # can sign scripts for experiences owned by this group
|
||||
|
||||
# Group Banning
|
||||
GROUP_BAN_ACCESS = 1 << 51 # Allows access to ban / un-ban agents from a group.
|
||||
|
||||
|
||||
@se.flag_field_serializer("RequestObjectPropertiesFamily", "ObjectData", "RequestFlags")
|
||||
@se.flag_field_serializer("ObjectPropertiesFamily", "ObjectData", "RequestFlags")
|
||||
class ObjectPropertiesFamilyRequestFlags(IntFlag):
|
||||
BUG_REPORT = 1 << 0
|
||||
COMPLAINT_REPORT = 1 << 1
|
||||
OBJECT_PAY = 1 << 2
|
||||
|
||||
|
||||
@se.enum_field_serializer("RequestImage", "RequestImage", "Type")
|
||||
class RequestImageType(IntEnum):
|
||||
NORMAL = 0
|
||||
AVATAR_BAKE = 1
|
||||
|
||||
|
||||
@se.enum_field_serializer("ImageData", "ImageID", "Codec")
|
||||
class ImageCodec(IntEnum):
|
||||
INVALID = 0
|
||||
RGB = 1
|
||||
J2C = 2
|
||||
BMP = 3
|
||||
TGA = 4
|
||||
JPEG = 5
|
||||
DXT = 6
|
||||
PNG = 7
|
||||
|
||||
|
||||
@se.http_serializer("RenderMaterials")
|
||||
class RenderMaterialsSerializer(se.BaseHTTPSerializer):
|
||||
@classmethod
|
||||
|
||||
@@ -34,7 +34,7 @@ LOG = logging.getLogger(__name__)
|
||||
OBJECT_OR_LOCAL = Union[Object, int]
|
||||
|
||||
|
||||
class UpdateType(enum.IntEnum):
|
||||
class ObjectUpdateType(enum.IntEnum):
|
||||
OBJECT_UPDATE = enum.auto()
|
||||
PROPERTIES = enum.auto()
|
||||
FAMILY = enum.auto()
|
||||
@@ -124,7 +124,7 @@ class ClientObjectManager:
|
||||
for local_id in local_ids:
|
||||
if local_id in unselected_ids:
|
||||
# Need to wait until we get our reply
|
||||
fut = self.state.register_future(local_id, UpdateType.PROPERTIES)
|
||||
fut = self.state.register_future(local_id, ObjectUpdateType.PROPERTIES)
|
||||
else:
|
||||
# This was selected so we should already have up to date info
|
||||
fut = asyncio.Future()
|
||||
@@ -159,7 +159,7 @@ class ClientObjectManager:
|
||||
|
||||
futures = []
|
||||
for local_id in local_ids:
|
||||
futures.append(self.state.register_future(local_id, UpdateType.OBJECT_UPDATE))
|
||||
futures.append(self.state.register_future(local_id, ObjectUpdateType.OBJECT_UPDATE))
|
||||
return futures
|
||||
|
||||
|
||||
@@ -168,15 +168,15 @@ class ObjectEvent:
|
||||
|
||||
object: Object
|
||||
updated: Set[str]
|
||||
update_type: UpdateType
|
||||
update_type: ObjectUpdateType
|
||||
|
||||
def __init__(self, obj: Object, updated: Set[str], update_type: UpdateType):
|
||||
def __init__(self, obj: Object, updated: Set[str], update_type: ObjectUpdateType):
|
||||
self.object = obj
|
||||
self.updated = updated
|
||||
self.update_type = update_type
|
||||
|
||||
@property
|
||||
def name(self) -> UpdateType:
|
||||
def name(self) -> ObjectUpdateType:
|
||||
return self.update_type
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ class ClientWorldObjectManager:
|
||||
self._session: BaseClientSession = session
|
||||
self._settings = settings
|
||||
self.name_cache = name_cache or NameCache()
|
||||
self.events: MessageHandler[ObjectEvent, UpdateType] = MessageHandler(take_by_default=False)
|
||||
self.events: MessageHandler[ObjectEvent, ObjectUpdateType] = MessageHandler(take_by_default=False)
|
||||
self._fullid_lookup: Dict[UUID, Object] = {}
|
||||
self._avatars: Dict[UUID, Avatar] = {}
|
||||
self._avatar_objects: Dict[UUID, Object] = {}
|
||||
@@ -295,7 +295,7 @@ class ClientWorldObjectManager:
|
||||
self._rebuild_avatar_objects()
|
||||
self._region_managers.clear()
|
||||
|
||||
def _update_existing_object(self, obj: Object, new_properties: dict, update_type: UpdateType):
|
||||
def _update_existing_object(self, obj: Object, new_properties: dict, update_type: ObjectUpdateType):
|
||||
old_parent_id = obj.ParentID
|
||||
new_parent_id = new_properties.get("ParentID", obj.ParentID)
|
||||
old_local_id = obj.LocalID
|
||||
@@ -354,7 +354,7 @@ class ClientWorldObjectManager:
|
||||
if obj.PCode == PCode.AVATAR:
|
||||
self._avatar_objects[obj.FullID] = obj
|
||||
self._rebuild_avatar_objects()
|
||||
self._run_object_update_hooks(obj, set(obj.to_dict().keys()), UpdateType.OBJECT_UPDATE)
|
||||
self._run_object_update_hooks(obj, set(obj.to_dict().keys()), ObjectUpdateType.OBJECT_UPDATE)
|
||||
|
||||
def _kill_object_by_local_id(self, region_state: RegionObjectsState, local_id: int):
|
||||
obj = region_state.lookup_localid(local_id)
|
||||
@@ -406,7 +406,7 @@ class ClientWorldObjectManager:
|
||||
# our view of the world then we want to move it to this region.
|
||||
obj = self.lookup_fullid(object_data["FullID"])
|
||||
if obj:
|
||||
self._update_existing_object(obj, object_data, UpdateType.OBJECT_UPDATE)
|
||||
self._update_existing_object(obj, object_data, ObjectUpdateType.OBJECT_UPDATE)
|
||||
else:
|
||||
if region_state is None:
|
||||
continue
|
||||
@@ -430,7 +430,7 @@ class ClientWorldObjectManager:
|
||||
# Need the Object as context because decoding state requires PCode.
|
||||
state_deserializer = ObjectStateSerializer.deserialize
|
||||
object_data["State"] = state_deserializer(ctx_obj=obj, val=object_data["State"])
|
||||
self._update_existing_object(obj, object_data, UpdateType.OBJECT_UPDATE)
|
||||
self._update_existing_object(obj, object_data, ObjectUpdateType.OBJECT_UPDATE)
|
||||
else:
|
||||
if region_state:
|
||||
region_state.missing_locals.add(object_data["LocalID"])
|
||||
@@ -458,7 +458,7 @@ class ClientWorldObjectManager:
|
||||
self._update_existing_object(obj, {
|
||||
"UpdateFlags": update_flags,
|
||||
"RegionHandle": handle,
|
||||
}, UpdateType.OBJECT_UPDATE)
|
||||
}, ObjectUpdateType.OBJECT_UPDATE)
|
||||
continue
|
||||
|
||||
cached_obj_data = self._lookup_cache_entry(handle, block["ID"], block["CRC"])
|
||||
@@ -497,7 +497,7 @@ class ClientWorldObjectManager:
|
||||
LOG.warning(f"Got ObjectUpdateCompressed for unknown region {handle}: {object_data!r}")
|
||||
obj = self.lookup_fullid(object_data["FullID"])
|
||||
if obj:
|
||||
self._update_existing_object(obj, object_data, UpdateType.OBJECT_UPDATE)
|
||||
self._update_existing_object(obj, object_data, ObjectUpdateType.OBJECT_UPDATE)
|
||||
else:
|
||||
if region_state is None:
|
||||
continue
|
||||
@@ -514,7 +514,7 @@ class ClientWorldObjectManager:
|
||||
obj = self.lookup_fullid(block["ObjectID"])
|
||||
if obj:
|
||||
seen_locals.append(obj.LocalID)
|
||||
self._update_existing_object(obj, object_properties, UpdateType.PROPERTIES)
|
||||
self._update_existing_object(obj, object_properties, ObjectUpdateType.PROPERTIES)
|
||||
else:
|
||||
LOG.debug(f"Received {packet.name} for unknown {block['ObjectID']}")
|
||||
packet.meta["ObjectUpdateIDs"] = tuple(seen_locals)
|
||||
@@ -561,9 +561,9 @@ class ClientWorldObjectManager:
|
||||
LOG.debug(f"Received ObjectCost for unknown {object_id}")
|
||||
continue
|
||||
obj.ObjectCosts.update(object_costs)
|
||||
self._run_object_update_hooks(obj, {"ObjectCosts"}, UpdateType.COSTS)
|
||||
self._run_object_update_hooks(obj, {"ObjectCosts"}, ObjectUpdateType.COSTS)
|
||||
|
||||
def _run_object_update_hooks(self, obj: Object, updated_props: Set[str], update_type: UpdateType):
|
||||
def _run_object_update_hooks(self, obj: Object, updated_props: Set[str], update_type: ObjectUpdateType):
|
||||
region_state = self._get_region_state(obj.RegionHandle)
|
||||
region_state.resolve_futures(obj, update_type)
|
||||
if obj.PCode == PCode.AVATAR and "NameValue" in updated_props:
|
||||
@@ -572,7 +572,7 @@ class ClientWorldObjectManager:
|
||||
self.events.handle(ObjectEvent(obj, updated_props, update_type))
|
||||
|
||||
def _run_kill_object_hooks(self, obj: Object):
|
||||
self.events.handle(ObjectEvent(obj, set(), UpdateType.KILL))
|
||||
self.events.handle(ObjectEvent(obj, set(), ObjectUpdateType.KILL))
|
||||
|
||||
def _rebuild_avatar_objects(self):
|
||||
# Get all avatars known through coarse locations and which region the location was in
|
||||
@@ -779,7 +779,7 @@ class RegionObjectsState:
|
||||
del self._orphans[parent_id]
|
||||
return removed
|
||||
|
||||
def register_future(self, local_id: int, future_type: UpdateType) -> asyncio.Future[Object]:
|
||||
def register_future(self, local_id: int, future_type: ObjectUpdateType) -> asyncio.Future[Object]:
|
||||
fut = asyncio.Future()
|
||||
fut_key = (local_id, future_type)
|
||||
local_futs = self._object_futures.get(fut_key, [])
|
||||
@@ -788,7 +788,7 @@ class RegionObjectsState:
|
||||
fut.add_done_callback(local_futs.remove)
|
||||
return fut
|
||||
|
||||
def resolve_futures(self, obj: Object, update_type: UpdateType):
|
||||
def resolve_futures(self, obj: Object, update_type: ObjectUpdateType):
|
||||
futures = self._object_futures.get((obj.LocalID, update_type), [])
|
||||
for fut in futures[:]:
|
||||
fut.set_result(obj)
|
||||
|
||||
@@ -61,6 +61,7 @@ class BaseInteractionManager:
|
||||
# Used to initialize a REPL environment with commonly desired helpers
|
||||
REPL_INITIALIZER = r"""
|
||||
from hippolyzer.lib.base.datatypes import *
|
||||
from hippolyzer.lib.base.templates import *
|
||||
from hippolyzer.lib.base.message.message import Block, Message, Direction
|
||||
from hippolyzer.lib.proxy.addon_utils import send_chat, show_message
|
||||
"""
|
||||
@@ -141,8 +142,15 @@ class AddonManager:
|
||||
if _locals is None:
|
||||
_locals = stack.frame.f_locals
|
||||
|
||||
_globals = dict(_globals)
|
||||
exec(REPL_INITIALIZER, _globals, None)
|
||||
init_globals = {}
|
||||
exec(REPL_INITIALIZER, init_globals, None)
|
||||
# We're modifying the globals of the caller, be careful of things we imported
|
||||
# for the REPL initializer clobber things that already exist in the caller's globals.
|
||||
# Making our own mutable copy of the globals dict, mutating that and then passing it
|
||||
# to embed() is not an option due to https://github.com/prompt-toolkit/ptpython/issues/279
|
||||
for global_name, global_val in init_globals.items():
|
||||
if global_name not in _globals:
|
||||
_globals[global_name] = global_val
|
||||
|
||||
async def _wrapper():
|
||||
coro: Coroutine = ptpython.repl.embed( # noqa: the type signature lies
|
||||
|
||||
@@ -11,7 +11,7 @@ from hippolyzer.lib.base.templates import PCode
|
||||
from hippolyzer.lib.client.namecache import NameCache
|
||||
from hippolyzer.lib.client.object_manager import (
|
||||
ClientObjectManager,
|
||||
UpdateType, ClientWorldObjectManager,
|
||||
ObjectUpdateType, ClientWorldObjectManager,
|
||||
)
|
||||
|
||||
from hippolyzer.lib.base.objects import Object
|
||||
@@ -133,7 +133,7 @@ class ProxyWorldObjectManager(ClientWorldObjectManager):
|
||||
region_mgr.queued_cache_misses |= missing_locals
|
||||
region_mgr.request_missed_cached_objects_soon()
|
||||
|
||||
def _run_object_update_hooks(self, obj: Object, updated_props: Set[str], update_type: UpdateType):
|
||||
def _run_object_update_hooks(self, obj: Object, updated_props: Set[str], update_type: ObjectUpdateType):
|
||||
super()._run_object_update_hooks(obj, updated_props, update_type)
|
||||
region = self._session.region_by_handle(obj.RegionHandle)
|
||||
if self._settings.ALLOW_AUTO_REQUEST_OBJECTS:
|
||||
|
||||
@@ -129,12 +129,16 @@ class ProxiedRegion(BaseClientRegion):
|
||||
|
||||
def register_proxy_cap(self, name: str):
|
||||
"""Register a cap to be completely handled by the proxy"""
|
||||
if name in self.caps:
|
||||
# If we have an existing cap then we should just use that.
|
||||
cap_data = self.caps[name]
|
||||
if cap_data[1] == CapType.PROXY_ONLY:
|
||||
return cap_data[0]
|
||||
cap_url = f"http://{uuid.uuid4()!s}.caps.hippo-proxy.localhost"
|
||||
self.register_cap(name, cap_url, CapType.PROXY_ONLY)
|
||||
return cap_url
|
||||
|
||||
def register_cap(self, name: str, cap_url: str, cap_type: CapType = CapType.NORMAL):
|
||||
"""Register a Cap that only has meaning the first time it's used"""
|
||||
self.caps.add(name, (cap_type, cap_url))
|
||||
self._recalc_caps()
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ class WebAppCapAddon(BaseAddon, abc.ABC):
|
||||
def handle_region_registered(self, session: Session, region: ProxiedRegion):
|
||||
# Register a fake URL for our cap. This will add the cap URL to the Seed
|
||||
# response that gets sent back to the client if that cap name was requested.
|
||||
if self.CAP_NAME not in region.cap_urls:
|
||||
region.register_proxy_cap(self.CAP_NAME)
|
||||
|
||||
def handle_session_init(self, session: Session):
|
||||
for region in session.regions:
|
||||
region.register_proxy_cap(self.CAP_NAME)
|
||||
|
||||
def handle_http_request(self, session_manager: SessionManager, flow: HippoHTTPFlow):
|
||||
|
||||
2
setup.py
2
setup.py
@@ -25,7 +25,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
version = '0.11.0'
|
||||
version = '0.11.1'
|
||||
|
||||
with open(path.join(here, 'README.md')) as readme_fh:
|
||||
readme = readme_fh.read()
|
||||
|
||||
@@ -10,7 +10,8 @@ from hippolyzer.lib.base.message.message import Block, Message as Message
|
||||
from hippolyzer.lib.base.message.udpdeserializer import UDPMessageDeserializer
|
||||
from hippolyzer.lib.base.message.udpserializer import UDPMessageSerializer
|
||||
from hippolyzer.lib.base.objects import Object, normalize_object_update_compressed_data
|
||||
from hippolyzer.lib.base.templates import ExtraParamType, PCode
|
||||
from hippolyzer.lib.base.templates import ExtraParamType, PCode, JUST_CREATED_FLAGS
|
||||
from hippolyzer.lib.client.object_manager import ObjectUpdateType
|
||||
from hippolyzer.lib.proxy.addons import AddonManager
|
||||
from hippolyzer.lib.proxy.addon_utils import BaseAddon
|
||||
from hippolyzer.lib.proxy.region import ProxiedRegion
|
||||
@@ -663,3 +664,45 @@ class SessionObjectManagerTests(ObjectManagerTestMixin, unittest.IsolatedAsyncio
|
||||
self._create_object(local_id=av.LocalID, full_id=av.FullID,
|
||||
pcode=PCode.AVATAR, parent_id=seat_id, pos=(1, 2, 9))
|
||||
self.assertEqual(set(), self.region_object_manager.queued_cache_misses)
|
||||
|
||||
async def test_handle_object_update_event(self):
|
||||
with self.session.objects.events.subscribe_async(
|
||||
message_names=(ObjectUpdateType.OBJECT_UPDATE,),
|
||||
predicate=lambda e: e.object.UpdateFlags & JUST_CREATED_FLAGS and "LocalID" in e.updated,
|
||||
) as get_events:
|
||||
self._create_object(local_id=999)
|
||||
evt = await asyncio.wait_for(get_events(), 1.0)
|
||||
self.assertEqual(999, evt.object.LocalID)
|
||||
|
||||
async def test_handle_object_update_predicate(self):
|
||||
with self.session.objects.events.subscribe_async(
|
||||
message_names=(ObjectUpdateType.OBJECT_UPDATE,),
|
||||
) as get_events:
|
||||
self._create_object(local_id=999)
|
||||
evt = await asyncio.wait_for(get_events(), 1.0)
|
||||
self.assertEqual(999, evt.object.LocalID)
|
||||
|
||||
async def test_handle_object_update_events_two_subscribers(self):
|
||||
with self.session.objects.events.subscribe_async(
|
||||
message_names=(ObjectUpdateType.OBJECT_UPDATE,),
|
||||
) as get_events:
|
||||
with self.session.objects.events.subscribe_async(
|
||||
message_names=(ObjectUpdateType.OBJECT_UPDATE,),
|
||||
) as get_events2:
|
||||
self._create_object(local_id=999)
|
||||
evt = await asyncio.wait_for(get_events(), 1.0)
|
||||
evt2 = await asyncio.wait_for(get_events2(), 1.0)
|
||||
self.assertEqual(999, evt.object.LocalID)
|
||||
self.assertEqual(evt, evt2)
|
||||
|
||||
async def test_handle_object_update_events_two_subscribers_timeout(self):
|
||||
with self.session.objects.events.subscribe_async(
|
||||
message_names=(ObjectUpdateType.OBJECT_UPDATE,),
|
||||
) as get_events:
|
||||
with self.session.objects.events.subscribe_async(
|
||||
message_names=(ObjectUpdateType.OBJECT_UPDATE,),
|
||||
) as get_events2:
|
||||
self._create_object(local_id=999)
|
||||
evt = asyncio.wait_for(get_events(), 0.01)
|
||||
evt2 = asyncio.wait_for(get_events2(), 0.01)
|
||||
await asyncio.gather(evt, evt2)
|
||||
|
||||
Reference in New Issue
Block a user