Add grid perspective support to API for FOV-aware entity filtering
When a Grid has a perspective entity set (typically the player), the API now respects field-of-view by default. Only entities visible to the perspective entity are returned in /scene responses. Changes: - serialize_grid() filters entities using grid.is_in_fov() - Added ?omniscient=true query param to bypass FOV filtering - Response includes perspective info with hidden_entities count - Updated README with perspective documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff46043023
commit
96c66decba
3 changed files with 109 additions and 17 deletions
|
|
@ -30,14 +30,44 @@ Returns the current scene graph with all UI elements.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8765/scene
|
curl http://localhost:8765/scene
|
||||||
|
|
||||||
|
# Omniscient mode (see all entities, ignoring FOV)
|
||||||
|
curl "http://localhost:8765/scene?omniscient=true"
|
||||||
```
|
```
|
||||||
|
|
||||||
Response includes:
|
**Query Parameters:**
|
||||||
|
- `omniscient`: If `true`, show all entities regardless of FOV. Default is `false` (respect perspective).
|
||||||
|
|
||||||
|
**Response includes:**
|
||||||
- Scene name
|
- Scene name
|
||||||
- Viewport dimensions
|
- Viewport dimensions
|
||||||
- All UI elements with type, bounds, visibility, interactivity
|
- All UI elements with type, bounds, visibility, interactivity
|
||||||
- Nested children
|
- Nested children
|
||||||
- Type-specific properties (text for Caption, grid_size for Grid, etc.)
|
- Type-specific properties (text for Caption, grid_size for Grid, etc.)
|
||||||
|
- `perspective_mode`: Either "respect_fov" or "omniscient"
|
||||||
|
|
||||||
|
**Grid Perspective Support:**
|
||||||
|
|
||||||
|
When a Grid has a `perspective` entity set (typically the player), the API respects
|
||||||
|
field-of-view by default. Only entities visible to the perspective entity are returned.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "Grid",
|
||||||
|
"entities": [...], // Only entities in FOV
|
||||||
|
"perspective": {
|
||||||
|
"enabled": true,
|
||||||
|
"entity_name": "player",
|
||||||
|
"entity_pos": {"x": 5, "y": 5},
|
||||||
|
"fov_radius": 10,
|
||||||
|
"hidden_entities": 3,
|
||||||
|
"note": "Entities filtered by FOV - only showing what perspective entity can see"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `?omniscient=true` to bypass FOV filtering and see all entities (useful for
|
||||||
|
debugging or when partial information isn't needed).
|
||||||
|
|
||||||
### GET /affordances
|
### GET /affordances
|
||||||
Returns only interactive elements with semantic labels.
|
Returns only interactive elements with semantic labels.
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,17 @@ def serialize_entity(entity) -> Dict[str, Any]:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def serialize_grid(grid) -> Dict[str, Any]:
|
def serialize_grid(grid, respect_perspective: bool = True) -> Dict[str, Any]:
|
||||||
"""Serialize a Grid element with its entities."""
|
"""Serialize a Grid element with its entities.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid: The Grid element to serialize
|
||||||
|
respect_perspective: If True and grid has a perspective entity,
|
||||||
|
only include entities visible to that entity's FOV
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary representation of the grid
|
||||||
|
"""
|
||||||
bounds = get_bounds(grid)
|
bounds = get_bounds(grid)
|
||||||
|
|
||||||
# Get grid dimensions
|
# Get grid dimensions
|
||||||
|
|
@ -106,10 +115,27 @@ def serialize_grid(grid) -> Dict[str, Any]:
|
||||||
center = getattr(grid, 'center', None)
|
center = getattr(grid, 'center', None)
|
||||||
zoom = float(getattr(grid, 'zoom', 1.0))
|
zoom = float(getattr(grid, 'zoom', 1.0))
|
||||||
|
|
||||||
# Serialize entities
|
# Check for perspective (player POV)
|
||||||
|
perspective_entity = getattr(grid, 'perspective', None)
|
||||||
|
perspective_enabled = bool(getattr(grid, 'perspective_enabled', False))
|
||||||
|
has_perspective = perspective_entity is not None and perspective_enabled
|
||||||
|
|
||||||
|
# Serialize entities, optionally filtering by FOV
|
||||||
entities = []
|
entities = []
|
||||||
|
hidden_count = 0
|
||||||
try:
|
try:
|
||||||
for entity in grid.entities:
|
for entity in grid.entities:
|
||||||
|
# Check if entity is visible from perspective
|
||||||
|
if has_perspective and respect_perspective:
|
||||||
|
try:
|
||||||
|
entity_pos = (int(entity.grid_x), int(entity.grid_y))
|
||||||
|
in_fov = grid.is_in_fov(entity_pos)
|
||||||
|
if not in_fov:
|
||||||
|
hidden_count += 1
|
||||||
|
continue # Skip entities not in FOV
|
||||||
|
except Exception:
|
||||||
|
pass # If FOV check fails, include the entity
|
||||||
|
|
||||||
entities.append(serialize_entity(entity))
|
entities.append(serialize_entity(entity))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -129,23 +155,42 @@ def serialize_grid(grid) -> Dict[str, Any]:
|
||||||
"has_cell_enter": getattr(grid, 'on_cell_enter', None) is not None,
|
"has_cell_enter": getattr(grid, 'on_cell_enter', None) is not None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add perspective info
|
||||||
|
if has_perspective:
|
||||||
|
data["perspective"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"entity_name": getattr(perspective_entity, 'name', None) or "",
|
||||||
|
"entity_pos": {
|
||||||
|
"x": float(getattr(perspective_entity, 'grid_x', 0)),
|
||||||
|
"y": float(getattr(perspective_entity, 'grid_y', 0))
|
||||||
|
},
|
||||||
|
"fov_radius": int(getattr(grid, 'fov_radius', 0)),
|
||||||
|
"hidden_entities": hidden_count,
|
||||||
|
"note": "Entities filtered by FOV - only showing what perspective entity can see"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data["perspective"] = {
|
||||||
|
"enabled": False,
|
||||||
|
"note": "No perspective set - showing all entities (omniscient view)"
|
||||||
|
}
|
||||||
|
|
||||||
# Add cell size estimate if texture available
|
# Add cell size estimate if texture available
|
||||||
texture = getattr(grid, 'texture', None)
|
texture = getattr(grid, 'texture', None)
|
||||||
if texture:
|
if texture:
|
||||||
# Texture dimensions divided by sprite count would give cell size
|
|
||||||
# but this is an approximation
|
|
||||||
data["has_texture"] = True
|
data["has_texture"] = True
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str, Any]:
|
def serialize_element(element, depth: int = 0, max_depth: int = 10,
|
||||||
|
respect_perspective: bool = True) -> Dict[str, Any]:
|
||||||
"""Serialize a UI element to a dictionary.
|
"""Serialize a UI element to a dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
element: The UI element to serialize
|
element: The UI element to serialize
|
||||||
depth: Current recursion depth
|
depth: Current recursion depth
|
||||||
max_depth: Maximum recursion depth for children
|
max_depth: Maximum recursion depth for children
|
||||||
|
respect_perspective: If True, filter grid entities by perspective FOV
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary representation of the element
|
Dictionary representation of the element
|
||||||
|
|
@ -155,7 +200,7 @@ def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str,
|
||||||
|
|
||||||
# Special handling for Grid
|
# Special handling for Grid
|
||||||
if element_type == "Grid":
|
if element_type == "Grid":
|
||||||
return serialize_grid(element)
|
return serialize_grid(element, respect_perspective=respect_perspective)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"type": element_type,
|
"type": element_type,
|
||||||
|
|
@ -195,7 +240,8 @@ def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str,
|
||||||
children = []
|
children = []
|
||||||
try:
|
try:
|
||||||
for child in element.children:
|
for child in element.children:
|
||||||
children.append(serialize_element(child, depth + 1, max_depth))
|
children.append(serialize_element(child, depth + 1, max_depth,
|
||||||
|
respect_perspective))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
data["children"] = children
|
data["children"] = children
|
||||||
|
|
@ -204,9 +250,13 @@ def serialize_element(element, depth: int = 0, max_depth: int = 10) -> Dict[str,
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def serialize_scene() -> Dict[str, Any]:
|
def serialize_scene(respect_perspective: bool = True) -> Dict[str, Any]:
|
||||||
"""Serialize the entire current scene graph.
|
"""Serialize the entire current scene graph.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
respect_perspective: If True, filter grid entities by perspective FOV.
|
||||||
|
If False, show all entities (omniscient view).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with scene name, viewport info, and all elements
|
Dictionary with scene name, viewport info, and all elements
|
||||||
"""
|
"""
|
||||||
|
|
@ -224,7 +274,8 @@ def serialize_scene() -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
if scene:
|
if scene:
|
||||||
for element in scene.children:
|
for element in scene.children:
|
||||||
elements.append(serialize_element(element))
|
elements.append(serialize_element(element,
|
||||||
|
respect_perspective=respect_perspective))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -233,7 +284,8 @@ def serialize_scene() -> Dict[str, Any]:
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"viewport": {"width": width, "height": height},
|
"viewport": {"width": width, "height": height},
|
||||||
"element_count": len(elements),
|
"element_count": len(elements),
|
||||||
"elements": elements
|
"elements": elements,
|
||||||
|
"perspective_mode": "respect_fov" if respect_perspective else "omniscient"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class GameAPIHandler(BaseHTTPRequestHandler):
|
||||||
query = parse_qs(parsed.query)
|
query = parse_qs(parsed.query)
|
||||||
|
|
||||||
if path == '/scene':
|
if path == '/scene':
|
||||||
self.handle_scene()
|
self.handle_scene(query)
|
||||||
elif path == '/affordances':
|
elif path == '/affordances':
|
||||||
self.handle_affordances()
|
self.handle_affordances()
|
||||||
elif path == '/screenshot':
|
elif path == '/screenshot':
|
||||||
|
|
@ -145,16 +145,26 @@ class GameAPIHandler(BaseHTTPRequestHandler):
|
||||||
"timestamp": time.time()
|
"timestamp": time.time()
|
||||||
})
|
})
|
||||||
|
|
||||||
def handle_scene(self) -> None:
|
def handle_scene(self, query: Dict) -> None:
|
||||||
"""Return the current scene graph with semantic annotations."""
|
"""Return the current scene graph with semantic annotations.
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
omniscient: If 'true', show all entities regardless of FOV.
|
||||||
|
Default is to respect perspective (show only what
|
||||||
|
the perspective entity can see).
|
||||||
|
"""
|
||||||
|
# Check if client wants omniscient (all-seeing) view
|
||||||
|
omniscient = query.get('omniscient', ['false'])[0].lower() == 'true'
|
||||||
|
respect_perspective = not omniscient
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try to use lock for thread safety, but fall back if not available
|
# Try to use lock for thread safety, but fall back if not available
|
||||||
try:
|
try:
|
||||||
with mcrfpy.lock():
|
with mcrfpy.lock():
|
||||||
scene_data = serialize_scene()
|
scene_data = serialize_scene(respect_perspective=respect_perspective)
|
||||||
except (RuntimeError, AttributeError):
|
except (RuntimeError, AttributeError):
|
||||||
# Lock not available (e.g., main thread or headless mode issue)
|
# Lock not available (e.g., main thread or headless mode issue)
|
||||||
scene_data = serialize_scene()
|
scene_data = serialize_scene(respect_perspective=respect_perspective)
|
||||||
self.send_json(scene_data)
|
self.send_json(scene_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.send_error_json(f"Scene introspection failed: {str(e)}", 500)
|
self.send_error_json(f"Scene introspection failed: {str(e)}", 500)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue