Files
Hippolyzer/addon_examples/horror_animator.py

137 lines
5.0 KiB
Python

"""
Body horror local animation mutator
Demonstrates programmatic modification / generation of animations
It will make you look absurd, obscene.
"""
import copy
import mitmproxy.http
from hippolyzer.lib.base.datatypes import UUID
from hippolyzer.lib.base.llanim import Animation
from hippolyzer.lib.proxy.addon_utils import AssetAliasTracker, BaseAddon, GlobalProperty
from hippolyzer.lib.proxy.http_flow import HippoHTTPFlow
from hippolyzer.lib.base.message.message import Message
from hippolyzer.lib.proxy.region import ProxiedRegion
from hippolyzer.lib.proxy.sessions import Session, SessionManager
from hippolyzer.lib.base.vfs import STATIC_VFS
JOINT_REPLS = {
"Left": "Right",
"Right": "Left",
"LEFT": "RIGHT",
"RIGHT": "LEFT",
}
def _change_joint_name(joint_name: str):
for orig, repl in JOINT_REPLS.items():
if orig in joint_name:
return joint_name.replace(orig, repl)
return joint_name
def _mutate_anim_bytes(anim_bytes: bytes):
anim = Animation.from_bytes(anim_bytes)
new_joints = {}
for name, joint in anim.joints.items():
new_joints[_change_joint_name(name)] = joint
anim.joints = new_joints
for constraint in anim.constraints:
constraint.source_volume = _change_joint_name(constraint.source_volume)
constraint.target_volume = _change_joint_name(constraint.target_volume)
return anim.to_bytes()
class HorrorAnimatorAddon(BaseAddon):
horror_anim_tracker: AssetAliasTracker = GlobalProperty(AssetAliasTracker)
def handle_init(self, session_manager: SessionManager):
# We've reloaded, so make sure assets get new aliases
self.horror_anim_tracker.invalidate_aliases()
def handle_lludp_message(self, session: Session, region: ProxiedRegion, message: Message):
tracker = self.horror_anim_tracker
if message.name == "AvatarAnimation":
# Only do this for the current user
if message["Sender"]["ID"] != session.agent_id:
return
# Replace inbound anim IDs with alias IDs so we can force a cache
# miss and replace the contents
for block in message["AnimationList"][:]:
anim_id = block["AnimID"]
# Many of the anims in the static VFS have special meanings and the viewer
# does different things based on the presence or absence of their IDs
# in the motion list. Make sure those motions come through as usual, but
# also add an alias so we can override the motions with an edited
# version of the motion.
if block["AnimID"] in STATIC_VFS:
new_block = copy.deepcopy(block)
new_block["AnimID"] = tracker.get_alias_uuid(anim_id)
message["AnimationList"].append(new_block)
else:
block["AnimID"] = tracker.get_alias_uuid(anim_id)
elif message.name == "AgentAnimation":
# Make sure to remove any alias IDs from our outbound anim requests
for block in message["AnimationList"]:
orig_id = tracker.get_orig_uuid(block["AnimID"])
if orig_id:
block["AnimID"] = orig_id
def handle_http_request(self, session_manager: SessionManager, flow: HippoHTTPFlow):
if not flow.cap_data.asset_server_cap:
return
anim_id = flow.request.query.get("animatn_id")
if not anim_id:
return
orig_anim_id = self.horror_anim_tracker.get_orig_uuid(UUID(anim_id))
if not orig_anim_id:
return
flow.request.query["animatn_id"] = str(orig_anim_id)
flow.can_stream = False
flow.metadata["horror_anim"] = True
if orig_anim_id in STATIC_VFS:
# These animations are only in the static VFS and won't be served
# by the asset server. Read the anim out of the static VFS and
# send the response back immediately
block = STATIC_VFS[orig_anim_id]
anim_data = STATIC_VFS.read_block(block)
flow.response = mitmproxy.http.Response.make(
200,
_mutate_anim_bytes(anim_data),
{
"Content-Type": "binary/octet-stream",
"Connection": "close",
}
)
return True
# Partial requests for an anim wouldn't make any sense
flow.request.headers.pop("Range", None)
def handle_http_response(self, session_manager: SessionManager, flow: HippoHTTPFlow):
if not flow.metadata.get("horror_anim"):
return
if flow.response.status_code not in (200, 206):
return
flow.response.content = _mutate_anim_bytes(flow.response.content)
# Not a range anymore, update the headers and status.
flow.response.headers.pop("Content-Range", None)
flow.response.status_code = 200
return True
addons = [HorrorAnimatorAddon()]