2021-04-30 17:30:24 +00:00
|
|
|
"""
|
|
|
|
|
Addon demonstrating a Qt GUI, use of the object manager and associated addon hooks
|
|
|
|
|
|
|
|
|
|
Displays a list of all objects that are mostly blue on at least one face based
|
|
|
|
|
on prim colors.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
import enum
|
|
|
|
|
import os.path
|
|
|
|
|
from typing import *
|
|
|
|
|
|
2021-12-06 13:18:31 +00:00
|
|
|
from PySide6 import QtCore, QtGui, QtWidgets
|
2021-04-30 17:30:24 +00:00
|
|
|
|
|
|
|
|
from hippolyzer.lib.base.datatypes import Vector3
|
2021-06-03 02:58:41 +00:00
|
|
|
from hippolyzer.lib.base.message.message import Block, Message
|
2021-04-30 17:30:24 +00:00
|
|
|
from hippolyzer.lib.base.objects import Object
|
|
|
|
|
from hippolyzer.lib.base.ui_helpers import loadUi
|
2021-06-01 08:23:33 +00:00
|
|
|
from hippolyzer.lib.base.templates import PCode
|
2021-04-30 17:30:24 +00:00
|
|
|
from hippolyzer.lib.proxy.addons import AddonManager
|
2021-04-30 18:03:11 +00:00
|
|
|
from hippolyzer.lib.proxy.addon_utils import BaseAddon, SessionProperty
|
2021-04-30 17:30:24 +00:00
|
|
|
from hippolyzer.lib.proxy.commands import handle_command
|
2021-06-03 02:58:41 +00:00
|
|
|
from hippolyzer.lib.base.network.transport import Direction
|
2021-04-30 17:30:24 +00:00
|
|
|
from hippolyzer.lib.proxy.region import ProxiedRegion
|
|
|
|
|
from hippolyzer.lib.proxy.sessions import Session
|
|
|
|
|
from hippolyzer.lib.proxy.task_scheduler import TaskLifeScope
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_color_blueish(color: bytes) -> bool:
|
|
|
|
|
# Eh this is pretty transparent.
|
|
|
|
|
if color[3] < 128:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# pretty low value, more black than anything
|
|
|
|
|
if color[2] < 50:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Blue channel makes up at least 70% of the value
|
|
|
|
|
return (color[2] / sum(color[:3])) > 0.7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_object_blueish(obj: Object):
|
|
|
|
|
if obj.PCode != PCode.PRIMITIVE:
|
|
|
|
|
return False
|
|
|
|
|
for color in obj.TextureEntry.Color.values():
|
|
|
|
|
if _is_color_blueish(color):
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BlueishObjectListGUIAddon(BaseAddon):
|
2021-04-30 18:03:11 +00:00
|
|
|
blueish_model: Optional[BlueishObjectModel] = SessionProperty(None)
|
2021-04-30 17:30:24 +00:00
|
|
|
|
|
|
|
|
# Cancel the coroutine associated with this command if the region, session or addon
|
|
|
|
|
# changes for any reason. Only one allowed at once across all sessions.
|
|
|
|
|
@handle_command(
|
|
|
|
|
single_instance=True,
|
|
|
|
|
lifetime=TaskLifeScope.SESSION | TaskLifeScope.REGION | TaskLifeScope.ADDON
|
|
|
|
|
)
|
|
|
|
|
async def track_blueish(self, session: Session, region: ProxiedRegion):
|
|
|
|
|
"""Open a window that tracks blueish objects in the region"""
|
|
|
|
|
parent = AddonManager.UI.main_window_handle()
|
|
|
|
|
if parent is None:
|
|
|
|
|
raise RuntimeError("Must be run under the GUI proxy")
|
|
|
|
|
|
|
|
|
|
win = BlueishObjectWindow(parent, session)
|
|
|
|
|
win.objectHighlightClicked.connect(self._highlight_object) # type: ignore
|
|
|
|
|
win.objectTeleportClicked.connect(self._teleport_to_object) # type: ignore
|
|
|
|
|
win.show()
|
|
|
|
|
try:
|
|
|
|
|
self.blueish_model = win.model
|
|
|
|
|
self._scan_all_objects(session, region)
|
|
|
|
|
await win.closing
|
|
|
|
|
self.blueish_model = None
|
|
|
|
|
except:
|
|
|
|
|
# Task got killed or something exploded, close the window ourselves
|
|
|
|
|
self.blueish_model = None
|
|
|
|
|
win.close()
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
def _highlight_object(self, session: Session, obj: Object):
|
2021-12-09 05:30:12 +00:00
|
|
|
session.main_region.circuit.send(Message(
|
2021-04-30 17:30:24 +00:00
|
|
|
"ForceObjectSelect",
|
|
|
|
|
Block("Header", ResetList=False),
|
|
|
|
|
Block("Data", LocalID=obj.LocalID),
|
|
|
|
|
direction=Direction.IN,
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
def _teleport_to_object(self, session: Session, obj: Object):
|
2021-12-09 05:30:12 +00:00
|
|
|
session.main_region.circuit.send(Message(
|
2021-04-30 17:30:24 +00:00
|
|
|
"TeleportLocationRequest",
|
|
|
|
|
Block("AgentData", AgentID=session.agent_id, SessionID=session.id),
|
|
|
|
|
Block(
|
|
|
|
|
"Info",
|
|
|
|
|
RegionHandle=session.main_region.handle,
|
|
|
|
|
Position=obj.RegionPosition,
|
|
|
|
|
LookAt=Vector3(0.0, 0.0, 0.0)
|
|
|
|
|
),
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
def _scan_all_objects(self, _session: Session, region: ProxiedRegion):
|
|
|
|
|
self.blueish_model.clear()
|
|
|
|
|
|
|
|
|
|
for obj in region.objects.all_objects:
|
|
|
|
|
if _is_object_blueish(obj):
|
|
|
|
|
self.blueish_model.addObject(obj)
|
|
|
|
|
|
|
|
|
|
obj_list = self.blueish_model.objects
|
|
|
|
|
region.objects.request_object_properties([o for o in obj_list if o.Name is None])
|
|
|
|
|
|
|
|
|
|
# Make sure we request any objects we didn't know about before,
|
|
|
|
|
# they'll get picked up in the update handler.
|
|
|
|
|
region.objects.request_missing_objects()
|
|
|
|
|
|
|
|
|
|
def handle_object_updated(self, session: Session, region: ProxiedRegion,
|
2023-06-18 18:29:51 +00:00
|
|
|
obj: Object, updated_props: Set[str], msg: Optional[Message]):
|
2021-04-30 17:30:24 +00:00
|
|
|
if self.blueish_model is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if _is_object_blueish(obj):
|
|
|
|
|
if obj not in self.blueish_model:
|
|
|
|
|
if obj.Name is None:
|
|
|
|
|
region.objects.request_object_properties(obj)
|
|
|
|
|
self.blueish_model.addObject(obj)
|
|
|
|
|
else:
|
|
|
|
|
# mark the object as updated in the model,
|
|
|
|
|
# fields may have changed.
|
|
|
|
|
self.blueish_model.updateObject(obj)
|
|
|
|
|
else:
|
|
|
|
|
if obj in self.blueish_model:
|
|
|
|
|
self.blueish_model.removeObject(obj)
|
|
|
|
|
|
|
|
|
|
def handle_object_killed(self, session: Session, region: ProxiedRegion, obj: Object):
|
|
|
|
|
if self.blueish_model is None:
|
|
|
|
|
return
|
|
|
|
|
if obj in self.blueish_model:
|
|
|
|
|
self.blueish_model.removeObject(obj)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BlueishModelHeader(enum.IntEnum):
|
|
|
|
|
Name = 0
|
|
|
|
|
Position = enum.auto()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BlueishObjectModel(QtCore.QAbstractTableModel):
|
|
|
|
|
def __init__(self, parent):
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
self.objects: List[Object] = []
|
|
|
|
|
|
|
|
|
|
def __contains__(self, item):
|
|
|
|
|
return item in self.objects
|
|
|
|
|
|
|
|
|
|
def addObject(self, obj: Object):
|
|
|
|
|
if obj in self.objects:
|
|
|
|
|
self.updateObject(obj)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
num_objs = len(self.objects)
|
|
|
|
|
self.beginInsertRows(QtCore.QModelIndex(), num_objs, num_objs)
|
|
|
|
|
self.objects.append(obj)
|
|
|
|
|
self.endInsertRows()
|
|
|
|
|
|
|
|
|
|
def removeObject(self, obj: Object):
|
|
|
|
|
try:
|
|
|
|
|
obj_idx = self.objects.index(obj)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.beginRemoveRows(QtCore.QModelIndex(), obj_idx, obj_idx)
|
|
|
|
|
self.objects.remove(obj)
|
|
|
|
|
self.endRemoveRows()
|
|
|
|
|
|
|
|
|
|
def updateObject(self, obj: Object):
|
|
|
|
|
try:
|
|
|
|
|
obj_idx = self.objects.index(obj)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return
|
|
|
|
|
top_left = self.createIndex(obj_idx, 1)
|
|
|
|
|
bottom_right = self.createIndex(obj_idx, self.columnCount())
|
|
|
|
|
self.dataChanged.emit(top_left, bottom_right)
|
|
|
|
|
|
|
|
|
|
def rowCount(self, parent=None, *args, **kwargs):
|
|
|
|
|
return len(self.objects)
|
|
|
|
|
|
|
|
|
|
def columnCount(self, parent: QtCore.QModelIndex = None) -> int:
|
|
|
|
|
return len(BlueishModelHeader)
|
|
|
|
|
|
|
|
|
|
def data(self, index, role=None):
|
|
|
|
|
if not index.isValid():
|
|
|
|
|
return None
|
|
|
|
|
obj = self.objects[index.row()]
|
|
|
|
|
if role == QtCore.Qt.UserRole:
|
|
|
|
|
return obj
|
|
|
|
|
if role != QtCore.Qt.DisplayRole:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
col = index.column()
|
|
|
|
|
val = None
|
|
|
|
|
|
|
|
|
|
if col == BlueishModelHeader.Name:
|
|
|
|
|
val = obj.Name or ""
|
|
|
|
|
elif col == BlueishModelHeader.Position:
|
|
|
|
|
try:
|
|
|
|
|
val = str(obj.RegionPosition)
|
|
|
|
|
except ValueError:
|
|
|
|
|
# If the object is orphaned we may not be able to figure
|
|
|
|
|
# out the region pos
|
|
|
|
|
val = "Unknown"
|
|
|
|
|
|
|
|
|
|
return val
|
|
|
|
|
|
|
|
|
|
def headerData(self, col, orientation, role=None):
|
|
|
|
|
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
|
|
|
|
|
return BlueishModelHeader(col).name
|
|
|
|
|
|
|
|
|
|
def clear(self):
|
|
|
|
|
self.beginResetModel()
|
|
|
|
|
self.objects = []
|
|
|
|
|
self.endResetModel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BLUEISH_UI_PATH = os.path.join(os.path.dirname(__file__), "blueish_object_list.ui")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BlueishObjectWindow(QtWidgets.QMainWindow):
|
|
|
|
|
objectHighlightClicked = QtCore.Signal(Session, Object)
|
|
|
|
|
objectTeleportClicked = QtCore.Signal(Session, Object)
|
|
|
|
|
|
|
|
|
|
tableView: QtWidgets.QTableView
|
|
|
|
|
|
|
|
|
|
def __init__(self, parent, session: Session):
|
|
|
|
|
self.closing = asyncio.Future()
|
|
|
|
|
super().__init__(parent=parent)
|
|
|
|
|
loadUi(BLUEISH_UI_PATH, self)
|
|
|
|
|
self.model = BlueishObjectModel(self)
|
|
|
|
|
self.session = session
|
|
|
|
|
self.tableView.setModel(self.model)
|
|
|
|
|
self.tableView.horizontalHeader().resizeSection(BlueishModelHeader.Name, 150)
|
|
|
|
|
self.tableView.horizontalHeader().setStretchLastSection(True)
|
|
|
|
|
self.buttonHighlight.clicked.connect(self._highlightClicked)
|
|
|
|
|
self.buttonTeleport.clicked.connect(self._teleportClicked)
|
|
|
|
|
|
|
|
|
|
def closeEvent(self, event: QtGui.QCloseEvent):
|
|
|
|
|
if not self.closing.done():
|
|
|
|
|
self.closing.set_result(True)
|
|
|
|
|
super().closeEvent(event)
|
|
|
|
|
|
|
|
|
|
def _highlightClicked(self):
|
|
|
|
|
self._emitForSelectedObject(self.objectHighlightClicked)
|
|
|
|
|
|
|
|
|
|
def _teleportClicked(self):
|
|
|
|
|
self._emitForSelectedObject(self.objectTeleportClicked)
|
|
|
|
|
|
|
|
|
|
def _emitForSelectedObject(self, signal: QtCore.Signal):
|
|
|
|
|
object_indexes = self.tableView.selectionModel().selectedIndexes()
|
|
|
|
|
if not object_indexes:
|
|
|
|
|
return
|
|
|
|
|
obj = object_indexes[0].data(QtCore.Qt.UserRole)
|
|
|
|
|
signal.emit(self.session, obj) # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addons = [BlueishObjectListGUIAddon()]
|