first commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://cwsdumb6pp10m
|
||||
@@ -0,0 +1,127 @@
|
||||
## Converts raw M2Loader data into a Godot Node3D scene tree.
|
||||
## Usage:
|
||||
## var data = M2Loader.new().load_m2(abs_path)
|
||||
## var node = M2Builder.build(data, "res://extracted")
|
||||
## add_child(node)
|
||||
class_name M2Builder
|
||||
|
||||
static var _texture_cache: Dictionary = {}
|
||||
|
||||
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "M2"
|
||||
|
||||
var verts: PackedVector3Array = data.get("vertices", PackedVector3Array())
|
||||
var normals: PackedVector3Array = data.get("normals", PackedVector3Array())
|
||||
var uvs: PackedVector2Array = data.get("uvs", PackedVector2Array())
|
||||
var indices: PackedInt32Array = data.get("indices", PackedInt32Array())
|
||||
var batches: Array = data.get("batches", [])
|
||||
|
||||
if verts.is_empty() or indices.is_empty() or batches.is_empty():
|
||||
return root
|
||||
|
||||
var textures: PackedStringArray = data.get("textures", PackedStringArray())
|
||||
var materials: Array = data.get("materials", [])
|
||||
var tex_combos: PackedInt32Array = data.get("texture_combos", PackedInt32Array())
|
||||
|
||||
var mesh := ArrayMesh.new()
|
||||
|
||||
for batch_variant in batches:
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
var idx_start: int = int(batch.get("index_start", 0))
|
||||
var idx_count: int = int(batch.get("index_count", 0))
|
||||
var mat_id: int = int(batch.get("material_id", -1))
|
||||
var tc_idx: int = int(batch.get("texture_combo_index", -1))
|
||||
|
||||
if idx_count <= 0 or idx_start + idx_count > indices.size():
|
||||
continue
|
||||
|
||||
var batch_indices := indices.slice(idx_start, idx_start + idx_count)
|
||||
if batch_indices.is_empty():
|
||||
continue
|
||||
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
if normals.size() == verts.size():
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
if uvs.size() == verts.size():
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||
arrays[Mesh.ARRAY_INDEX] = batch_indices
|
||||
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
|
||||
var surf_idx := mesh.get_surface_count() - 1
|
||||
var mat_def: Dictionary = materials[mat_id] if mat_id >= 0 and mat_id < materials.size() else {}
|
||||
mesh.surface_set_material(surf_idx, _build_material(mat_def, tc_idx, textures, tex_combos, extracted_dir))
|
||||
|
||||
if mesh.get_surface_count() == 0:
|
||||
return root
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.name = "Mesh"
|
||||
mi.mesh = mesh
|
||||
root.add_child(mi)
|
||||
return root
|
||||
|
||||
|
||||
static func _build_material(
|
||||
mat_def: Dictionary,
|
||||
texture_combo_index: int,
|
||||
textures: PackedStringArray,
|
||||
tex_combos: PackedInt32Array,
|
||||
extracted_dir: String) -> StandardMaterial3D:
|
||||
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.roughness = 0.85
|
||||
mat.metallic = 0.0
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
||||
mat.vertex_color_use_as_albedo = false
|
||||
|
||||
var blend_mode: int = mat_def.get("blend_mode", 0)
|
||||
match blend_mode:
|
||||
0: mat.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
|
||||
1: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||||
_: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
|
||||
if texture_combo_index >= 0 and texture_combo_index < tex_combos.size():
|
||||
var tex_idx: int = tex_combos[texture_combo_index]
|
||||
if tex_idx >= 0 and tex_idx < textures.size():
|
||||
var tex_path: String = str(textures[tex_idx]).replace("\\", "/")
|
||||
if not tex_path.is_empty():
|
||||
var tex := _load_texture(tex_path, extracted_dir)
|
||||
if tex:
|
||||
mat.albedo_texture = tex
|
||||
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
||||
|
||||
return mat
|
||||
|
||||
|
||||
static func _load_texture(rel_path: String, extracted_dir: String) -> Texture2D:
|
||||
if rel_path.is_empty() or extracted_dir.is_empty():
|
||||
return null
|
||||
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(rel_path))
|
||||
if _texture_cache.has(abs_path):
|
||||
return _texture_cache[abs_path]
|
||||
|
||||
if not ClassDB.class_exists("BLPLoader"):
|
||||
return null
|
||||
|
||||
var loader = ClassDB.instantiate("BLPLoader")
|
||||
if loader == null:
|
||||
return null
|
||||
|
||||
var img: Image = loader.call("load_image", abs_path)
|
||||
if img == null or img.is_empty():
|
||||
_texture_cache[abs_path] = null
|
||||
return null
|
||||
|
||||
img.generate_mipmaps()
|
||||
var tex := ImageTexture.create_from_image(img)
|
||||
_texture_cache[abs_path] = tex
|
||||
return tex
|
||||
@@ -0,0 +1 @@
|
||||
uid://bsr7e0tp543di
|
||||
@@ -0,0 +1,164 @@
|
||||
## Converts raw WMOLoader data into a Godot Node3D scene tree.
|
||||
## Usage:
|
||||
## var data = WMOLoader.new().load_wmo(abs_path)
|
||||
## var node = WMOBuilder.build(data, "res://extracted")
|
||||
## add_child(node)
|
||||
class_name WMOBuilder
|
||||
|
||||
static var _texture_cache: Dictionary = {}
|
||||
|
||||
# Returns a Node3D containing one MeshInstance3D per WMO group.
|
||||
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "WMO"
|
||||
|
||||
if not data.has("groups"):
|
||||
return root
|
||||
|
||||
var textures: PackedStringArray = data.get("textures", PackedStringArray())
|
||||
var materials: Array = data.get("materials", [])
|
||||
|
||||
# Build Godot materials from WMO material definitions
|
||||
var godot_materials: Array[StandardMaterial3D] = []
|
||||
for mat_def in materials:
|
||||
godot_materials.append(_build_material(mat_def, textures, extracted_dir))
|
||||
|
||||
# Build one MeshInstance3D per group
|
||||
var groups: Array = data.get("groups", [])
|
||||
for gi in groups.size():
|
||||
var g: Dictionary = groups[gi]
|
||||
if g.is_empty():
|
||||
continue
|
||||
var mesh_inst := _build_group_mesh(g, godot_materials)
|
||||
mesh_inst.name = "Group_%d" % gi
|
||||
root.add_child(mesh_inst)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
static func _build_group_mesh(
|
||||
g: Dictionary,
|
||||
godot_mats: Array[StandardMaterial3D]) -> MeshInstance3D:
|
||||
|
||||
var verts: PackedVector3Array = g.get("vertices", PackedVector3Array())
|
||||
var normals: PackedVector3Array = g.get("normals", PackedVector3Array())
|
||||
var uvs: PackedVector2Array = g.get("uvs", PackedVector2Array())
|
||||
var colors: PackedColorArray = g.get("colors", PackedColorArray())
|
||||
var indices: PackedInt32Array = g.get("indices", PackedInt32Array())
|
||||
var batches: Array = g.get("batches", [])
|
||||
var mesh := ArrayMesh.new()
|
||||
if verts.is_empty() or indices.is_empty():
|
||||
var empty := MeshInstance3D.new()
|
||||
empty.mesh = mesh
|
||||
return empty
|
||||
|
||||
if batches.is_empty():
|
||||
batches = [{
|
||||
"index_start": 0,
|
||||
"index_count": indices.size(),
|
||||
"material_id": -1,
|
||||
}]
|
||||
|
||||
for batch_variant in batches:
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
var idx_start: int = int(batch.get("index_start", 0))
|
||||
var idx_count: int = int(batch.get("index_count", 0))
|
||||
var mat_id: int = int(batch.get("material_id", -1))
|
||||
if idx_count <= 0 or idx_start >= indices.size():
|
||||
continue
|
||||
|
||||
var batch_indices := PackedInt32Array()
|
||||
for i in range(idx_start, mini(idx_start + idx_count, indices.size()), 3):
|
||||
if i + 2 >= indices.size():
|
||||
break
|
||||
# Reverse winding for the current local basis conversion.
|
||||
batch_indices.append(indices[i + 0])
|
||||
batch_indices.append(indices[i + 2])
|
||||
batch_indices.append(indices[i + 1])
|
||||
if batch_indices.is_empty():
|
||||
continue
|
||||
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
if normals.size() == verts.size():
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
if uvs.size() == verts.size():
|
||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||
if colors.size() == verts.size():
|
||||
arrays[Mesh.ARRAY_COLOR] = colors
|
||||
arrays[Mesh.ARRAY_INDEX] = batch_indices
|
||||
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
|
||||
var surf_idx := mesh.get_surface_count() - 1
|
||||
if mat_id >= 0 and mat_id < godot_mats.size():
|
||||
mesh.surface_set_material(surf_idx, godot_mats[mat_id])
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
return mi
|
||||
|
||||
|
||||
static func _build_material(
|
||||
mat_def: Dictionary,
|
||||
textures: PackedStringArray,
|
||||
extracted_dir: String = "") -> StandardMaterial3D:
|
||||
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.roughness = 0.85
|
||||
mat.metallic = 0.0
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
||||
mat.vertex_color_use_as_albedo = false
|
||||
|
||||
var blend_mode: int = mat_def.get("blend_mode", 0)
|
||||
match blend_mode:
|
||||
0: mat.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
|
||||
1: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||||
_: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
|
||||
# Texture is loaded later by the scene assembler (needs BLP→Image conversion)
|
||||
# Store the path as metadata for the assembler to pick up
|
||||
var tex0_id: int = mat_def.get("texture0", -1)
|
||||
if tex0_id >= 0 and tex0_id < textures.size():
|
||||
var tex_path: String = str(textures[tex0_id]).replace("\\", "/")
|
||||
mat.set_meta("texture0_path", tex_path)
|
||||
var tex := _load_texture(tex_path, extracted_dir)
|
||||
if tex:
|
||||
mat.albedo_texture = tex
|
||||
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
||||
|
||||
mat.set_meta("wow_flags", mat_def.get("flags", 0))
|
||||
mat.set_meta("wow_shader", mat_def.get("shader", 0))
|
||||
|
||||
return mat
|
||||
|
||||
|
||||
static func _load_texture(rel_path: String, extracted_dir: String) -> Texture2D:
|
||||
if rel_path.is_empty() or extracted_dir.is_empty():
|
||||
return null
|
||||
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(rel_path.replace("\\", "/")))
|
||||
if _texture_cache.has(abs_path):
|
||||
return _texture_cache[abs_path]
|
||||
|
||||
if not ClassDB.class_exists("BLPLoader"):
|
||||
return null
|
||||
|
||||
var loader = ClassDB.instantiate("BLPLoader")
|
||||
if loader == null:
|
||||
return null
|
||||
|
||||
var img: Image = loader.call("load_image", abs_path)
|
||||
if img == null or img.is_empty():
|
||||
_texture_cache[abs_path] = null
|
||||
return null
|
||||
|
||||
img.generate_mipmaps()
|
||||
var tex := ImageTexture.create_from_image(img)
|
||||
_texture_cache[abs_path] = tex
|
||||
return tex
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7qb5j8qms0b0
|
||||
@@ -0,0 +1,11 @@
|
||||
[configuration]
|
||||
entry_symbol = "mpq_extractor_init"
|
||||
compatibility_minimum = "4.6"
|
||||
|
||||
[libraries]
|
||||
windows.x86_64.debug = "res://addons/mpq_extractor/bin/libmpq_extractor.windows.x86_64.dll"
|
||||
windows.x86_64.release = "res://addons/mpq_extractor/bin/libmpq_extractor.windows.x86_64.dll"
|
||||
linux.x86_64.debug = "res://addons/mpq_extractor/bin/libmpq_extractor.linux.x86_64.so"
|
||||
linux.x86_64.release = "res://addons/mpq_extractor/bin/libmpq_extractor.linux.x86_64.so"
|
||||
macos.debug = "res://addons/mpq_extractor/bin/libmpq_extractor.macos.universal.dylib"
|
||||
macos.release = "res://addons/mpq_extractor/bin/libmpq_extractor.macos.universal.dylib"
|
||||
@@ -0,0 +1 @@
|
||||
uid://bd6x7bgktb61c
|
||||
@@ -0,0 +1,6 @@
|
||||
[plugin]
|
||||
name="MPQ Extractor"
|
||||
description="Extracts WoW 3.3.5a (build 12340) MPQ archives respecting Blizzard client priority."
|
||||
author="OpenWC"
|
||||
version="1.0.0"
|
||||
script="plugin.gd"
|
||||
@@ -0,0 +1,21 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
var _dock: Control
|
||||
|
||||
func _enter_tree() -> void:
|
||||
var script: Script = load("res://addons/mpq_extractor/ui/mpq_tool.gd")
|
||||
if script == null:
|
||||
push_error("MPQ Extractor: не удалось загрузить mpq_tool.gd")
|
||||
return
|
||||
_dock = VBoxContainer.new()
|
||||
_dock.name = "MPQExtractorDock"
|
||||
_dock.set_script(script)
|
||||
add_control_to_bottom_panel(_dock, "MPQ Extractor")
|
||||
make_bottom_panel_item_visible(_dock)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _dock:
|
||||
remove_control_from_bottom_panel(_dock)
|
||||
_dock.queue_free()
|
||||
_dock = null
|
||||
@@ -0,0 +1 @@
|
||||
uid://dds11be30harj
|
||||
@@ -0,0 +1,191 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
# ─── State ────────────────────────────────────────────────────────────────────
|
||||
var _mpq = null # MPQManager
|
||||
|
||||
const LOCALES := ["ruRU", "enUS", "enGB", "deDE", "frFR", "esES", "esMX",
|
||||
"koKR", "zhCN", "zhTW", "ptBR", "ptPT"]
|
||||
|
||||
# ─── UI nodes (created in _ready) ─────────────────────────────────────────────
|
||||
var _client_path_edit: LineEdit
|
||||
var _locale_option: OptionButton
|
||||
var _filter_edit: LineEdit
|
||||
var _output_edit: LineEdit
|
||||
var _open_btn: Button
|
||||
var _extract_btn: Button
|
||||
var _list_btn: Button
|
||||
var _status_label: Label
|
||||
var _file_list: ItemList
|
||||
var _archive_list: ItemList
|
||||
var _progress: ProgressBar
|
||||
|
||||
# ─── Build UI ─────────────────────────────────────────────────────────────────
|
||||
func _ready() -> void:
|
||||
size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
add_theme_constant_override("separation", 6)
|
||||
|
||||
_add_title()
|
||||
_add_separator()
|
||||
_add_client_row()
|
||||
_add_locale_row()
|
||||
_open_btn = _add_button("Open Client Archives", _on_open_pressed)
|
||||
_add_separator()
|
||||
_add_label("Loaded archives (highest priority first):")
|
||||
_archive_list = _add_item_list(100)
|
||||
_add_separator()
|
||||
_add_filter_row()
|
||||
_add_output_row()
|
||||
var btns := HBoxContainer.new(); add_child(btns)
|
||||
_list_btn = _add_button_to("List Files", btns, _on_list_pressed)
|
||||
_extract_btn = _add_button_to("Extract", btns, _on_extract_pressed)
|
||||
_progress = ProgressBar.new(); _progress.visible = false; add_child(_progress)
|
||||
_add_label("Files matching filter:")
|
||||
_file_list = _add_item_list(0, true) # expand vertically
|
||||
_status_label = _add_label("Ready.")
|
||||
|
||||
_extract_btn.disabled = true
|
||||
_list_btn.disabled = true
|
||||
|
||||
|
||||
# ─── UI helpers ───────────────────────────────────────────────────────────────
|
||||
func _add_title() -> void:
|
||||
var lbl := Label.new()
|
||||
lbl.text = "MPQ Extractor (WoW 3.3.5a)"
|
||||
lbl.add_theme_font_size_override("font_size", 14)
|
||||
add_child(lbl)
|
||||
|
||||
func _add_separator() -> void:
|
||||
add_child(HSeparator.new())
|
||||
|
||||
func _add_label(text: String) -> Label:
|
||||
var lbl := Label.new(); lbl.text = text
|
||||
add_child(lbl); return lbl
|
||||
|
||||
func _add_item_list(min_height: int, expand := false) -> ItemList:
|
||||
var il := ItemList.new()
|
||||
if min_height > 0:
|
||||
il.custom_minimum_size = Vector2(0, min_height)
|
||||
if expand:
|
||||
il.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(il); return il
|
||||
|
||||
func _add_button(text: String, cb: Callable) -> Button:
|
||||
var btn := Button.new(); btn.text = text
|
||||
btn.pressed.connect(cb); add_child(btn); return btn
|
||||
|
||||
func _add_button_to(text: String, parent: Control, cb: Callable) -> Button:
|
||||
var btn := Button.new()
|
||||
btn.text = text
|
||||
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
btn.pressed.connect(cb); parent.add_child(btn); return btn
|
||||
|
||||
func _add_row(label_text: String) -> HBoxContainer:
|
||||
var row := HBoxContainer.new(); add_child(row)
|
||||
var lbl := Label.new()
|
||||
lbl.text = label_text
|
||||
lbl.custom_minimum_size = Vector2(90, 0)
|
||||
row.add_child(lbl); return row
|
||||
|
||||
func _add_client_row() -> void:
|
||||
var row := _add_row("Client path")
|
||||
_client_path_edit = LineEdit.new()
|
||||
_client_path_edit.text = "res://sources"
|
||||
_client_path_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_client_path_edit.placeholder_text = "res://sources"
|
||||
row.add_child(_client_path_edit)
|
||||
|
||||
func _add_locale_row() -> void:
|
||||
var row := _add_row("Locale")
|
||||
_locale_option = OptionButton.new()
|
||||
_locale_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
for loc in LOCALES:
|
||||
_locale_option.add_item(loc)
|
||||
row.add_child(_locale_option)
|
||||
|
||||
func _add_filter_row() -> void:
|
||||
var row := _add_row("Filter")
|
||||
_filter_edit = LineEdit.new()
|
||||
_filter_edit.text = "*"
|
||||
_filter_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_filter_edit.placeholder_text = "DBFilesClient\\*.dbc"
|
||||
row.add_child(_filter_edit)
|
||||
|
||||
func _add_output_row() -> void:
|
||||
var row := _add_row("Output dir")
|
||||
_output_edit = LineEdit.new()
|
||||
_output_edit.text = "res://extracted"
|
||||
_output_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_child(_output_edit)
|
||||
|
||||
|
||||
# ─── Handlers ─────────────────────────────────────────────────────────────────
|
||||
func _on_open_pressed() -> void:
|
||||
if not ClassDB.class_exists("MPQManager"):
|
||||
_set_status("ERROR: MPQManager не найден. Собери GDExtension (build.bat Release).")
|
||||
return
|
||||
|
||||
if _mpq:
|
||||
_mpq.call("close")
|
||||
_mpq = ClassDB.instantiate("MPQManager")
|
||||
|
||||
var client_path: String = ProjectSettings.globalize_path(_client_path_edit.text)
|
||||
var locale: String = LOCALES[_locale_option.selected]
|
||||
|
||||
_set_status("Opening archives…")
|
||||
var count: int = _mpq.call("open_client", client_path, locale)
|
||||
|
||||
if count == 0:
|
||||
_set_status("ERROR: no archives opened. Check client path / locale.")
|
||||
_extract_btn.disabled = true
|
||||
_list_btn.disabled = true
|
||||
return
|
||||
|
||||
_archive_list.clear()
|
||||
var info_list: Array = _mpq.call("get_archive_info")
|
||||
for info in info_list:
|
||||
var lbl_text := "[%d] %s (%d files)" % [
|
||||
info["priority"],
|
||||
(info["path"] as String).get_file(),
|
||||
info["file_count"]
|
||||
]
|
||||
_archive_list.add_item(lbl_text)
|
||||
|
||||
_set_status("Opened %d archives. Locale: %s" % [count, locale])
|
||||
_extract_btn.disabled = false
|
||||
_list_btn.disabled = false
|
||||
|
||||
|
||||
func _on_list_pressed() -> void:
|
||||
if not _mpq: return
|
||||
_file_list.clear()
|
||||
_set_status("Listing files…")
|
||||
|
||||
var filter: String = _filter_edit.text if not _filter_edit.text.is_empty() else "*"
|
||||
var files: PackedStringArray = _mpq.call("list_files", filter)
|
||||
for f in files:
|
||||
_file_list.add_item(f)
|
||||
_set_status("Found %d files matching '%s'" % [files.size(), filter])
|
||||
|
||||
|
||||
func _on_extract_pressed() -> void:
|
||||
if not _mpq: return
|
||||
|
||||
var filter: String = _filter_edit.text if not _filter_edit.text.is_empty() else "*"
|
||||
var output_dir: String = ProjectSettings.globalize_path(_output_edit.text)
|
||||
|
||||
_set_status("Extracting…")
|
||||
_progress.visible = true
|
||||
_progress.value = 0
|
||||
|
||||
var count: int = _mpq.call("extract_files", filter, output_dir)
|
||||
|
||||
_progress.value = 100
|
||||
_progress.visible = false
|
||||
_set_status("Extracted %d files → %s" % [count, output_dir])
|
||||
|
||||
|
||||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
func _set_status(text: String) -> void:
|
||||
_status_label.text = text
|
||||
print("[MPQTool] ", text)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1s1aqegqtlg
|
||||
@@ -0,0 +1,102 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://mpq_tool"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/mpq_extractor/ui/mpq_tool.gd" id="1_script"]
|
||||
|
||||
[node name="MPQTool" type="VBoxContainer"]
|
||||
script = ExtResource("1_script")
|
||||
custom_minimum_size = Vector2(280, 0)
|
||||
|
||||
[node name="Title" type="Label" parent="."]
|
||||
text = "MPQ Extractor (WoW 3.3.5a)"
|
||||
theme_override_font_sizes/font_size = 14
|
||||
|
||||
[node name="HSep1" type="HSeparator" parent="."]
|
||||
|
||||
[node name="ClientPathRow" type="HBoxContainer" parent="."]
|
||||
|
||||
[node name="ClientLabel" type="Label" parent="ClientPathRow"]
|
||||
text = "Client path"
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
|
||||
[node name="ClientPathEdit" type="LineEdit" parent="ClientPathRow"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "res://sources"
|
||||
|
||||
[node name="LocaleRow" type="HBoxContainer" parent="."]
|
||||
|
||||
[node name="LocaleLabel" type="Label" parent="LocaleRow"]
|
||||
text = "Locale"
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
|
||||
[node name="LocaleOption" type="OptionButton" parent="LocaleRow"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="OpenBtn" type="Button" parent="."]
|
||||
unique_name_in_owner = true
|
||||
text = "Open Client Archives"
|
||||
|
||||
[node name="HSep2" type="HSeparator" parent="."]
|
||||
|
||||
[node name="ArchiveLabel" type="Label" parent="."]
|
||||
text = "Loaded archives (highest priority first):"
|
||||
|
||||
[node name="ArchiveList" type="ItemList" parent="."]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 110)
|
||||
|
||||
[node name="HSep3" type="HSeparator" parent="."]
|
||||
|
||||
[node name="FilterRow" type="HBoxContainer" parent="."]
|
||||
|
||||
[node name="FilterLabel" type="Label" parent="FilterRow"]
|
||||
text = "Filter"
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
|
||||
[node name="FilterEdit" type="LineEdit" parent="FilterRow"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "DBFilesClient\\*.dbc"
|
||||
text = "*"
|
||||
|
||||
[node name="OutputRow" type="HBoxContainer" parent="."]
|
||||
|
||||
[node name="OutputLabel" type="Label" parent="OutputRow"]
|
||||
text = "Output dir"
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
|
||||
[node name="OutputEdit" type="LineEdit" parent="OutputRow"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "res://extracted"
|
||||
text = "res://extracted"
|
||||
|
||||
[node name="ActionRow" type="HBoxContainer" parent="."]
|
||||
|
||||
[node name="ListBtn" type="Button" parent="ActionRow"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
text = "List Files"
|
||||
|
||||
[node name="ExtractBtn" type="Button" parent="ActionRow"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
text = "Extract"
|
||||
|
||||
[node name="ProgressBar" type="ProgressBar" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
|
||||
[node name="FileListLabel" type="Label" parent="."]
|
||||
text = "Files matching filter:"
|
||||
|
||||
[node name="FileList" type="ItemList" parent="."]
|
||||
unique_name_in_owner = true
|
||||
size_flags_vertical = 3
|
||||
custom_minimum_size = Vector2(0, 200)
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="."]
|
||||
unique_name_in_owner = true
|
||||
text = "Ready."
|
||||
autowrap_mode = 3
|
||||
Reference in New Issue
Block a user