# map_*3 Active Object Overlay Notes

2026-06-23 정적 분석 기준.

## 결론

- `map_*3` CNS는 resource VM에서 surface slot `0x000c`로 로드/표시된다.
- active object의 `field0x28`은 draw-source selector로 확정한다.
  - high word: surface slot
  - low word: EXE source-rect/frame index
  - `0x000cNNNN`이면 `map_*3` slot `0x000c`의 `NNNN`번째 rect/frame을 의미한다.
- active object의 `+0xe8/+0xea/+0xe6/+0xe7`은 movement/contact consumer가 읽는 tile position/bounds 값으로 확정한다.
- common overlay renderer는 `field0x28`과 `object+0x1c/+0x20`을 사용해 화면 rect를 만든다.
- active opcode `0x64`는 tile 좌표를 `object+0x1c/+0x20` projected draw 좌표로 변환하는 명령으로 확인했다.
- 따라서 `+e8/+ea/+e6/+e7`만으로 “최종 draw placement”라고 부르면 안 된다. 현재 증거로는 active object bounds/hotspot이며, 최종 draw 좌표는 `+0x1c/+0x20` 경로가 더 직접적이다.
- 같은 `(field0x28, x, y, w, h)` initializer가 여러 번 반복되는 것은 실제 중복 배치라기보다, 건물/문 배치는 같고 NPC/이벤트만 다른 scene/resource variant에서 같은 active object 정의를 복제한 것으로 본다.
- 특히 문은 “항상 놓인 정적 오브젝트”만이 아니라, 시나리오 중 NPC가 문을 열고 들어간 뒤 다시 닫는 active object 상태 전환일 수 있다. 따라서 `opcode64` projection row의 `objectId/state`는 정적 배치 좌표뿐 아니라 문 열림/닫힘, NPC 통과, 오브젝트 상태 변경을 가리킬 가능성을 같이 둔다.

## 핵심 근거

### `field0x28` draw-source selector

`0x00416ce2`:

- `0x00416ceb..0x00416cf6`: `object+0x28 & 0xffff`를 low-word rect/frame selector로 사용.
- `0x00416cf9..0x00416d07`: `object+0x28 >> 16`을 surface slot으로 사용하고 `0x0055abd8[slot]` surface wrapper를 조회.
- `0x00416d11..0x00416d3d`: `object+0x1c/+0x20`을 projected draw x/y로 사용.

### dirty/occlusion coverage

`0x00424cd6`:

- `0x00424cfe..0x00424d0b`: `0x00416ce2(object, rectOut)` 호출.
- `0x00424d1d..0x00424df8`: pixel rect를 16px tile coverage로 변환.
- `0x00424e99..0x00425024`: `0x005957d0` dirty/occlusion byte buffer를 갱신.

### active object bounds/contact

`0x00431ca6` 계열:

- active object list `0x00574100..0x005741ec`를 순회.
- `object+0xe6/+0xe7`을 size-like bounds로 읽음.
- `object+0xe8/+0xea`을 tile-position-like bounds로 읽음.
- movement/contact overlap state를 갱신하므로, 이 값들은 단순 source rect가 아니다.

### tile-to-draw projection command

Active opcode table 기준:

- opcode `0x64` -> handler `0x00407d09`
- opcode `0x70` -> handler `0x004090d2` 계열. 순회 방식만 다르고, tile 좌표를 projected draw 좌표로 쓰는 핵심 계산은 동일하다.

`0x64` stream format:

```text
64 mode x_lo x_hi y_lo y_hi objectId state
```

핵심 동작:

- active object order를 `0x004576e8 / 0x00574538 / 0x0059dd70` 경로로 순회한다.
- `stream+6`의 object id가 `object+0x16`과 일치하거나 `0xff`이면 대상 object로 처리한다.
- mode가 `1`, `3`, `4`이면 x를 갱신한다.
  - `object+0x1c = ((stream_x - cameraX(0x004576dc)) * 16 + 24) << 16`
  - `object+0xe8 = stream_x`
- mode가 `2`, `3`, `4`이면 y를 갱신한다.
  - `object+0x20 = ((stream_y - cameraY(0x004576de)) * 16 + 8) << 16`
  - `object+0xea = stream_y`
  - `object+0x24`는 `object+0xe7` height를 반영한 y/bottom anchor 계열 값으로 갱신된다.

알려진 수동 좌표 중 일부는 이 opcode로 직접 잡힌다.

- `0x004917d8`: `64 03 0f 00 08 00 01 02` -> tile `(15,8)`, object id `1`, state `2`. `map1_02b`의 `map_b3` 문 위치와 좌표가 일치한다.
- `0x0043cde4`: `64 03 13 00 14 00 00 01` -> tile `(19,20)`.
- `0x004953b8`, `0x0049552c`: tile `(19,20)`, object id `1`, state `2`.

이 증거는 “active object를 tile 위치로 보내고 그 좌표로 화면에 그리는 명령”을 확정한다. 다만 모든 static `map_*3` initializer row가 어떤 map/root에서 이 command와 결합되는지는 아직 완전 자동 binding되지 않았다. 예를 들어 `(15,8)`에는 `field0x28=0x000c0000` initializer도 있고 opcode `0x64` projection command도 있지만, projection의 object id와 initializer의 `field0x16`이 모든 케이스에서 직접 맞지는 않으므로 source selector와 position command의 결합은 별도 증거층으로 둔다.

### direct live-layer write negative

`0x00407686` active opcode `0x58`:

- mode `0`: `0x00595af0`에 tile/layer word 직접 write.
- mode `1`: `0x0058d7d0`에 tile/layer word 직접 write.
- 알려진 `map_*3` 문 좌표에서는 이 opcode stream hit가 0개였다.
- 따라서 알려진 문/오브젝트는 live layer에 박아넣는 방식보다 active object overlay 경로로 보는 것이 타당하다.

## 남은 미확정

- 모든 active initializer row와 opcode `0x64` projection command row의 정확한 1:1 또는 1:N binding.
- 반복되는 active initializer row가 정확히 어느 map/resource root에 귀속되는지의 완전한 map binding.
- door/NPC/chest 같은 gameplay role 이름은 아직 source rect/bounds만으로 자동 확정하지 않는다.
- 같은 좌표의 `opcode64` projection command가 보이더라도, 그것이 “최초 배치”인지 “시나리오 중 이동/열림/닫힘/재투영”인지는 command stream의 선후 문맥이 더 필요하다.
