212 lines
8.9 KiB
Python
212 lines
8.9 KiB
Python
import asyncio
|
|
import dataclasses
|
|
from typing import *
|
|
|
|
import numpy as np
|
|
|
|
from hippolyzer.lib.base.datatypes import UUID, Vector3, Vector2
|
|
from hippolyzer.lib.base.message.message import Message, Block
|
|
from hippolyzer.lib.base.templates import ParcelGridFlags, ParcelFlags
|
|
from hippolyzer.lib.client.state import BaseClientRegion
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Parcel:
|
|
local_id: int
|
|
name: str
|
|
flags: ParcelFlags
|
|
group_id: UUID
|
|
# TODO: More properties
|
|
|
|
|
|
class ParcelManager:
|
|
# We expect to receive this number of ParcelOverlay messages
|
|
NUM_CHUNKS = 4
|
|
# No, we don't support varregion or whatever.
|
|
REGION_SIZE = 256
|
|
# Basically, the minimum parcel size is 4 on either axis so each "point" in the
|
|
# ParcelOverlay represents an area this size
|
|
GRID_STEP = 4
|
|
GRIDS_PER_EDGE = REGION_SIZE // GRID_STEP
|
|
|
|
def __init__(self, region: BaseClientRegion):
|
|
# dimensions are south to north, west to east
|
|
self.overlay = np.zeros((self.GRIDS_PER_EDGE, self.GRIDS_PER_EDGE), dtype=np.uint8)
|
|
# 1-indexed parcel list index
|
|
self.parcel_indices = np.zeros((self.GRIDS_PER_EDGE, self.GRIDS_PER_EDGE), dtype=np.uint16)
|
|
self.parcels: List[Optional[Parcel]] = []
|
|
self.overlay_chunks: List[Optional[bytes]] = [None] * self.NUM_CHUNKS
|
|
self.overlay_complete = asyncio.Event()
|
|
self.parcels_downloaded = asyncio.Event()
|
|
self._parcels_dirty: bool = True
|
|
self._region = region
|
|
self._next_seq = 1
|
|
self._region.message_handler.subscribe("ParcelOverlay", self._handle_parcel_overlay)
|
|
|
|
def _handle_parcel_overlay(self, message: Message):
|
|
self.add_overlay_chunk(message["ParcelData"]["Data"], message["ParcelData"]["SequenceID"])
|
|
|
|
def add_overlay_chunk(self, chunk: bytes, chunk_num: int) -> bool:
|
|
self.overlay_chunks[chunk_num] = chunk
|
|
# Still have some pending chunks, don't try to parse this yet
|
|
if not all(self.overlay_chunks):
|
|
return False
|
|
|
|
new_overlay_data = b"".join(self.overlay_chunks)
|
|
self.overlay_chunks = [None] * self.NUM_CHUNKS
|
|
self._parcels_dirty = False
|
|
if new_overlay_data != self.overlay.data[:]:
|
|
# If the raw data doesn't match, then we have to parse again
|
|
self.overlay.data = new_overlay_data
|
|
self._parse_overlay()
|
|
# We could optimize this by just marking specific squares dirty
|
|
# if the parcel indices have changed between parses, but I don't care
|
|
# to do that.
|
|
self._parcels_dirty = True
|
|
self.parcels_downloaded.clear()
|
|
if not self.overlay_complete.is_set():
|
|
self.overlay_complete.set()
|
|
return True
|
|
|
|
@classmethod
|
|
def _pos_to_grid_coords(cls, pos: Vector3) -> Tuple[int, int]:
|
|
return round(pos.Y // cls.GRID_STEP), round(pos.X // cls.GRID_STEP)
|
|
|
|
def _parse_overlay(self):
|
|
# Zero out all parcel indices
|
|
self.parcel_indices[:, :] = 0
|
|
next_parcel_idx = 1
|
|
for y in range(0, self.GRIDS_PER_EDGE):
|
|
for x in range(0, self.GRIDS_PER_EDGE):
|
|
# We already have a parcel index for this grid, continue
|
|
if self.parcel_indices[y, x]:
|
|
continue
|
|
|
|
# Fill all adjacent grids with this parcel index
|
|
self._flood_fill_parcel_index(y, x, next_parcel_idx)
|
|
# SL doesn't allow disjoint grids to be part of the same parcel, so
|
|
# whatever grid we find next without a parcel index must be a new parcel
|
|
next_parcel_idx += 1
|
|
|
|
# Should have found at least one parcel
|
|
assert next_parcel_idx >= 2
|
|
|
|
# Have a different number of parcels now, we can't use the existing parcel objects
|
|
# because it's unlikely that just parcel boundaries have changed.
|
|
if len(self.parcels) != next_parcel_idx - 1:
|
|
# We don't know about any of these parcels yet, fill with none
|
|
self.parcels = [None] * (next_parcel_idx - 1)
|
|
|
|
def _flood_fill_parcel_index(self, start_y, start_x, parcel_idx):
|
|
"""Flood fill all neighboring grids with the parcel index, being mindful of parcel boundaries"""
|
|
# We know the start grid is assigned to this parcel index
|
|
self.parcel_indices[start_y, start_x] = parcel_idx
|
|
# Queue of grids to test the neighbors of, start with the start grid.
|
|
neighbor_test_queue: List[Tuple[int, int]] = [(start_y, start_x)]
|
|
|
|
while neighbor_test_queue:
|
|
to_test = neighbor_test_queue.pop(0)
|
|
test_grid = self.overlay[to_test]
|
|
|
|
for direction in ((-1, 0), (1, 0), (0, -1), (0, 1)):
|
|
new_pos = to_test[0] + direction[0], to_test[1] + direction[1]
|
|
|
|
if any(x < 0 or x >= self.GRIDS_PER_EDGE for x in new_pos):
|
|
# Outside bounds
|
|
continue
|
|
if self.parcel_indices[new_pos]:
|
|
# Already set, skip
|
|
continue
|
|
|
|
if direction[0] == -1 and test_grid & ParcelGridFlags.SOUTH_LINE:
|
|
# Test grid is already on a south line, can't go south.
|
|
continue
|
|
if direction[1] == -1 and test_grid & ParcelGridFlags.WEST_LINE:
|
|
# Test grid is already on a west line, can't go west.
|
|
continue
|
|
|
|
grid = self.overlay[new_pos]
|
|
|
|
if direction[0] == 1 and grid & ParcelGridFlags.SOUTH_LINE:
|
|
# Hit a south line going north, this is outside the current parcel
|
|
continue
|
|
if direction[1] == 1 and grid & ParcelGridFlags.WEST_LINE:
|
|
# Hit a west line going east, this is outside the current parcel
|
|
continue
|
|
# This grid is within the current parcel, set the parcel index
|
|
self.parcel_indices[new_pos] = parcel_idx
|
|
# Append the grid to the neighbour testing queue
|
|
neighbor_test_queue.append(new_pos)
|
|
|
|
async def request_dirty_parcels(self) -> Tuple[Parcel, ...]:
|
|
if self._parcels_dirty:
|
|
return await self.request_all_parcels()
|
|
return tuple(self.parcels)
|
|
|
|
async def request_all_parcels(self) -> Tuple[Parcel, ...]:
|
|
await self.overlay_complete.wait()
|
|
# Because of how we build up the parcel index map, it's safe for us to
|
|
# do this instead of keeping track of seen IDs in a set or similar
|
|
last_seen_parcel_index = 0
|
|
futs = []
|
|
for y in range(0, self.GRIDS_PER_EDGE):
|
|
for x in range(0, self.GRIDS_PER_EDGE):
|
|
parcel_index = self.parcel_indices[y, x]
|
|
assert parcel_index != 0
|
|
if parcel_index <= last_seen_parcel_index:
|
|
continue
|
|
assert parcel_index == last_seen_parcel_index + 1
|
|
last_seen_parcel_index = parcel_index
|
|
# Request a position within the parcel
|
|
futs.append(self.request_parcel_properties(
|
|
Vector2(x * self.GRID_STEP + 1.0, y * self.GRID_STEP + 1.0)
|
|
))
|
|
|
|
# Wait for all parcel properties to come in
|
|
await asyncio.gather(*futs)
|
|
self.parcels_downloaded.set()
|
|
self._parcels_dirty = False
|
|
return tuple(self.parcels)
|
|
|
|
async def request_parcel_properties(self, pos: Vector2) -> Parcel:
|
|
await self.overlay_complete.wait()
|
|
seq_id = self._next_seq
|
|
# Register a wait on a ParcelProperties matching this seq
|
|
parcel_props_fut = self._region.message_handler.wait_for(
|
|
("ParcelProperties",),
|
|
predicate=lambda msg: msg["ParcelData"]["SequenceID"] == seq_id,
|
|
timeout=10.0,
|
|
)
|
|
# We don't care about when we receive an ack, we only care about when we receive the parcel props
|
|
_ = self._region.circuit.send_reliable(Message(
|
|
"ParcelPropertiesRequest",
|
|
Block("AgentData", AgentID=self._region.session().agent_id, SessionID=self._region.session().id),
|
|
Block(
|
|
"ParcelData",
|
|
SequenceID=seq_id,
|
|
West=pos.X,
|
|
East=pos.X,
|
|
North=pos.Y,
|
|
South=pos.Y,
|
|
# What does this even mean?
|
|
SnapSelection=0,
|
|
),
|
|
))
|
|
self._next_seq += 1
|
|
|
|
parcel_props = await parcel_props_fut
|
|
data_block = parcel_props["ParcelData"][0]
|
|
# Parcel indices are one-indexed, convert to zero-indexed.
|
|
parcel_idx = self.parcel_indices[self._pos_to_grid_coords(pos)] - 1
|
|
assert len(self.parcels) > parcel_idx
|
|
|
|
self.parcels[parcel_idx] = parcel = Parcel(
|
|
local_id=data_block["LocalID"],
|
|
name=data_block["Name"],
|
|
flags=ParcelFlags(data_block["ParcelFlags"]),
|
|
group_id=data_block["GroupID"],
|
|
# Parcel UUID isn't in this response :/
|
|
)
|
|
|
|
return parcel
|