feat(M01): enforce coordinate conversion boundaries

Work-Package: M01-FND-COORD-BOUNDARY-GATE-001

Agent: sindo-main-codex
This commit is contained in:
2026-07-13 16:37:29 +04:00
parent c8e62acf4c
commit 47dec6c32c
13 changed files with 335 additions and 66 deletions
@@ -3,9 +3,12 @@ extends CharacterBody3D
const GEOSET_CONTROLLER_SCRIPT := preload("res://src/scenes/character/character_geoset_controller.gd")
const TEXTURE_COMPOSITOR_SCRIPT := preload("res://src/scenes/character/character_texture_compositor.gd")
const OUTFIT_RESOLVER_SCRIPT := preload("res://src/scenes/character/wow_character_outfit_resolver.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const TILE_SIZE := 533.33333
const CHUNK_SIZE := TILE_SIZE / 16.0
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
const UNIT_SIZE := CHUNK_SIZE / 8.0
@export var extracted_dir: String = "res://data/extracted"
@@ -62,8 +65,11 @@ func _ready() -> void:
_camera.current = true
_camera.far = 50000.0
if spawn_at_tile_center:
global_position.x = (float(spawn_tile_x) + 0.5) * TILE_SIZE
global_position.z = (float(spawn_tile_y) + 0.5) * TILE_SIZE
var spawn_tile = ADT_TILE_COORDINATE_SCRIPT.new(spawn_tile_x, spawn_tile_y)
var half_tile_size := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS * 0.5
var spawn_local = ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, global_position.y, half_tile_size)
var spawn_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(spawn_tile, spawn_local)
global_position = Vector3(spawn_position.x_units, spawn_position.y_units, spawn_position.z_units)
_load_character_visual()
var ground := _sample_ground_height(global_position)
if is_finite(ground):
@@ -324,16 +330,17 @@ func _apply_camera_transform() -> void:
func _sample_ground_height(world_pos: Vector3) -> float:
var tx := int(floor(world_pos.x / TILE_SIZE))
var ty := int(floor(world_pos.z / TILE_SIZE))
var data := _load_adt(tx, ty)
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_pos.x, world_pos.y, world_pos.z)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_world_position)
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
var data := _load_adt(tile_coordinate.tile_x, tile_coordinate.tile_y)
if data.is_empty():
return NAN
var tile_origin := _get_tile_origin(data)
var local := world_pos - tile_origin
var chunk_x := clampi(int(floor(local.x / CHUNK_SIZE)), 0, 15)
var chunk_y := clampi(int(floor(local.z / CHUNK_SIZE)), 0, 15)
var chunk_x := clampi(chunk_coordinate.chunk_x, 0, 15)
var chunk_y := clampi(chunk_coordinate.chunk_y, 0, 15)
var chunk := _find_chunk(data, chunk_x, chunk_y, tile_origin)
if chunk.is_empty():
return NAN
+15 -13
View File
@@ -2,9 +2,9 @@
extends Node
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const TILE_SIZE := 533.33333
const WOW_WORLD_CENTER := 17066.666
const LIGHT_COORD_SCALE := 36.0
const HALF_MINUTES_PER_DAY := 2880
@@ -701,28 +701,30 @@ func _get_target_wow_position() -> Vector3:
var world_pos := Vector3.ZERO
if _target:
world_pos = _target.global_position
return Vector3(
WOW_WORLD_CENTER - world_pos.z,
WOW_WORLD_CENTER - world_pos.x,
world_pos.y)
var canonical_position = COORDINATE_MAPPER_SCRIPT.godot_to_canonical(_typed_godot_position(world_pos))
return Vector3(canonical_position.x_yards, canonical_position.y_yards, canonical_position.z_yards)
func _get_target_area_id() -> int:
if not _target or map_name.is_empty():
return 0
var world_pos := _target.global_position
var tile_x := int(floor(world_pos.x / TILE_SIZE))
var tile_y := int(floor(world_pos.z / TILE_SIZE))
var local_x := world_pos.x - float(tile_x) * TILE_SIZE
var local_y := world_pos.z - float(tile_y) * TILE_SIZE
var chunk_x := clampi(int(floor(local_x / (TILE_SIZE / 16.0))), 0, 15)
var chunk_y := clampi(int(floor(local_y / (TILE_SIZE / 16.0))), 0, 15)
var areas := _load_adt_area_grid(tile_x, tile_y)
var typed_world_position = _typed_godot_position(world_pos)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
var local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_world_position)
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, local_position)
var areas := _load_adt_area_grid(tile_coordinate.tile_x, tile_coordinate.tile_y)
if areas.is_empty():
return 0
var chunk_x := clampi(chunk_coordinate.chunk_x, 0, 15)
var chunk_y := clampi(chunk_coordinate.chunk_y, 0, 15)
return int(areas[chunk_y * 16 + chunk_x])
func _typed_godot_position(world_position: Vector3):
return GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
func _load_adt_area_grid(tile_x: int, tile_y: int) -> PackedInt32Array:
var key := "%d_%d" % [tile_x, tile_y]
if _adt_area_cache.has(key):
+36 -25
View File
@@ -14,14 +14,17 @@ const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/l
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
const M2_MATERIAL_REFRESH_VERSION := 2
const WMO_MATERIAL_REFRESH_VERSION := 10
const TILE_SIZE := 533.33333
const CHUNK_SIZE := TILE_SIZE / 16.0
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
const ADT_CLIFFROCK_WORLD_YAW_OFFSET := PI
const ADT_WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
const QUALITY_CUSTOM := "Custom"
@@ -786,10 +789,9 @@ func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionar
var parts: PackedStringArray = key.split("_")
var tx: int = int(parts[0])
var ty: int = int(parts[1])
var tcx: float = (tx + 0.5) * TILE_SIZE
var tcz: float = (ty + 0.5) * TILE_SIZE
var dx: float = tcx - focus_pos.x
var dz: float = tcz - focus_pos.z
var tile_center := _tile_center_to_world(tx, ty)
var dx: float = tile_center.x - focus_pos.x
var dz: float = tile_center.z - focus_pos.z
_tile_load_queue.append({
"key": key,
"tx": tx,
@@ -2909,8 +2911,10 @@ func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]:
if threshold <= 0.0:
return result
var local_x := fposmod(focus_pos.x, TILE_SIZE) / TILE_SIZE
var local_z := fposmod(focus_pos.z, TILE_SIZE) / TILE_SIZE
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_focus_position)
var local_x: float = tile_local_position.east_yards / TILE_SIZE
var local_z: float = tile_local_position.south_yards / TILE_SIZE
var x_offsets: Array[int] = [0]
var z_offsets: Array[int] = [0]
@@ -3033,10 +3037,17 @@ func _is_tile_chunk_set_ready(state: Dictionary) -> bool:
func _tile_dist_sq(state: Dictionary, cam_pos: Vector3) -> float:
var min_x: float = float(state["tx"]) * TILE_SIZE
var max_x: float = min_x + TILE_SIZE
var min_z: float = float(state["ty"]) * TILE_SIZE
var max_z: float = min_z + TILE_SIZE
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(int(state["tx"]), int(state["ty"]))
var north_west_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
tile_coordinate,
ADT_TILE_LOCAL_POSITION_SCRIPT.new(0.0, 0.0, 0.0))
var south_east_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
tile_coordinate,
ADT_TILE_LOCAL_POSITION_SCRIPT.new(TILE_SIZE, 0.0, TILE_SIZE))
var min_x: float = north_west_position.x_units
var max_x: float = south_east_position.x_units
var min_z: float = north_west_position.z_units
var max_z: float = south_east_position.z_units
var dx := 0.0
if cam_pos.x < min_x:
@@ -5397,9 +5408,9 @@ func _position_camera_over_world() -> void:
if camera == null:
return
var center_tx: float = (_tile_min.x + _tile_max.x) * 0.5
var center_ty: float = (_tile_min.y + _tile_max.y) * 0.5
var center := _tile_center_to_world(center_tx, center_ty)
var minimum_tile_center := _tile_center_to_world(_tile_min.x, _tile_min.y)
var maximum_tile_center := _tile_center_to_world(_tile_max.x, _tile_max.y)
var center := (minimum_tile_center + maximum_tile_center) * 0.5
var tile_span: int = int(max(_tile_max.x - _tile_min.x + 1, _tile_max.y - _tile_min.y + 1))
var height: float = max(2000.0, float(tile_span) * TILE_SIZE * 0.18)
@@ -5408,19 +5419,19 @@ func _position_camera_over_world() -> void:
_camera_initialized = true
## Tile (tx, ty) NW corner sits at Godot (tx*TILE_SIZE, 0, ty*TILE_SIZE).
## This matches wow_to_godot output: chunk origins ≈ (TX*TILE_SIZE, h, TY*TILE_SIZE).
func _tile_center_to_world(tile_x: float, tile_y: float) -> Vector3:
return Vector3(
(tile_x + 0.5) * TILE_SIZE,
0.0,
(tile_y + 0.5) * TILE_SIZE)
func _tile_center_to_world(tile_x: int, tile_y: int) -> Vector3:
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(tile_x, tile_y)
var half_tile_size := TILE_SIZE * 0.5
var tile_center_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
tile_coordinate,
ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, 0.0, half_tile_size))
return Vector3(tile_center_position.x_units, tile_center_position.y_units, tile_center_position.z_units)
func _world_to_tile(world_pos: Vector3) -> Vector2i:
return Vector2i(
int(floor(world_pos.x / TILE_SIZE)),
int(floor(world_pos.z / TILE_SIZE)))
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_pos.x, world_pos.y, world_pos.z)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
func _tile_key(tx: int, ty: int) -> String: