Fairly invasive, but will help make lib.base useful again. No more Message / ProxiedMessage split!
137 lines
5.0 KiB
Python
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.HTTPResponse.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()]
|