Add demo autoattacher addon example
This commit is contained in:
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()]
|
||||
@@ -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")
|
||||
@@ -208,6 +252,7 @@ class Permissions(IntFlag):
|
||||
@se.flag_field_serializer("RezObject", "InventoryData", "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 +279,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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user