From 61ec51beecf3125340c2510caf7c9eb8c70314c8 Mon Sep 17 00:00:00 2001 From: Salad Dais Date: Tue, 19 Jul 2022 19:37:47 +0000 Subject: [PATCH] Add demo autoattacher addon example --- addon_examples/demo_autoattacher.py | 158 ++++++++++++++++++++++++++++ hippolyzer/lib/base/templates.py | 48 ++++++++- 2 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 addon_examples/demo_autoattacher.py diff --git a/addon_examples/demo_autoattacher.py b/addon_examples/demo_autoattacher.py new file mode 100644 index 0000000..30a41df --- /dev/null +++ b/addon_examples/demo_autoattacher.py @@ -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()] diff --git a/hippolyzer/lib/base/templates.py b/hippolyzer/lib/base/templates.py index 6dc7d31..9853b9a 100644 --- a/hippolyzer/lib/base/templates.py +++ b/hippolyzer/lib/base/templates.py @@ -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