# TECHNICAL INVESTIGATION & ARCHITECTURAL HANDOFF REPORT
## Milestone 1: Server Zone Gating, Spatial Grid Isolation & Player Spawning in Player Hideout

**Author**: `teamwork_preview_explorer` (M1 Entity Spatial Grid & Player Spawning Explorer)  
**Role**: Read-only Investigation, Architectural Strategy & Verification Formulation  
**Working Directory**: `c:\Projects\FreeExile\.agents\teamwork\explorer_m1_2`  
**Target Milestone**: Milestone 1: Server Zone Gating & Safe Haven Purity  
**Date**: 2026-09-30 (UTC)

---

## 1. Observation

### 1.1. Player Spawning Crash in `ZoneEngine`
- **File**: `server/world/zone_engine.py`
  - Lines 84-87 (`get_zone`):
    ```python
    def get_zone(self, zone_id: str) -> ZoneDefinition:
        if zone_id not in self.zones:
            raise KeyError(f"Zone '{zone_id}' not found in registry.")
        return self.zones[zone_id]
    ```
  - Lines 149-168 (`spawn_player`):
    ```python
    def spawn_player(
        self, player_id: str, zone_id: str, x: Optional[float] = None, y: Optional[float] = None
    ) -> PlayerLocation:
        zone = self.get_zone(zone_id)
        spawn_x = x if x is not None else zone.default_spawn_x
        spawn_y = y if y is not None else zone.default_spawn_y

        loc = PlayerLocation(
            player_id=player_id,
            zone_id=zone_id,
            x=spawn_x,
            y=spawn_y,
            z=0.0,
            instance_id=None,
            last_update_timestamp_ms=now_ms(),
        )
        self.player_locations[player_id] = loc
        self.get_unlocked_waypoints(player_id)
        return loc
    ```
  - Execution Verification:
    ```pwsh
    python -c "from server.world.zone_engine import ZoneEngine; ze = ZoneEngine(); ze.spawn_player('p1', 'zone_player_hideout')"
    ```
    Output:
    ```
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "C:\Projects\FreeExile\server\world\zone_engine.py", line 152, in spawn_player
        zone = self.get_zone(zone_id)
               ^^^^^^^^^^^^^^^^^^^^^^
      File "C:\Projects\FreeExile\server\world\zone_engine.py", line 86, in get_zone
        raise KeyError(f"Zone '{zone_id}' not found in registry.")
    KeyError: "Zone 'zone_player_hideout' not found in registry."
    ```
  - Direct Observation: `zone_player_hideout` is completely missing from `self.zones` and `self.zone_spatial_grids`.

### 1.2. Root Cause in Zone Catalog
- **File**: `server/world/zone_catalog.py`
  - Lines 29-497: `_register_zones(engine: ZoneEngine)` registers 10 zones:
    - 1 Sanctuary Hub: `zone_boundless_sanctuary` (Lines 31-98)
    - 9 Outer Open-World Zones: `zone_tang_kiem_nhai`, `zone_ancient_sword_barrow`, `zone_boundless_sandstorm`, `zone_blood_scale_ruins`, `zone_five_elements_altar`, `zone_abyssal_ice_pond`, `zone_infinite_blood_rift`, `zone_purgatory_lava_cavern`, `zone_boundless_celestial_palace` (Lines 100-497).
  - `zone_player_hideout` is never passed to `engine.add_zone()`.
  - Because `zone_player_hideout` was omitted, `zone_engine.py` line 111 had to use an ad-hoc hardcoded string check:
    ```python
    if zone_id == "zone_player_hideout":
        return False
    ```

### 1.3. Duplicate Method Declaration in `ZoneEngine`
- **File**: `server/world/zone_engine.py`
  - Lines 89-94:
    ```python
    def get_zone_definition(self, zone_id: str) -> ZoneDefinition:
        """Alias for get_zone for backward compatibility."""
        return self.get_zone(zone_id)

    def get_zone_definition(self, zone_id: str) -> Optional[ZoneDefinition]:
        return self.zones.get(zone_id)
    ```
  - The second declaration silently overrides the first in Python method resolution.

### 1.4. Spatial Grid Partitioning Architecture
- **File**: `server/world/zone_engine.py`
  - Line 70: `self.zone_spatial_grids: Dict[str, SpatialGrid] = {}`
  - Line 78: `self.zone_spatial_grids[zone.zone_id] = SpatialGrid(cell_size=64.0)`
- **File**: `server/world/spatial_grid.py`
  - Lines 20-25: Each `SpatialGrid` maintains its own `cells: Dict[Tuple[int, int], Set[int]]`, `entities: Dict[int, Entity]`, and `entity_cell_map`.
  - Lines 72-86: `get_entities_in_aoi(center_x, center_y, radius_cells=1)` inspects only local cells within the grid instance.
- Direct Observation: Spatial grids are keyed by `zone_id`. An entity in `zone_spatial_grids["zone_player_hideout"]` cannot be accessed, queried, or broadcasted to `zone_spatial_grids["zone_tang_kiem_nhai"]`.

### 1.5. Entity Querying & Safe Haven Purity
- **File**: `server/world/zone_engine.py`
  - Lines 106-129:
    - `can_spawn_hostile_monsters("zone_player_hideout")` returns `False`.
    - `validate_monster_spawn("zone_player_hideout", is_dummy=False)` returns `(False, "Nghiêm cấm sản sinh quái vật trong khu vực An Toàn (Sanctuary/Hideout: [zone_player_hideout]) theo chuẩn PoE2!")`.
    - `validate_monster_spawn("zone_player_hideout", is_dummy=True)` returns `(True, "Khôi Lỗi Luyện Võ (Target Dummy) được phép kích hoạt để thử nghiệm chiêu thức.")`.
- **File**: `client/webapp/js/engine/monster_system.js`
  - Lines 86-96 (`initZoneMonsters("zone_player_hideout")`):
    - Resets `activeMonsters.length = 0` (flushes previous zone entities).
    - Populates exclusively 1 entity: `dummy_primal_shrine` (`isDummy: true`, `isNeutral: true`, `maxHp: 65000`, `poise: 120`).
    - Line 230: In `updateMonstersTick(dt)`: `if (m.isDummy) continue;` skips all AI, pursuit, and attack logic.
  - Node.js sandbox execution test:
    - Querying `getActiveMonsters()` in `zone_player_hideout` returns exactly 1 entity (`dummy_primal_shrine`).
    - Filter for hostile entities `activeMonsters.filter(m => !m.isDummy)` returns length 0.
    - Simulating zone transition from `zone_tang_kiem_nhai` (4 hostile monsters) to `zone_player_hideout` flushes all 4 hostile monsters completely.

---

## 2. Logic Chain

1. **Premise 1**: PoE2 Safe Haven standards dictate that Player Hideouts (`zone_player_hideout`) and Town Hubs (`zone_boundless_sanctuary`) are strict non-combat safe zones. Only immortal training dummies (`isDummy: true`) are permitted.
2. **Premise 2**: In `ZoneEngine`, zone lookup via `get_zone(zone_id)` is a prerequisite for `spawn_player(player_id, zone_id)`.
3. **Inference from 1.1 & 1.2**: Because `zone_catalog.py` omitted `zone_player_hideout` from `_register_zones`, `ZoneEngine` does not have `zone_player_hideout` in `self.zones` or `self.zone_spatial_grids`. Therefore, calling `spawn_player(player_id, "zone_player_hideout")` raises `KeyError`.
4. **Resolution**: Registering `zone_player_hideout` as `ZoneDefinition` with `zone_type=ZoneType.SANCTUARY` inside `_register_zones` will:
   - Allow `get_zone("zone_player_hideout")` to return the `ZoneDefinition`.
   - Allow `spawn_player(player_id, "zone_player_hideout")` to return a valid `PlayerLocation` with `x=0.0, y=0.0, zone_id="zone_player_hideout"`.
   - Automatically initialize an isolated `SpatialGrid(cell_size=64.0)` in `self.zone_spatial_grids["zone_player_hideout"]`.
   - Enable `can_spawn_hostile_monsters("zone_player_hideout")` to return `False` purely by evaluating `zone.zone_type in (ZoneType.OPEN_WORLD, ZoneType.DUNGEON_INSTANCE, ZoneType.SECRET_CHAMBER)`, rendering the hardcoded string check in line 111 redundant.
5. **Inference from 1.4**: In `ZoneEngine.add_zone()`, each zone receives an isolated `SpatialGrid` instance. When an entity is placed in `zone_spatial_grids["zone_player_hideout"]`, its cell membership is stored solely in that grid's internal structures. Queries across outer map grids (`zone_tang_kiem_nhai`, etc.) query distinct dictionary objects and will never retrieve hideout entities or vice-versa.
6. **Inference from 1.5**: Both client (`monster_system.js`) and server (`zone_engine.py`) strictly enforce that `zone_player_hideout` contains zero hostile mobs. Gating validation rejects any non-dummy spawn, while client initialization flushes previous map entities and creates exclusively `dummy_primal_shrine`.

---

## 3. Caveats

1. **Server-Side Entity Attachment to Grids**: `ZoneEngine.spawn_player()` tracks `self.player_locations[player_id] = loc`, but does not automatically instantiate an `Entity(entity_id, x, y)` into `self.zone_spatial_grids[zone_id]`. This is intentional in FreeExile's architecture: `ZoneEngine` is the authoritative spatial/zoning registry, while `AuthoritativeGatewayService` (`server/gateway/authoritative_gateway_service.py`) and `ServerEngineLoop` (`server/world/server_engine_loop.py`) manage active runtime network entities.
2. **Client-Side Spawner Bypass (`spawnQAEnemy`)**: On the client, clicking QA debug buttons (`qa-spawn-pack` or `qa-spawn-boss`) in `monster_system.js:412` does not currently check `currentZoneId`. This is assigned to Milestone 2 (Feature #7: "Client Spawner Zone Gate") and does not invalidate Milestone 1 server gating.
3. **Target Dummy Telemetry & Immortality**: Enhancing the dummy's DPS/combo HUD and HP clamping is scoped under Milestone 2 (Features #5 and #6). In Milestone 1, the dummy already has `isDummy: true`, `isNeutral: true`, and zero hostile routines.

---

## 4. Conclusion & Actionable Technical Strategy

### 4.1. Technical Strategy for Downstream Worker

The downstream Worker for Milestone 1 must perform the following 3 discrete, surgical modifications:

#### Step 1: Register `zone_player_hideout` in `server/world/zone_catalog.py`
In `_register_zones(engine: ZoneEngine)` in `server/world/zone_catalog.py`, add the canonical definition:

```python
    # 2. Sanctuary: Player Hideout (Tiên Phủ Động Thiên - Sào Huyệt Cá Nhân)
    engine.add_zone(
        ZoneDefinition(
            zone_id="zone_player_hideout",
            name="Tiên Phủ Động Thiên (Hideout)",
            zone_type=ZoneType.SANCTUARY,
            environment=ZoneEnvironment.NORMAL,
            min_level=1,
            max_players=100,
            bounds_width=600.0,
            bounds_height=600.0,
            default_spawn_x=0.0,
            default_spawn_y=0.0,
            respawn_zone_id="zone_player_hideout",
            respawn_x=0.0,
            respawn_y=0.0,
            waypoints={
                "wp_hideout_central": Waypoint(
                    waypoint_id="wp_hideout_central",
                    name="Trụ Đá Thần Hành: Sào Huyệt",
                    zone_id="zone_player_hideout",
                    x=0.0,
                    y=0.0,
                    is_unlocked_by_default=True,
                )
            },
            portals={
                "portal_hideout_to_sanctuary": ZonePortal(
                    portal_id="portal_hideout_to_sanctuary",
                    name="Cổng Đến Doanh Trại Bến Lưu Đày",
                    source_zone_id="zone_player_hideout",
                    source_x=0.0,
                    source_y=-250.0,
                    target_zone_id="zone_boundless_sanctuary",
                    target_x=0.0,
                    target_y=0.0,
                    min_level=1,
                )
            },
            npc_ids=["npc_outcast_scavenger"],
        )
    )
```

Also add the reverse portal to `zone_boundless_sanctuary`'s `portals` dictionary:
```python
                "portal_sanctuary_to_hideout": ZonePortal(
                    portal_id="portal_sanctuary_to_hideout",
                    name="Cổng Vào Tiên Phủ Động Thiên",
                    source_zone_id="zone_boundless_sanctuary",
                    source_x=-200.0,
                    source_y=-200.0,
                    target_zone_id="zone_player_hideout",
                    target_x=0.0,
                    target_y=0.0,
                    min_level=1,
                ),
```

#### Step 2: Clean Duplicate Method in `server/world/zone_engine.py`
In `server/world/zone_engine.py`:
- Remove lines 89-91 (the redundant `def get_zone_definition(...) -> ZoneDefinition:`).
- Retain the clean method:
  ```python
  def get_zone_definition(self, zone_id: str) -> Optional[ZoneDefinition]:
      """Returns the ZoneDefinition for the given zone_id or None if not found."""
      return self.zones.get(zone_id)
  ```
- In `can_spawn_hostile_monsters(zone_id: str)`:
  Keep `if zone_id == "zone_player_hideout": return False` as defense-in-depth or simplify since `zone.zone_type == ZoneType.SANCTUARY` handles it.

#### Step 3: Expand Unit Tests in `tests/unit/test_zone_monster_spawning_rules.py`
Add the following test methods to `TestPoE2ZoneSpawningPurity`:
1. `test_hideout_zone_registration_and_type`:
   - `hideout = zone_engine.get_zone("zone_player_hideout")`
   - `assert hideout.zone_type == ZoneType.SANCTUARY`
   - `assert hideout.name == "Tiên Phủ Động Thiên (Hideout)"`
2. `test_hideout_player_spawning_and_bounds`:
   - `loc = zone_engine.spawn_player("player_test_01", "zone_player_hideout")`
   - `assert loc.zone_id == "zone_player_hideout"`
   - `assert loc.x == 0.0 and loc.y == 0.0`
   - `assert "wp_hideout_central" in zone_engine.get_unlocked_waypoints("player_test_01")`
   - `ok, _ = zone_engine.update_player_position("player_test_01", 150.0, -100.0)`
   - `assert ok is True`
   - `fail_ok, _ = zone_engine.update_player_position("player_test_01", 350.0, 0.0)` (exceeds half-width 300)
   - `assert fail_ok is False`
3. `test_spatial_grid_isolation_hideout_vs_wilderness`:
   - `h_grid = zone_engine.zone_spatial_grids["zone_player_hideout"]`
   - `w_grid = zone_engine.zone_spatial_grids["zone_tang_kiem_nhai"]`
   - `assert h_grid is not w_grid`
   - `h_grid.add_entity(Entity(entity_id=9001, x=3.2, y=1.4))` (dummy)
   - `w_grid.add_entity(Entity(entity_id=1001, x=3.2, y=1.4))` (hostile mob)
   - `assert 9001 in h_grid.get_entities_in_aoi(3.2, 1.4)`
   - `assert 1001 not in h_grid.get_entities_in_aoi(3.2, 1.4)`
   - `assert 1001 in w_grid.get_entities_in_aoi(3.2, 1.4)`
   - `assert 9001 not in w_grid.get_entities_in_aoi(3.2, 1.4)`
4. `test_hideout_monster_spawn_validation`:
   - `can_spawn, msg = zone_engine.validate_monster_spawn("zone_player_hideout", is_dummy=False)`
   - `assert can_spawn is False`
   - `can_dummy, _ = zone_engine.validate_monster_spawn("zone_player_hideout", is_dummy=True)`
   - `assert can_dummy is True`
5. `test_get_zone_definition_backward_compatibility`:
   - `assert zone_engine.get_zone_definition("zone_player_hideout") is not None`
   - `assert zone_engine.get_zone_definition("invalid_zone") is None`

---

## 5. Verification Method

To independently verify the implementation:

1. **Execute Unit Tests**:
   ```pwsh
   pytest tests/unit/test_zone_monster_spawning_rules.py -v
   ```
   *Expected Result*: All 10 tests pass in < 0.20s with 0 failures.

2. **Verify Related Subsystems**:
   ```pwsh
   pytest tests/unit/test_hideout_engine.py -v
   pytest tests/unit/test_spatial_grid.py -v
   pytest tests/unit/test_npc_system.py -v
   ```
   *Expected Result*: All pass cleanly, proving zero regression on hideout facilities, waypoints, and NPC interactions.

3. **Verify Code & Documentation Hygiene**:
   ```pwsh
   python tools/lint/check_code_and_doc_hygiene.py --strict
   ```
   *Expected Result*: `zone_engine.py` remains <= 450 lines (well under hard cap 500 lines); `zone_catalog.py` remains <= 610 lines (well under hard cap 1000 lines).

4. **Verify Spatial Isolation via Interactive Python**:
   ```python
   from server.world.zone_engine import ZoneEngine
   from server.world.spatial_grid import Entity
   ze = ZoneEngine()
   loc = ze.spawn_player("p1", "zone_player_hideout")
   assert loc.zone_id == "zone_player_hideout"
   h_grid = ze.zone_spatial_grids["zone_player_hideout"]
   w_grid = ze.zone_spatial_grids["zone_tang_kiem_nhai"]
   assert h_grid is not w_grid
   ```
