архитектура и цели для агентов
This commit is contained in:
@@ -365,10 +365,11 @@ void ADTLoader::_parse_adt(const uint8_t *raw, size_t len, Dictionary &result) {
|
||||
adt_placement_rot_to_godot(dd[i].rot[0], dd[i].rot[1], dd[i].rot[2], rx, ry, rz);
|
||||
|
||||
Dictionary p;
|
||||
p["name_id"] = (int)dd[i].nameId;
|
||||
p["pos"] = Vector3(gx, gy, gz);
|
||||
p["rot"] = Vector3(rx, ry, rz);
|
||||
p["scale"] = dd[i].scale / 1024.f;
|
||||
p["name_id"] = (int)dd[i].nameId;
|
||||
p["unique_id"] = (int)dd[i].uniqueId;
|
||||
p["pos"] = Vector3(gx, gy, gz);
|
||||
p["rot"] = Vector3(rx, ry, rz);
|
||||
p["scale"] = dd[i].scale / 1024.f;
|
||||
m2_placements.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
namespace godot {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ADTLoader
|
||||
//
|
||||
// Loads WoW 3.3.5a ADT terrain tiles.
|
||||
@@ -32,13 +31,13 @@ namespace godot {
|
||||
// "wmo_names": PackedStringArray, # WMO filenames
|
||||
// "m2_placements": Array[Dictionary],
|
||||
// "wmo_placements": Array[Dictionary],
|
||||
// "chunks": Array[Dictionary], # 16×16 = 256 entries, row-major
|
||||
// "chunks": Array[Dictionary], # 16x16 = 256 entries, row-major
|
||||
// }
|
||||
//
|
||||
// Placement Dictionary (both M2 and WMO):
|
||||
// {
|
||||
// "name_id": int, # index into m2_names / wmo_names
|
||||
// "unique_id": int, # WMO only — MODF uniqueId (dedupe key across tiles)
|
||||
// "unique_id": int, # MDDF/MODF uniqueId (dedupe key across tiles)
|
||||
// "pos": Vector3, # Godot world coords
|
||||
// "rot": Vector3, # Euler angles (radians)
|
||||
// "scale": float, # M2 only (WMO always 1.0)
|
||||
@@ -52,7 +51,7 @@ namespace godot {
|
||||
// "index_x": int, # 0-15
|
||||
// "index_y": int, # 0-15
|
||||
// "origin": Vector3, # Godot world position of chunk origin
|
||||
// "heights": PackedFloat32Array, # 145 values (9×9 outer + 8×8 inner grid)
|
||||
// "heights": PackedFloat32Array, # 145 values (9x9 outer + 8x8 inner grid)
|
||||
// "normals": PackedVector3Array, # 145 normals
|
||||
// "holes": int, # hole bit mask (low 16 bits)
|
||||
// "layers": Array[Dictionary], # texture layer definitions
|
||||
@@ -66,7 +65,6 @@ namespace godot {
|
||||
// "effect_id": int,
|
||||
// "alpha_offset": int,
|
||||
// }
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
class ADTLoader : public RefCounted {
|
||||
GDCLASS(ADTLoader, RefCounted)
|
||||
|
||||
|
||||
@@ -389,6 +389,34 @@ static Vector4 decode_comp_quat(const uint16_t q[4]) {
|
||||
return wow_quat_to_godot(x, y, z, w);
|
||||
}
|
||||
|
||||
static Vector4 decode_comp_quat_raw(const uint16_t q[4]) {
|
||||
float x = ((float)q[0] - 32768.0f) / 32767.0f;
|
||||
float y = ((float)q[1] - 32768.0f) / 32767.0f;
|
||||
float z = ((float)q[2] - 32768.0f) / 32767.0f;
|
||||
float w = ((float)q[3] - 32768.0f) / 32767.0f;
|
||||
return normalize_quat(x, y, z, w);
|
||||
}
|
||||
|
||||
static Vector4 texture_quat_to_uv_rotation(const Vector4 &q) {
|
||||
float x = q.x;
|
||||
float y = q.y;
|
||||
float z = q.z;
|
||||
float w = q.w;
|
||||
return Vector4(
|
||||
1.0f - 2.0f * (y * y + z * z),
|
||||
2.0f * (x * y - z * w),
|
||||
2.0f * (x * y + z * w),
|
||||
1.0f - 2.0f * (x * x + z * z));
|
||||
}
|
||||
|
||||
static float texture_quat_z_angle(const Vector4 &q) {
|
||||
float x = q.x;
|
||||
float y = q.y;
|
||||
float z = q.z;
|
||||
float w = q.w;
|
||||
return std::atan2(2.0f * (w * z + x * y), 1.0f - 2.0f * (y * y + z * z));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static const T *track_sequence_array(const std::vector<uint8_t> &buf, const M2ArrayRef &outer, uint32_t sequence_index, uint32_t &count_out) {
|
||||
count_out = 0;
|
||||
@@ -448,6 +476,37 @@ static Vector2 first_vec3_track_xy_speed(const std::vector<uint8_t> &buf, const
|
||||
return Vector2(0.0f, 0.0f);
|
||||
}
|
||||
|
||||
static float angle_delta(float from, float to) {
|
||||
float delta = to - from;
|
||||
const float pi = 3.14159265358979323846f;
|
||||
while (delta > pi) delta -= pi * 2.0f;
|
||||
while (delta < -pi) delta += pi * 2.0f;
|
||||
return delta;
|
||||
}
|
||||
|
||||
static float first_quat_track_z_rotation_speed(const std::vector<uint8_t> &buf, const M2Track &track) {
|
||||
for (uint32_t i = 0; i < track.values.count && i < track.timestamps.count; ++i) {
|
||||
uint32_t time_count = 0;
|
||||
uint32_t value_count = 0;
|
||||
const uint32_t *times = track_sequence_array<uint32_t>(buf, track.timestamps, i, time_count);
|
||||
const M2CompQuat *values = track_sequence_array<M2CompQuat>(buf, track.values, i, value_count);
|
||||
uint32_t key_count = std::min(time_count, value_count);
|
||||
if (!times || !values || key_count < 2) {
|
||||
continue;
|
||||
}
|
||||
uint32_t first = 0;
|
||||
uint32_t last = key_count - 1;
|
||||
float duration = (float)(times[last] - times[first]) / 1000.0f;
|
||||
if (duration <= 0.0001f) {
|
||||
continue;
|
||||
}
|
||||
float first_angle = texture_quat_z_angle(decode_comp_quat_raw(values[first].v));
|
||||
float last_angle = texture_quat_z_angle(decode_comp_quat_raw(values[last].v));
|
||||
return angle_delta(first_angle, last_angle) / duration;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
static uint32_t sequence_activity_score(const std::vector<uint8_t> &buf, const M2CompBone *bones, uint32_t bone_count, uint32_t sequence_index) {
|
||||
if (!bones) return 0;
|
||||
uint32_t score = 0;
|
||||
@@ -704,18 +763,25 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
||||
for (uint32_t i = 0; i < hdr.nTexTransforms; ++i) {
|
||||
Vector2 translation(0.0f, 0.0f);
|
||||
Vector2 scale(1.0f, 1.0f);
|
||||
Vector4 rotation(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
const M2Float3 *trans = first_track_value<M2Float3>(buf, transform_arr[i].translationTrack);
|
||||
if (trans) {
|
||||
translation = Vector2(trans->v[0], trans->v[1]);
|
||||
}
|
||||
const M2CompQuat *rot = first_track_value<M2CompQuat>(buf, transform_arr[i].rotationTrack);
|
||||
if (rot) {
|
||||
rotation = texture_quat_to_uv_rotation(decode_comp_quat_raw(rot->v));
|
||||
}
|
||||
const M2Float3 *scl = first_track_value<M2Float3>(buf, transform_arr[i].scaleTrack);
|
||||
if (scl) {
|
||||
scale = Vector2(scl->v[0], scl->v[1]);
|
||||
}
|
||||
Dictionary t;
|
||||
t["translation"] = translation;
|
||||
t["rotation"] = rotation;
|
||||
t["scale"] = scale;
|
||||
t["translation_speed"] = first_vec3_track_xy_speed(buf, transform_arr[i].translationTrack);
|
||||
t["rotation_speed"] = first_quat_track_z_rotation_speed(buf, transform_arr[i].rotationTrack);
|
||||
texture_transforms.push_back(t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
[gd_scene format=3 uid="uid://drf5o036q4dn8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://yi6lawwjgocg" path="res://src/scenes/streaming/streaming_world_loader.gd" id="1_sisqv"]
|
||||
[ext_resource type="Script" uid="uid://ltn2ko5kxvo4" path="res://src/scenes/player/third_person_wow_controller.gd" id="2_puy8r"]
|
||||
[ext_resource type="Script" uid="uid://didjth34ut0v1" path="res://src/scenes/sky/wow_sky_controller.gd" id="3_43ha7"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
|
||||
radius = 0.45
|
||||
height = 2.1
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="PlayerMaterial_1"]
|
||||
albedo_color = Color(0.25, 0.42, 0.85, 1)
|
||||
roughness = 0.65
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="PlayerCapsuleMesh_1"]
|
||||
material = SubResource("PlayerMaterial_1")
|
||||
radius = 0.45
|
||||
height = 2.1
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_1"]
|
||||
sky_top_color = Color(0.13, 0.32, 0.62, 1)
|
||||
sky_horizon_color = Color(0.62, 0.76, 0.9, 1)
|
||||
sky_curve = 0.18
|
||||
sky_energy_multiplier = 1.15
|
||||
ground_bottom_color = Color(0.12, 0.17, 0.14, 1)
|
||||
ground_horizon_color = Color(0.42, 0.5, 0.38, 1)
|
||||
ground_curve = 0.12
|
||||
ground_energy_multiplier = 0.55
|
||||
sun_angle_max = 12.0
|
||||
sun_curve = 0.08
|
||||
|
||||
[sub_resource type="Sky" id="Sky_1"]
|
||||
sky_material = SubResource("ProceduralSkyMaterial_1")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_1"]
|
||||
background_mode = 2
|
||||
background_energy_multiplier = 0.9
|
||||
sky = SubResource("Sky_1")
|
||||
ambient_light_source = 3
|
||||
ambient_light_color = Color(0.72, 0.8, 0.88, 1)
|
||||
ambient_light_sky_contribution = 0.65
|
||||
ambient_light_energy = 0.75
|
||||
reflected_light_source = 2
|
||||
fog_enabled = true
|
||||
fog_mode = 1
|
||||
fog_light_color = Color(0.55, 0.66, 0.72, 1)
|
||||
fog_light_energy = 0.55
|
||||
fog_sun_scatter = 0.08
|
||||
fog_density = 0.00018
|
||||
fog_sky_affect = 0.35
|
||||
fog_depth_begin = 1200.0
|
||||
fog_depth_end = 5200.0
|
||||
|
||||
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
|
||||
script = ExtResource("1_sisqv")
|
||||
map_name = "Kalimdor"
|
||||
camera_path = NodePath("ThirdPersonPlayer/CameraPivot/Camera3D")
|
||||
quality_preset = "High"
|
||||
update_interval = 0.1
|
||||
tiles_per_tick = 1
|
||||
max_concurrent_tile_tasks = 1
|
||||
tile_lod_remove_ops_per_tick = 1
|
||||
m2_build_groups_per_tick = 1
|
||||
m2_multimesh_batch_size = 64
|
||||
m2_animated_denylist_patterns = PackedStringArray("gryphonroost")
|
||||
m2_animated_allowlist_patterns = PackedStringArray("creature/fish/", "creature/eagle/", "world/critter/")
|
||||
wmo_render_group_ops_per_tick = 16
|
||||
cached_tile_mesh_limit = 48
|
||||
terrain_quality_mesh_cache_limit = 48
|
||||
lod0_radius_chunks = 10
|
||||
lod1_radius_chunks = 20
|
||||
lod2_radius_chunks = 40
|
||||
lod2_tile_radius = 5
|
||||
prewarm_tile_margin = 3
|
||||
retain_tile_margin = 4
|
||||
full_lod_prewarm_tiles = 1.5
|
||||
boundary_prefetch_threshold = 0.42
|
||||
auto_position_camera = false
|
||||
enable_occlusion_culling = false
|
||||
terrain_full_quality_radius_tiles = 5
|
||||
terrain_control_splat_hide_fallback = true
|
||||
terrain_control_splat_radius_tiles = 5
|
||||
max_active_terrain_control_splat_tiles = 121
|
||||
terrain_control_splat_cache_finalize_ops_per_tick = 8
|
||||
terrain_splat_quality_enabled = false
|
||||
enable_water = true
|
||||
m2_tile_radius = 3
|
||||
wmo_tile_radius = 5
|
||||
m2_visibility_range = 1600.0
|
||||
wmo_visibility_range = 3600.0
|
||||
debug_streaming = true
|
||||
hitch_profiler_enabled = true
|
||||
|
||||
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
|
||||
script = ExtResource("2_puy8r")
|
||||
spawn_tile_x = 31
|
||||
spawn_tile_y = 49
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="ThirdPersonPlayer" unique_id=1297880621]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.05, 0)
|
||||
shape = SubResource("PlayerCapsuleShape_1")
|
||||
|
||||
[node name="Visual" type="MeshInstance3D" parent="ThirdPersonPlayer" unique_id=1028210492]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.05, 0)
|
||||
mesh = SubResource("PlayerCapsuleMesh_1")
|
||||
|
||||
[node name="CameraPivot" type="Node3D" parent="ThirdPersonPlayer" unique_id=499263249]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7, 0)
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="ThirdPersonPlayer/CameraPivot" unique_id=2142337971]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 8)
|
||||
current = true
|
||||
fov = 70.0
|
||||
far = 50000.0
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=1436804627]
|
||||
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 0, 0)
|
||||
light_color = Color(1, 0.91, 0.78, 1)
|
||||
light_energy = 1.25
|
||||
directional_shadow_mode = 1
|
||||
directional_shadow_fade_start = 0.85
|
||||
directional_shadow_max_distance = 2200.0
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=12906896]
|
||||
environment = SubResource("Environment_1")
|
||||
|
||||
[node name="WowSkyController" type="Node" parent="." unique_id=1473026848]
|
||||
script = ExtResource("3_43ha7")
|
||||
target_path = NodePath("../ThirdPersonPlayer")
|
||||
world_environment_path = NodePath("../WorldEnvironment")
|
||||
sun_path = NodePath("../Sun")
|
||||
@@ -12,11 +12,11 @@ const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_buil
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 4
|
||||
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 := 1
|
||||
const WMO_MATERIAL_REFRESH_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
|
||||
@@ -183,9 +183,11 @@ var _m2_group_result_mutex := Mutex.new()
|
||||
var _m2_group_result_queue: Array = []
|
||||
var _m2_build_jobs: Dictionary = {}
|
||||
var _m2_build_queue: Array = []
|
||||
var _m2_unique_registry: Dictionary = {}
|
||||
var _m2_mesh_cache: Dictionary = {}
|
||||
var _m2_mesh_load_requests: Dictionary = {}
|
||||
var _m2_mesh_finalize_queue: Array = []
|
||||
var _m2_runtime_rebuild_required_cache: Dictionary = {}
|
||||
var _m2_animation_load_requests: Dictionary = {}
|
||||
var _m2_animation_finalize_queue: Array = []
|
||||
var _wmo_build_jobs: Dictionary = {}
|
||||
@@ -2066,6 +2068,7 @@ func _wait_for_tile_tasks() -> void:
|
||||
ResourceLoader.load_threaded_get(path)
|
||||
_m2_mesh_load_requests.clear()
|
||||
_m2_mesh_finalize_queue.clear()
|
||||
_m2_runtime_rebuild_required_cache.clear()
|
||||
|
||||
for pending in _m2_animation_load_requests.values():
|
||||
var path: String = String(pending.get("path", ""))
|
||||
@@ -2219,6 +2222,8 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
|
||||
"m2_names": data.get("m2_names", PackedStringArray()),
|
||||
"m2_placements": data.get("m2_placements", []),
|
||||
"m2_built": false,
|
||||
"m2_unique_keys": [],
|
||||
"m2_skipped_unique_keys": [],
|
||||
"wmo_names": data.get("wmo_names", PackedStringArray()),
|
||||
"wmo_placements": data.get("wmo_placements", []),
|
||||
"wmo_refs": [],
|
||||
@@ -2295,6 +2300,8 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v
|
||||
"m2_names": baked_m2_names,
|
||||
"m2_placements": baked_m2_placements,
|
||||
"m2_built": false,
|
||||
"m2_unique_keys": [],
|
||||
"m2_skipped_unique_keys": [],
|
||||
"wmo_names": baked_wmo_names,
|
||||
"wmo_placements": baked_wmo_placements,
|
||||
"wmo_refs": [],
|
||||
@@ -2370,6 +2377,8 @@ func _finalize_loaded_streaming_tile(request: Dictionary, stream_tile: Resource)
|
||||
"m2_names": stream_m2_names,
|
||||
"m2_placements": stream_m2_placements,
|
||||
"m2_built": false,
|
||||
"m2_unique_keys": [],
|
||||
"m2_skipped_unique_keys": [],
|
||||
"wmo_names": stream_wmo_names,
|
||||
"wmo_placements": stream_wmo_placements,
|
||||
"wmo_refs": [],
|
||||
@@ -2658,6 +2667,7 @@ func _release_tile(key: String) -> void:
|
||||
|
||||
var state: Dictionary = _tile_states[key]
|
||||
_unregister_tile_wmos(state)
|
||||
_remove_tile_m2_assets(state)
|
||||
_terrain_upgrade_tasks.erase(key)
|
||||
_terrain_control_splat_cache_tasks.erase(key)
|
||||
_terrain_splat_cache_tasks.erase(key)
|
||||
@@ -2708,8 +2718,10 @@ func _clear_streamed_world() -> void:
|
||||
for key in _m2_build_jobs.keys():
|
||||
_cancel_m2_build_job(String(key))
|
||||
_m2_build_queue.clear()
|
||||
_m2_unique_registry.clear()
|
||||
_m2_mesh_load_requests.clear()
|
||||
_m2_mesh_finalize_queue.clear()
|
||||
_m2_runtime_rebuild_required_cache.clear()
|
||||
_m2_animation_load_requests.clear()
|
||||
_m2_animation_finalize_queue.clear()
|
||||
_wmo_build_jobs.clear()
|
||||
@@ -3192,12 +3204,17 @@ func _process_detail_asset_key(key: String) -> void:
|
||||
|
||||
if enable_m2_assets and _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius):
|
||||
if not bool(state.get("m2_built", false)):
|
||||
state["m2_built"] = _request_tile_m2_assets(
|
||||
var m2_request := _request_tile_m2_assets(
|
||||
tile_root as Node3D,
|
||||
String(state.get("key", "")),
|
||||
state.get("origin", Vector3.ZERO),
|
||||
state.get("m2_names", PackedStringArray()),
|
||||
state.get("m2_placements", []))
|
||||
state["m2_built"] = bool(m2_request.get("built", false))
|
||||
if m2_request.has("unique_keys"):
|
||||
state["m2_unique_keys"] = m2_request.get("unique_keys", [])
|
||||
if m2_request.has("skipped_unique_keys"):
|
||||
state["m2_skipped_unique_keys"] = m2_request.get("skipped_unique_keys", [])
|
||||
else:
|
||||
_remove_tile_m2_assets(state)
|
||||
state["m2_built"] = false
|
||||
@@ -3854,19 +3871,121 @@ func _request_tile_m2_assets(
|
||||
key: String,
|
||||
tile_origin: Vector3,
|
||||
m2_names: PackedStringArray,
|
||||
m2_placements: Array) -> bool:
|
||||
m2_placements: Array) -> Dictionary:
|
||||
var result := {
|
||||
"built": false,
|
||||
}
|
||||
if key.is_empty() or m2_names.is_empty() or m2_placements.is_empty():
|
||||
return false
|
||||
return result
|
||||
var existing_m2_root := tile_root.get_node_or_null("M2s")
|
||||
if existing_m2_root != null:
|
||||
return not existing_m2_root.is_queued_for_deletion()
|
||||
result["built"] = not existing_m2_root.is_queued_for_deletion()
|
||||
return result
|
||||
if _m2_group_tasks.has(key) or _m2_build_jobs.has(key):
|
||||
return false
|
||||
return result
|
||||
|
||||
var filtered := _reserve_tile_m2_placements(key, m2_placements)
|
||||
var filtered_placements: Array = filtered.get("placements", [])
|
||||
result["unique_keys"] = filtered.get("unique_keys", [])
|
||||
result["skipped_unique_keys"] = filtered.get("skipped_unique_keys", [])
|
||||
if filtered_placements.is_empty():
|
||||
result["built"] = true
|
||||
return result
|
||||
|
||||
var task_id: int = WorkerThreadPool.add_task(
|
||||
_group_tile_m2_task.bind(key, tile_origin, m2_names, m2_placements))
|
||||
_group_tile_m2_task.bind(key, tile_origin, m2_names, filtered_placements))
|
||||
_m2_group_tasks[key] = task_id
|
||||
return false
|
||||
return result
|
||||
|
||||
|
||||
func _reserve_tile_m2_placements(tile_key: String, m2_placements: Array) -> Dictionary:
|
||||
var filtered_placements: Array = []
|
||||
var unique_keys: Array = []
|
||||
var skipped_unique_keys: Array = []
|
||||
for index in m2_placements.size():
|
||||
var placement_variant = m2_placements[index]
|
||||
if not (placement_variant is Dictionary):
|
||||
continue
|
||||
var placement: Dictionary = placement_variant
|
||||
var unique_key := _m2_unique_key(placement)
|
||||
if unique_key.is_empty():
|
||||
filtered_placements.append(placement)
|
||||
continue
|
||||
if _m2_unique_registry.has(unique_key):
|
||||
var entry: Dictionary = _m2_unique_registry[unique_key]
|
||||
var owner_tile := String(entry.get("tile", ""))
|
||||
if owner_tile == tile_key and unique_keys.has(unique_key):
|
||||
continue
|
||||
if owner_tile != tile_key:
|
||||
if not skipped_unique_keys.has(unique_key):
|
||||
skipped_unique_keys.append(unique_key)
|
||||
continue
|
||||
_m2_unique_registry[unique_key] = {"tile": tile_key}
|
||||
if not unique_keys.has(unique_key):
|
||||
unique_keys.append(unique_key)
|
||||
filtered_placements.append(placement)
|
||||
return {
|
||||
"placements": filtered_placements,
|
||||
"unique_keys": unique_keys,
|
||||
"skipped_unique_keys": skipped_unique_keys,
|
||||
}
|
||||
|
||||
|
||||
func _m2_unique_key(placement: Dictionary) -> String:
|
||||
var uid: int = int(placement.get("unique_id", -1))
|
||||
if uid <= 0:
|
||||
return ""
|
||||
return "uid:%d" % uid
|
||||
|
||||
|
||||
func _release_tile_m2_unique_keys(state: Dictionary, notify_candidates: bool = true) -> void:
|
||||
var tile_key := String(state.get("key", ""))
|
||||
var unique_keys: Array = state.get("m2_unique_keys", [])
|
||||
if tile_key.is_empty() or unique_keys.is_empty():
|
||||
state["m2_unique_keys"] = []
|
||||
state["m2_skipped_unique_keys"] = []
|
||||
return
|
||||
|
||||
var released_keys: Array = []
|
||||
for unique_key_variant in unique_keys:
|
||||
var unique_key := String(unique_key_variant)
|
||||
if unique_key.is_empty() or not _m2_unique_registry.has(unique_key):
|
||||
continue
|
||||
var entry: Dictionary = _m2_unique_registry[unique_key]
|
||||
if String(entry.get("tile", "")) != tile_key:
|
||||
continue
|
||||
_m2_unique_registry.erase(unique_key)
|
||||
released_keys.append(unique_key)
|
||||
|
||||
state["m2_unique_keys"] = []
|
||||
state["m2_skipped_unique_keys"] = []
|
||||
if not notify_candidates:
|
||||
return
|
||||
for unique_key in released_keys:
|
||||
_queue_m2_tiles_skipping_unique_key(String(unique_key), tile_key)
|
||||
|
||||
|
||||
func _queue_m2_tiles_skipping_unique_key(unique_key: String, released_tile_key: String) -> void:
|
||||
if unique_key.is_empty():
|
||||
return
|
||||
for tile_key_variant in _tile_states.keys():
|
||||
var tile_key := String(tile_key_variant)
|
||||
if tile_key == released_tile_key:
|
||||
continue
|
||||
var state: Dictionary = _tile_states[tile_key]
|
||||
var skipped_keys: Array = state.get("m2_skipped_unique_keys", [])
|
||||
if not skipped_keys.has(unique_key):
|
||||
continue
|
||||
if not bool(state.get("wanted", false)):
|
||||
continue
|
||||
if not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius):
|
||||
continue
|
||||
_cancel_m2_build_job(tile_key)
|
||||
_remove_tile_m2_root(state)
|
||||
state["m2_built"] = false
|
||||
state["m2_skipped_unique_keys"] = []
|
||||
_tile_states[tile_key] = state
|
||||
_enqueue_detail_asset_sync(tile_key)
|
||||
|
||||
|
||||
func _group_tile_m2_task(
|
||||
@@ -4059,6 +4178,9 @@ func _process_m2_build_jobs() -> void:
|
||||
var state: Dictionary = _tile_states[key]
|
||||
if not bool(state.get("wanted", false)) or not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius):
|
||||
_cancel_m2_build_job(key)
|
||||
_release_tile_m2_unique_keys(state)
|
||||
state["m2_built"] = false
|
||||
_tile_states[key] = state
|
||||
_m2_build_queue.pop_front()
|
||||
continue
|
||||
|
||||
@@ -4314,6 +4436,11 @@ func _remove_tile_m2_assets(state: Dictionary) -> void:
|
||||
var key := String(state.get("key", ""))
|
||||
if not key.is_empty():
|
||||
_cancel_m2_build_job(key)
|
||||
_release_tile_m2_unique_keys(state)
|
||||
_remove_tile_m2_root(state)
|
||||
|
||||
|
||||
func _remove_tile_m2_root(state: Dictionary) -> void:
|
||||
var tile_root: Node = state.get("root", null)
|
||||
if tile_root == null or not is_instance_valid(tile_root):
|
||||
return
|
||||
@@ -4403,7 +4530,11 @@ func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
||||
return null
|
||||
if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION:
|
||||
return mesh
|
||||
var rebuilt := _rebuild_m2_mesh_from_raw(normalized_rel)
|
||||
var data := _load_m2_raw_data_for_refresh(normalized_rel)
|
||||
if data.is_empty() or not _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel, data):
|
||||
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
|
||||
return mesh
|
||||
var rebuilt := _rebuild_m2_mesh_from_data(data)
|
||||
if rebuilt != null:
|
||||
return rebuilt
|
||||
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
|
||||
@@ -4421,16 +4552,67 @@ func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
|
||||
mesh_instance.mesh = prepared
|
||||
|
||||
|
||||
func _rebuild_m2_mesh_from_raw(normalized_rel: String) -> Mesh:
|
||||
func _load_m2_raw_data_for_refresh(normalized_rel: String) -> Dictionary:
|
||||
if normalized_rel.is_empty() or not ClassDB.class_exists("M2Loader"):
|
||||
return null
|
||||
return {}
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
return null
|
||||
return {}
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
if loader == null:
|
||||
return null
|
||||
var data: Dictionary = loader.call("load_m2", abs_path)
|
||||
return {}
|
||||
var data: Variant = loader.call("load_m2", abs_path)
|
||||
return data if data is Dictionary else {}
|
||||
|
||||
|
||||
func _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel: String, data: Dictionary) -> bool:
|
||||
if _m2_runtime_rebuild_required_cache.has(normalized_rel):
|
||||
return bool(_m2_runtime_rebuild_required_cache[normalized_rel])
|
||||
var needs_rebuild := _m2_raw_data_has_billboards(data) or _m2_raw_data_has_uv_rotation(data)
|
||||
_m2_runtime_rebuild_required_cache[normalized_rel] = needs_rebuild
|
||||
return needs_rebuild
|
||||
|
||||
|
||||
func _m2_raw_data_has_billboards(data: Dictionary) -> bool:
|
||||
for batch_variant in data.get("batches", []):
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
if bool(batch.get("has_billboard", false)) or int(batch.get("billboard_vertex_count", 0)) > 0:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _m2_raw_data_has_uv_rotation(data: Dictionary) -> bool:
|
||||
var transforms: Array = data.get("texture_transforms", [])
|
||||
if transforms.is_empty():
|
||||
return false
|
||||
var combos: PackedInt32Array = data.get("texture_transform_combos", PackedInt32Array())
|
||||
if combos.is_empty():
|
||||
return false
|
||||
for batch_variant in data.get("batches", []):
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
|
||||
var base_combo := int(batch.get("texture_transform_combo_index", -1))
|
||||
for stage in mini(texture_count, 4):
|
||||
var combo_index := base_combo + stage
|
||||
if combo_index < 0 or combo_index >= combos.size():
|
||||
continue
|
||||
var transform_index := int(combos[combo_index])
|
||||
if transform_index < 0 or transform_index >= transforms.size():
|
||||
continue
|
||||
var transform: Dictionary = transforms[transform_index] if transforms[transform_index] is Dictionary else {}
|
||||
var rotation: Vector4 = transform.get("rotation", Vector4(1.0, 0.0, 0.0, 1.0))
|
||||
if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
|
||||
return true
|
||||
if absf(float(transform.get("rotation_speed", 0.0))) > 0.000001:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _rebuild_m2_mesh_from_data(data: Dictionary) -> Mesh:
|
||||
if data.is_empty():
|
||||
return null
|
||||
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
extends SceneTree
|
||||
|
||||
const STREAMING_SCENE := "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn"
|
||||
|
||||
const CHECKPOINTS := [
|
||||
{
|
||||
"name": "elwynn_waterfall_front",
|
||||
"camera": Vector3(16445.0, 125.0, 26295.0),
|
||||
"target": Vector3(16518.84, 68.0, 26427.27),
|
||||
"player": Vector3(16518.84, 55.0, 26427.27),
|
||||
},
|
||||
{
|
||||
"name": "elwynn_waterfall_side",
|
||||
"camera": Vector3(16670.0, 115.0, 26320.0),
|
||||
"target": Vector3(16518.84, 72.0, 26427.27),
|
||||
"player": Vector3(16518.84, 55.0, 26427.27),
|
||||
},
|
||||
{
|
||||
"name": "goldshire_inn_windows",
|
||||
"camera": Vector3(16942.0, 82.0, 26503.0),
|
||||
"target": Vector3(17042.27, 66.0, 26530.91),
|
||||
"player": Vector3(17042.27, 58.0, 26530.91),
|
||||
},
|
||||
{
|
||||
"name": "goldshire_blacksmith_windows",
|
||||
"camera": Vector3(16892.0, 79.0, 26562.0),
|
||||
"target": Vector3(16972.46, 64.0, 26526.7),
|
||||
"player": Vector3(16972.46, 58.0, 26526.7),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_capture_async.call_deferred()
|
||||
|
||||
|
||||
func _capture_async() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var output_dir := _arg(args, "--output", "user://render_checkpoints")
|
||||
var only := _arg(args, "--only", "").to_lower()
|
||||
var wait_seconds := float(_arg(args, "--wait", "8.0"))
|
||||
var width := int(_arg(args, "--width", "1280"))
|
||||
var height := int(_arg(args, "--height", "900"))
|
||||
var headless := DisplayServer.get_name().to_lower() == "headless"
|
||||
var dry_run := args.has("--dry-run") or headless
|
||||
|
||||
get_root().size = Vector2i(maxi(16, width), maxi(16, height))
|
||||
var abs_output_dir := ProjectSettings.globalize_path(output_dir)
|
||||
if not dry_run:
|
||||
DirAccess.make_dir_recursive_absolute(abs_output_dir)
|
||||
elif headless and not args.has("--dry-run"):
|
||||
print("RENDER_CHECKPOINT dry-run: headless display cannot capture viewport PNGs.")
|
||||
|
||||
var packed: PackedScene = load(STREAMING_SCENE)
|
||||
if packed == null:
|
||||
push_error("Cannot load streaming scene: %s" % STREAMING_SCENE)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var world = packed.instantiate()
|
||||
if not (world is Node3D):
|
||||
push_error("Streaming scene root is not Node3D: %s" % STREAMING_SCENE)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "CheckpointCamera"
|
||||
camera.current = true
|
||||
camera.fov = 62.0
|
||||
camera.far = 50000.0
|
||||
camera.position = CHECKPOINTS[0].get("camera", Vector3.ZERO)
|
||||
(world as Node3D).add_child(camera)
|
||||
world.set("camera_path", NodePath("CheckpointCamera"))
|
||||
world.set("debug_streaming", true)
|
||||
get_root().add_child(world)
|
||||
await process_frame
|
||||
await process_frame
|
||||
|
||||
var player := world.get_node_or_null("ThirdPersonPlayer") as Node3D
|
||||
if player != null:
|
||||
player.set_process(false)
|
||||
player.set_physics_process(false)
|
||||
var visual := player.get_node_or_null("Visual") as Node3D
|
||||
if visual != null:
|
||||
visual.visible = false
|
||||
|
||||
var captured := 0
|
||||
for checkpoint_variant in CHECKPOINTS:
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||
if not only.is_empty() and not checkpoint_name.to_lower().contains(only):
|
||||
continue
|
||||
|
||||
camera.global_position = checkpoint.get("camera", Vector3.ZERO)
|
||||
camera.look_at(checkpoint.get("target", Vector3.ZERO), Vector3.UP)
|
||||
if player != null:
|
||||
player.global_position = checkpoint.get("player", checkpoint.get("target", Vector3.ZERO))
|
||||
if world.has_method("_refresh_streaming_targets_at"):
|
||||
world.call("_refresh_streaming_targets_at", camera.global_position, true)
|
||||
|
||||
await create_timer(maxf(wait_seconds, 0.0)).timeout
|
||||
if dry_run:
|
||||
print("RENDER_CHECKPOINT dry_run name=%s camera=%s target=%s" % [
|
||||
checkpoint_name,
|
||||
camera.global_position,
|
||||
checkpoint.get("target", Vector3.ZERO),
|
||||
])
|
||||
captured += 1
|
||||
continue
|
||||
|
||||
await RenderingServer.frame_post_draw
|
||||
|
||||
var image := get_root().get_texture().get_image()
|
||||
var file_name := "%s.png" % checkpoint_name
|
||||
var abs_path := abs_output_dir.path_join(file_name)
|
||||
var err := image.save_png(abs_path)
|
||||
if err != OK:
|
||||
push_error("Failed to save checkpoint %s to %s (err=%d)" % [checkpoint_name, abs_path, err])
|
||||
quit(1)
|
||||
return
|
||||
print("RENDER_CHECKPOINT saved %s" % abs_path)
|
||||
captured += 1
|
||||
|
||||
world.queue_free()
|
||||
if captured <= 0:
|
||||
push_error("No checkpoints captured. --only filter was: %s" % only)
|
||||
quit(1)
|
||||
return
|
||||
quit(0)
|
||||
|
||||
|
||||
func _arg(args: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var idx := args.find(name)
|
||||
if idx >= 0 and idx + 1 < args.size():
|
||||
return args[idx + 1]
|
||||
return default_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://ct7wd7gtf8h6o
|
||||
@@ -0,0 +1,80 @@
|
||||
extends SceneTree
|
||||
|
||||
const DEFAULT_EXTRACTED_DIR := "res://data/extracted"
|
||||
const DEFAULT_MAP := "Azeroth"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var extracted_dir := _arg(args, "--extracted", DEFAULT_EXTRACTED_DIR)
|
||||
var map_name := _arg(args, "--map", DEFAULT_MAP)
|
||||
var kind := _arg(args, "--kind", "wmo").to_lower()
|
||||
var path_contains := _arg(args, "--path-contains", "").to_lower()
|
||||
var limit := int(_arg(args, "--limit", "25"))
|
||||
|
||||
if not ClassDB.class_exists("ADTLoader"):
|
||||
push_error("ADTLoader is not registered. Rebuild the native GDExtension first.")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var map_dir := ProjectSettings.globalize_path(
|
||||
extracted_dir.path_join("World/Maps").path_join(map_name)
|
||||
)
|
||||
var dir := DirAccess.open(map_dir)
|
||||
if dir == null:
|
||||
push_error("Cannot open map directory: %s" % map_dir)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var loader = ClassDB.instantiate("ADTLoader")
|
||||
var files := dir.get_files()
|
||||
files.sort()
|
||||
var printed := 0
|
||||
for file_name in files:
|
||||
if printed >= limit:
|
||||
break
|
||||
if not file_name.to_lower().ends_with(".adt"):
|
||||
continue
|
||||
if not file_name.begins_with("%s_" % map_name):
|
||||
continue
|
||||
var data: Dictionary = loader.call("load_adt", map_dir.path_join(file_name))
|
||||
var names: PackedStringArray = data.get("%s_names" % kind, PackedStringArray())
|
||||
var placements: Array = data.get("%s_placements" % kind, [])
|
||||
for placement_variant in placements:
|
||||
if printed >= limit:
|
||||
break
|
||||
if not (placement_variant is Dictionary):
|
||||
continue
|
||||
var placement: Dictionary = placement_variant
|
||||
var name_id := int(placement.get("name_id", -1))
|
||||
if name_id < 0 or name_id >= names.size():
|
||||
continue
|
||||
var rel_path := String(names[name_id]).replace("\\", "/")
|
||||
if not path_contains.is_empty() and not rel_path.to_lower().contains(path_contains):
|
||||
continue
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
print("ADT_%s tile=%s uid=%d path=%s pos=%s rot_deg=%s scale=%.3f" % [
|
||||
kind.to_upper(),
|
||||
_tile_from_adt_filename(file_name),
|
||||
int(placement.get("unique_id", -1)),
|
||||
rel_path,
|
||||
placement.get("pos", Vector3.ZERO),
|
||||
Vector3(rad_to_deg(rot.x), rad_to_deg(rot.y), rad_to_deg(rot.z)),
|
||||
float(placement.get("scale", 1.0)),
|
||||
])
|
||||
printed += 1
|
||||
quit(0)
|
||||
|
||||
|
||||
func _tile_from_adt_filename(file_name: String) -> Vector2i:
|
||||
var parts := file_name.get_basename().split("_")
|
||||
if parts.size() < 3:
|
||||
return Vector2i(-1, -1)
|
||||
return Vector2i(int(parts[parts.size() - 2]), int(parts[parts.size() - 1]))
|
||||
|
||||
|
||||
func _arg(args: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var idx := args.find(name)
|
||||
if idx >= 0 and idx + 1 < args.size():
|
||||
return args[idx + 1]
|
||||
return default_value
|
||||
@@ -0,0 +1 @@
|
||||
uid://bad7oihytwnq0
|
||||
@@ -0,0 +1,145 @@
|
||||
extends SceneTree
|
||||
|
||||
const DEFAULT_EXTRACTED_DIR := "res://data/extracted"
|
||||
const DEFAULT_MAP := "Azeroth"
|
||||
const DEFAULT_TARGET_UID := 11785
|
||||
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var extracted_dir := _arg(args, "--extracted", DEFAULT_EXTRACTED_DIR)
|
||||
var map_name := _arg(args, "--map", DEFAULT_MAP)
|
||||
var target_uid := int(_arg(args, "--uid", str(DEFAULT_TARGET_UID)))
|
||||
var path_contains := _arg(args, "--path-contains", "waterfall").to_lower()
|
||||
print("ADT M2 placement verification started. map=%s uid=%d path_contains=%s" % [
|
||||
map_name,
|
||||
target_uid,
|
||||
path_contains,
|
||||
])
|
||||
|
||||
var matches := _find_m2_placements(extracted_dir, map_name, target_uid, path_contains)
|
||||
if matches.is_empty():
|
||||
_fail("No matching ADT M2 placement found. map=%s uid=%d path_contains=%s" % [
|
||||
map_name,
|
||||
target_uid,
|
||||
path_contains,
|
||||
])
|
||||
else:
|
||||
print("ADT M2 placement verification matched %d placement(s)." % matches.size())
|
||||
for match in matches:
|
||||
print(_format_match(match))
|
||||
|
||||
if _failures > 0:
|
||||
quit(1)
|
||||
return
|
||||
quit(0)
|
||||
|
||||
|
||||
func _find_m2_placements(
|
||||
extracted_dir: String,
|
||||
map_name: String,
|
||||
target_uid: int,
|
||||
path_contains: String) -> Array:
|
||||
if not ClassDB.class_exists("ADTLoader"):
|
||||
_fail("ADTLoader is not registered. Rebuild the native GDExtension first.")
|
||||
return []
|
||||
|
||||
var map_dir := ProjectSettings.globalize_path(
|
||||
extracted_dir.path_join("World/Maps").path_join(map_name)
|
||||
)
|
||||
var dir := DirAccess.open(map_dir)
|
||||
if dir == null:
|
||||
_fail("Cannot open map directory: %s" % map_dir)
|
||||
return []
|
||||
|
||||
var files := _collect_adt_files(dir, map_name)
|
||||
if files.is_empty():
|
||||
_fail("No ADT files found in %s" % map_dir)
|
||||
return []
|
||||
|
||||
var loader = ClassDB.instantiate("ADTLoader")
|
||||
if loader == null:
|
||||
_fail("Cannot instantiate ADTLoader.")
|
||||
return []
|
||||
|
||||
var matches := []
|
||||
for file_name in files:
|
||||
var tile := _tile_from_adt_filename(file_name)
|
||||
var data: Dictionary = loader.call("load_adt", map_dir.path_join(file_name))
|
||||
var names: PackedStringArray = data.get("m2_names", PackedStringArray())
|
||||
var placements: Array = data.get("m2_placements", [])
|
||||
for placement_variant in placements:
|
||||
if not (placement_variant is Dictionary):
|
||||
continue
|
||||
var placement: Dictionary = placement_variant
|
||||
var uid := int(placement.get("unique_id", -1))
|
||||
if target_uid >= 0 and uid != target_uid:
|
||||
continue
|
||||
var name_id := int(placement.get("name_id", -1))
|
||||
if name_id < 0 or name_id >= names.size():
|
||||
continue
|
||||
var rel_path := String(names[name_id]).replace("\\", "/")
|
||||
if not path_contains.is_empty() and not rel_path.to_lower().contains(path_contains):
|
||||
continue
|
||||
matches.append({
|
||||
"tile": tile,
|
||||
"uid": uid,
|
||||
"path": rel_path,
|
||||
"pos": placement.get("pos", Vector3.ZERO),
|
||||
"rot": placement.get("rot", Vector3.ZERO),
|
||||
"scale": float(placement.get("scale", 1.0)),
|
||||
})
|
||||
return matches
|
||||
|
||||
|
||||
func _collect_adt_files(dir: DirAccess, map_name: String) -> Array:
|
||||
var result := []
|
||||
dir.list_dir_begin()
|
||||
while true:
|
||||
var file_name := dir.get_next()
|
||||
if file_name.is_empty():
|
||||
break
|
||||
if dir.current_is_dir():
|
||||
continue
|
||||
if not file_name.to_lower().ends_with(".adt"):
|
||||
continue
|
||||
if not file_name.begins_with("%s_" % map_name):
|
||||
continue
|
||||
result.append(file_name)
|
||||
dir.list_dir_end()
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
|
||||
func _tile_from_adt_filename(file_name: String) -> Vector2i:
|
||||
var parts := file_name.get_basename().split("_")
|
||||
if parts.size() < 3:
|
||||
return Vector2i(-1, -1)
|
||||
return Vector2i(int(parts[parts.size() - 2]), int(parts[parts.size() - 1]))
|
||||
|
||||
|
||||
func _format_match(match: Dictionary) -> String:
|
||||
var rot: Vector3 = match.get("rot", Vector3.ZERO)
|
||||
var rot_deg := Vector3(rad_to_deg(rot.x), rad_to_deg(rot.y), rad_to_deg(rot.z))
|
||||
return "ADT_M2_PLACEMENT tile=%s uid=%d path=%s pos=%s rot_deg=%s scale=%.3f" % [
|
||||
match.get("tile", Vector2i(-1, -1)),
|
||||
int(match.get("uid", -1)),
|
||||
String(match.get("path", "")),
|
||||
match.get("pos", Vector3.ZERO),
|
||||
rot_deg,
|
||||
float(match.get("scale", 1.0)),
|
||||
]
|
||||
|
||||
|
||||
func _arg(args: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var idx := args.find(name)
|
||||
if idx >= 0 and idx + 1 < args.size():
|
||||
return args[idx + 1]
|
||||
return default_value
|
||||
|
||||
|
||||
func _fail(message: String) -> void:
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bbg18pmk3svu2
|
||||
@@ -0,0 +1,54 @@
|
||||
extends SceneTree
|
||||
|
||||
const STREAMING_WORLD_LOADER := preload("res://src/scenes/streaming/streaming_world_loader.gd")
|
||||
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var loader = STREAMING_WORLD_LOADER.new()
|
||||
var placement := {
|
||||
"name_id": 0,
|
||||
"unique_id": 11785,
|
||||
"pos": Vector3.ZERO,
|
||||
"rot": Vector3.ZERO,
|
||||
"scale": 1.0,
|
||||
}
|
||||
|
||||
var first: Dictionary = loader.call("_reserve_tile_m2_placements", "30_49", [placement])
|
||||
_expect((first.get("placements", []) as Array).size() == 1, "first tile should reserve unique M2 placement")
|
||||
_expect((first.get("unique_keys", []) as Array).has("uid:11785"), "first tile should own uid:11785")
|
||||
_expect((first.get("skipped_unique_keys", []) as Array).is_empty(), "first tile should not skip uid:11785")
|
||||
|
||||
var duplicate: Dictionary = loader.call("_reserve_tile_m2_placements", "31_49", [placement])
|
||||
_expect((duplicate.get("placements", []) as Array).is_empty(), "second tile should skip duplicate uid:11785")
|
||||
_expect((duplicate.get("skipped_unique_keys", []) as Array).has("uid:11785"), "second tile should remember skipped uid:11785")
|
||||
|
||||
loader.call("_release_tile_m2_unique_keys", {
|
||||
"key": "30_49",
|
||||
"m2_unique_keys": ["uid:11785"],
|
||||
"m2_skipped_unique_keys": [],
|
||||
}, false)
|
||||
var after_release: Dictionary = loader.call("_reserve_tile_m2_placements", "31_49", [placement])
|
||||
_expect((after_release.get("placements", []) as Array).size() == 1, "second tile should reserve uid:11785 after owner release")
|
||||
_expect((after_release.get("unique_keys", []) as Array).has("uid:11785"), "second tile should own uid:11785 after release")
|
||||
|
||||
var same_tile_loader = STREAMING_WORLD_LOADER.new()
|
||||
var same_tile_duplicate: Dictionary = same_tile_loader.call("_reserve_tile_m2_placements", "31_49", [placement, placement])
|
||||
_expect((same_tile_duplicate.get("placements", []) as Array).size() == 1, "same tile should keep only one copy of duplicate uid:11785")
|
||||
|
||||
loader.free()
|
||||
same_tile_loader.free()
|
||||
if _failures > 0:
|
||||
push_error("M2 unique dedupe verification failed: %d issue(s)" % _failures)
|
||||
quit(1)
|
||||
return
|
||||
print("M2 unique dedupe verification passed.")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b2xmxm1dxmx10
|
||||
@@ -0,0 +1,142 @@
|
||||
extends SceneTree
|
||||
|
||||
const WOW_M2_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_m2_material.gd")
|
||||
const WOW_WMO_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_wmo_material.gd")
|
||||
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_check_project_descriptor_setting()
|
||||
_check_wmo_window_material()
|
||||
_check_wmo_wall_material()
|
||||
_check_wmo_glass_material()
|
||||
_check_m2_alpha_material()
|
||||
_check_m2_billboard_and_uv_rotation_shader()
|
||||
|
||||
if _failures > 0:
|
||||
push_error("Renderer material verification failed: %d issue(s)" % _failures)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("Renderer material verification passed.")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _check_project_descriptor_setting() -> void:
|
||||
var descriptors := int(ProjectSettings.get_setting(
|
||||
"rendering/rendering_device/d3d12/max_resource_descriptors",
|
||||
0
|
||||
))
|
||||
_expect(
|
||||
descriptors >= 1048576,
|
||||
"D3D12 resource descriptor heap setting should be at least 1048576, got %d" % descriptors
|
||||
)
|
||||
|
||||
|
||||
func _check_wmo_window_material() -> void:
|
||||
var mat := WOW_WMO_MATERIAL.build(
|
||||
null,
|
||||
null,
|
||||
0x11,
|
||||
0,
|
||||
0,
|
||||
"DUNGEONS/TEXTURES/WINDOWS/MM_ELWYNN_WND_EXT__01.BLP"
|
||||
)
|
||||
var code := mat.shader.code
|
||||
_expect(bool(mat.get_meta("wow_wmo_window_material", false)), "Elwynn exterior window should be tagged as a WMO window material")
|
||||
_expect(not bool(mat.get_meta("wow_wmo_glass_blend_material", false)), "Opaque Elwynn exterior window should not be tagged as glass blend")
|
||||
_expect(float(mat.get_shader_parameter("unlit_material")) > 0.5, "WMO flag 0x01 should make Elwynn exterior window unlit")
|
||||
_expect(code.contains("cull_back"), "Opaque WMO window atlas shader should use cull_back")
|
||||
_expect(not code.contains("cull_disabled"), "Opaque WMO window atlas shader should not use cull_disabled")
|
||||
_expect(code.contains("depth_draw_opaque"), "Opaque WMO window atlas shader should use opaque depth draw")
|
||||
_expect(code.contains("filter_linear, repeat_disable"), "WMO window atlas shader should use stable non-mipmapped albedo sampler")
|
||||
_expect(not code.contains("ALPHA ="), "Opaque WMO window atlas shader should not write ALPHA")
|
||||
|
||||
|
||||
func _check_wmo_wall_material() -> void:
|
||||
var mat := WOW_WMO_MATERIAL.build(
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"DUNGEONS/TEXTURES/WALLS/MM_STRMWND_WALL_03.BLP"
|
||||
)
|
||||
var code := mat.shader.code
|
||||
_expect(not bool(mat.get_meta("wow_wmo_window_material", false)), "Stormwind wall texture name must not be matched as a window")
|
||||
_expect(float(mat.get_shader_parameter("unlit_material")) < 0.5, "Ordinary WMO wall should not be unlit")
|
||||
_expect(code.contains("cull_disabled"), "Ordinary WMO wall should keep cull_disabled")
|
||||
_expect(code.contains("filter_linear_mipmap_anisotropic, repeat_enable"), "Ordinary WMO wall should keep mipmapped repeating albedo sampler")
|
||||
|
||||
|
||||
func _check_wmo_glass_material() -> void:
|
||||
var mat := WOW_WMO_MATERIAL.build(
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
"DUNGEONS/TEXTURES/WINDOWS/MM_ELWYNN_WND_EXT__01.BLP"
|
||||
)
|
||||
var code := mat.shader.code
|
||||
_expect(bool(mat.get_meta("wow_wmo_glass_blend_material", false)), "BlendMode 2 WMO window should be tagged as glass")
|
||||
_expect(code.contains("cull_back"), "WMO glass shader should use cull_back")
|
||||
_expect(code.contains("depth_draw_never"), "WMO glass shader should avoid depth writes")
|
||||
_expect(code.contains("ALPHA = clamp"), "WMO glass shader should write alpha")
|
||||
|
||||
|
||||
func _check_m2_alpha_material() -> void:
|
||||
var mat := WOW_M2_MATERIAL.build(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"NewWaterfall",
|
||||
{}
|
||||
)
|
||||
var code := mat.shader.code
|
||||
_expect(code.contains("blend_mix"), "M2 alpha shader should use blend_mix")
|
||||
_expect(code.contains("depth_draw_never"), "M2 alpha shader should avoid depth writes")
|
||||
_expect(code.contains("ALPHA = clamp"), "M2 alpha shader should write alpha")
|
||||
|
||||
|
||||
func _check_m2_billboard_and_uv_rotation_shader() -> void:
|
||||
var mat := WOW_M2_MATERIAL.build(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0x04,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"BillboardProbe",
|
||||
{
|
||||
"billboard_enabled": true,
|
||||
"stage0_uv_rotation": Vector4(0.0, -1.0, 1.0, 0.0),
|
||||
"stage0_uv_rotation_speed": 0.25,
|
||||
}
|
||||
)
|
||||
var code := mat.shader.code
|
||||
_expect(bool(mat.get_meta("wow_m2_billboard_enabled", false)), "M2 material should preserve billboard metadata")
|
||||
_expect(bool(mat.get_shader_parameter("billboard_enabled")), "M2 material should enable billboard shader parameter")
|
||||
_expect(code.contains("apply_m2_billboard"), "M2 shader should keep billboard vertex path")
|
||||
_expect(code.contains("CUSTOM0"), "M2 shader should consume CUSTOM0 billboard data")
|
||||
_expect(code.contains("stage0_uv_rotation"), "M2 shader should keep UV rotation uniforms")
|
||||
_expect(code.contains("uv_rotation_speed * TIME"), "M2 shader should keep animated UV rotation path")
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if condition:
|
||||
return
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c58bfxr64yglq
|
||||
Reference in New Issue
Block a user