diff options
Diffstat (limited to 'extractor')
-rw-r--r-- | extractor/actions.gd | 1 | ||||
-rw-r--r-- | extractor/extractor_interface.gd | 45 | ||||
-rw-r--r-- | extractor/extractor_interface.tscn | 98 | ||||
-rw-r--r-- | extractor/gnd_format.gd | 130 | ||||
-rw-r--r-- | extractor/grf.gd | 49 | ||||
-rw-r--r-- | extractor/rsm_format.gd | 65 | ||||
-rw-r--r-- | extractor/rsw_format.gd | 71 |
7 files changed, 429 insertions, 30 deletions
diff --git a/extractor/actions.gd b/extractor/actions.gd index 5c94cef..e5dafda 100644 --- a/extractor/actions.gd +++ b/extractor/actions.gd @@ -1,3 +1,4 @@ +class_name Actions extends Node2D diff --git a/extractor/extractor_interface.gd b/extractor/extractor_interface.gd new file mode 100644 index 0000000..4d74a20 --- /dev/null +++ b/extractor/extractor_interface.gd @@ -0,0 +1,45 @@ +extends Control + + +func _ready() -> void: + pass + + var grf = GRF.open("res://client_data/data.grf") + #grf.extract("res://client_data") + grf.convert("res://client_data") + + #Sprite.from_bytes(FileAccess.get_file_as_bytes("res://client_data/data/sprite/cursors.spr")) + #ActionFormat.from_bytes( + #ByteStream.from_bytes( + #FileAccess.get_file_as_bytes("res://client_data/data/sprite/cursors.act") + #) + #) + #GATFormat.from_bytes( + #ByteStream.from_bytes( + #FileAccess.get_file_as_bytes("res://client_data/data/int_land02.gat") + #) + #) + #GNDFormat.from_bytes( + #ByteStream.from_bytes( + #FileAccess.get_file_as_bytes("res://client_data/data/int_land02.gnd") + #) + #) + #var rsw = RSWFormat.from_bytes( + #ByteStream.from_bytes( + ##FileAccess.get_file_as_bytes("res://client_data/data/int_land02.rsw") + #FileAccess.get_file_as_bytes("res://client_data/data/pay_dun00.rsw") + #) + #) + #RSMFormat.from_bytes( + #ByteStream.from_bytes( + ##FileAccess.get_file_as_bytes("res://client_data/data/model/prontera/chair_01.rsm") + #FileAccess.get_file_as_bytes("res://client_data/data/model/izlude/iz_academy.rsm") + ## TODO: not parseable + ##FileAccess.get_file_as_bytes("res://client_data/data/model/graywolf/bridge_e_01.rsm2") + #) + #) + + #var scene_root := rsw.convert("pay_dun00", "res://client_data") + #var scene := PackedScene.new() + #scene.pack(scene_root) + #ResourceSaver.save(scene, "res://extractor/test/pay_dun00.tscn") diff --git a/extractor/extractor_interface.tscn b/extractor/extractor_interface.tscn new file mode 100644 index 0000000..7220d5c --- /dev/null +++ b/extractor/extractor_interface.tscn @@ -0,0 +1,98 @@ +[gd_scene load_steps=3 format=3 uid="uid://bfi14ujn6fe1n"] + +[ext_resource type="Script" uid="uid://db2yelol3joq0" path="res://extractor/extractor_interface.gd" id="1_hyoq1"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hyoq1"] +bg_color = Color(0.287085, 0.287085, 0.287085, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.47166, 0.47166, 0.471659, 1) + +[node name="ExtractorInterface" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_hyoq1") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 +offset_left = 387.0 +offset_top = 307.0 +offset_right = 765.0 +offset_bottom = 340.0 + +[node name="PanelContainer" type="PanelContainer" parent="HBoxContainer"] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_hyoq1") + +[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/PanelContainer"] +layout_mode = 2 +theme_override_constants/separation = 0 + +[node name="LineEdit" type="LineEdit" parent="HBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +text = "res://client_data/data.grf" +expand_to_text_length = true + +[node name="Button" type="Button" parent="HBoxContainer/PanelContainer/HBoxContainer"] +layout_mode = 2 +text = "Select File" + +[node name="Button" type="Button" parent="HBoxContainer"] +layout_mode = 2 +text = "Open GRF" + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_left = 386.0 +offset_top = 349.0 +offset_right = 754.0 +offset_bottom = 389.0 + +[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="PanelContainer2" type="PanelContainer" parent="VBoxContainer/HBoxContainer2"] +layout_mode = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_hyoq1") + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer2/PanelContainer2"] +layout_mode = 2 +theme_override_constants/separation = 0 + +[node name="LineEdit" type="LineEdit" parent="VBoxContainer/HBoxContainer2/PanelContainer2/HBoxContainer"] +layout_mode = 2 +text = "res://client_data" +expand_to_text_length = true + +[node name="Button" type="Button" parent="VBoxContainer/HBoxContainer2/PanelContainer2/HBoxContainer"] +layout_mode = 2 +text = "Select Directory" + +[node name="Button" type="Button" parent="VBoxContainer/HBoxContainer2"] +layout_mode = 2 +text = "Extract GRF" + +[node name="ProgressBar" type="ProgressBar" parent="VBoxContainer"] +layout_mode = 2 + +[node name="Button" type="Button" parent="."] +layout_mode = 0 +offset_left = 272.0 +offset_top = 433.0 +offset_right = 476.0 +offset_bottom = 464.0 +text = "Convert Sprites & Actions" + +[node name="Button2" type="Button" parent="."] +layout_mode = 0 +offset_left = 498.0 +offset_top = 434.0 +offset_right = 614.0 +offset_bottom = 465.0 +text = "Convert Maps" diff --git a/extractor/gnd_format.gd b/extractor/gnd_format.gd index ea8d965..c49bce6 100644 --- a/extractor/gnd_format.gd +++ b/extractor/gnd_format.gd @@ -1,6 +1,9 @@ class_name GNDFormat +const MAP_TILE_SIZE := 10.0 + + ## Byte Length: 4 [br] ## GRAT var signature: String = "GRGN" @@ -88,7 +91,7 @@ static func from_bytes(bytes: ByteStream) -> GNDFormat: gnd_format.texture_paths = [] for _n in gnd_format.texture_count: gnd_format.texture_paths.append( - bytes.get_string_from_ascii(gnd_format.texture_path_length) + bytes.get_string_from_ro(gnd_format.texture_path_length) ) gnd_format.light_map_slice_count = bytes.decode_s32() @@ -120,6 +123,76 @@ static func from_bytes(bytes: ByteStream) -> GNDFormat: return gnd_format +func get_cubes() -> Array: + # TODO: return custom type with helper functions for getting neighboring cubes and building a full cube + + var cubes := [] + for cube in ground_mesh_cubes: + cubes.append({ + "cube": cube, + Vector3(0, 1, 0): { + "surface": surfaces[cube.upwards_facing_surface_index], + "mesh": surfaces[cube.upwards_facing_surface_index].get_mesh({ + Vector2(0, 0): Vector3(0, cube.top_left_height, 0), + Vector2(1, 0): Vector3(MAP_TILE_SIZE, cube.top_right_height, 0), + Vector2(1, 1): Vector3(MAP_TILE_SIZE, cube.bottom_right_height, MAP_TILE_SIZE), + Vector2(0, 1): Vector3(0, cube.bottom_left_height, MAP_TILE_SIZE), + }), + }, + }) + + return cubes + + +func convert(data_path: String) -> GridMap: + var grid_map := GridMap.new() + grid_map.cell_size = Vector3(GNDFormat.MAP_TILE_SIZE, GNDFormat.MAP_TILE_SIZE, GNDFormat.MAP_TILE_SIZE) + grid_map.cell_center_x = false + grid_map.cell_center_y = false + grid_map.cell_center_z = false + + grid_map.position = Vector3( + (width * MAP_TILE_SIZE) / 2.0, + 0, + -(height * MAP_TILE_SIZE) / 2.0, + ) + + var library := MeshLibrary.new() + grid_map.mesh_library = library + + var cubes := get_cubes() + + var cache := {} + # TODO: use texture_index and surface uvs as key + # TODO: for deduplication of cell items (as long as other sides aren't accounted for..) + + for x in width: + for y in height: + var cube = cubes[x + y * width] + + # TODO: get all sides of a cube and add that mesh to the library + # TODO: so a single mesh per cube + for surface_type in [Vector3(0, 1, 0)]: + var mesh: ArrayMesh = cube[surface_type].mesh + var material := StandardMaterial3D.new() + material.albedo_texture = load( + "%s/data/texture/%s" % [ + data_path, + texture_paths[cube[surface_type].surface.texture_index] + ] + ) + + mesh.surface_set_material(0, material) + + var id := library.get_last_unused_item_id() + library.create_item(id) + library.set_item_mesh(id, mesh) + + grid_map.set_cell_item(Vector3(-x, 0, y), id) + + return grid_map + + class LightMapSlice: ## Byte Type: i32 [br] ## Byte Length: 4 @@ -229,6 +302,59 @@ class Surface: ) + func get_mesh(vertices: Dictionary[Vector2, Vector3]) -> ArrayMesh: + var surface_tool := SurfaceTool.new() + surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES) + + var uvs = get_uvs() + var color = get_vertex_color() + + surface_tool.add_triangle_fan( + PackedVector3Array([ + vertices[Vector2(0, 0)], + vertices[Vector2(1, 0)], + vertices[Vector2(1, 1)], + ]), + PackedVector2Array([ + uvs[Vector2(0, 0)], + uvs[Vector2(1, 0)], + uvs[Vector2(1, 1)], + ]), + PackedColorArray([ + color, color, color, + ]) + ) + surface_tool.add_triangle_fan( + PackedVector3Array([ + vertices[Vector2(0, 0)], + vertices[Vector2(1, 1)], + vertices[Vector2(0, 1)], + ]), + PackedVector2Array([ + uvs[Vector2(0, 0)], + uvs[Vector2(1, 1)], + uvs[Vector2(0, 1)], + ]), + PackedColorArray([ + color, color, color, + ]) + ) + + return surface_tool.commit() + + + func get_uv_id() -> String: + # TODO: add more data? + return ( + "%d %d %d %d %d %d %d %d" % [ + u_top_left, v_top_left, + u_top_right, v_top_right, + u_bottom_right, v_bottom_right, + u_bottom_left, v_bottom_left + ] + ).md5_text() + + static func from_bytes(bytes: ByteStream) -> Surface: var surface = Surface.new() @@ -301,3 +427,5 @@ class GroundMeshCube: #print(inst_to_dict(mesh)) return mesh + + # TODO: convert function to build cube mesh? or at least with neighbor input build full cube struct diff --git a/extractor/grf.gd b/extractor/grf.gd index a723fdc..5b7046f 100644 --- a/extractor/grf.gd +++ b/extractor/grf.gd @@ -156,7 +156,7 @@ class FileEntry: var file_access: FileAccess -static func open(path: String): +static func open(path: String) -> GRF: var grf = GRF.new() grf.file_access = FileAccess.open(path, FileAccess.ModeFlags.READ) @@ -190,10 +190,13 @@ static func open(path: String): return grf +signal extracted_file(index: int) + func extract(destination: String = "user://client_data"): DirAccess.make_dir_recursive_absolute(destination) - for file_entry in file_entries: + for idx in file_entries.size(): + var file_entry := file_entries[idx] var file_path: String = file_entry.get_file_path() var base_directory = DirAccess.open(destination) @@ -201,6 +204,8 @@ func extract(destination: String = "user://client_data"): var file = FileAccess.open("%s/%s" % [destination, file_path], FileAccess.WRITE_READ) file.store_buffer(file_entry.get_contents(file_access)) + + extracted_file.emit(idx) func convert(destination: String = "res://client_data"): @@ -220,17 +225,40 @@ func convert(destination: String = "res://client_data"): #DirAccess.make_dir_recursive_absolute(base_file_directory_path) + # BMP + if file_path.ends_with(".bmp"): + #continue + if not FileAccess.file_exists("%s/%s" % [destination, file_path]): + continue + + # load existing bmp files, so language specific overwrites are kept + + var texture: CompressedTexture2D = load("%s/%s" % [destination, file_path]) + if not texture: + # TODO: check if .godot/imported file is there (alrdy sufficient?) + continue + + var texture_image := texture.get_image() + texture_image.decompress() + var image := BMPTexture.convert_image( + texture_image, + [Color.MAGENTA] + ) + image.save_png("%s/%s" % [destination, file_path.replace(".bmp", ".png")]) + + + continue # Sprite.spr and Action.act - var player_head_path_part = "¸Ó¸®Åë" - var player_body_path_part = "¸öÅë" + var player_head_path_part = "머리통" + var player_body_path_part = "몸통" - if file_path.ends_with(".spr") and file_path.contains(player_head_path_part): + if file_path.ends_with(".spr"): #and (file_path.contains(player_head_path_part) or file_path.contains(player_body_path_part)): continue var sprite = SpriteFormat.from_bytes(file_entry.get_contents(file_access)) sprite.save_to_file(base_file_directory_path) - elif file_path.ends_with(".act") and file_path.contains(player_head_path_part): #or file_path.contains(player_body_path_part): + elif file_path.ends_with(".act") and file_path.contains("cursors"): #and (file_path.contains(player_head_path_part) or file_path.contains(player_body_path_part)): #continue if not FileAccess.file_exists("%s/000.png.import" % base_file_directory_path): continue @@ -245,6 +273,7 @@ func convert(destination: String = "res://client_data"): ResourceSaver.save(scene, "%s/actions.tscn" % base_file_directory_path) + continue # Map.rsw and .gnd and .gat if file_path.ends_with(".rsw") and (file_path.contains("pay_dun") or file_path.contains("iz_int") or file_path.contains("int_land")): @@ -257,5 +286,9 @@ func convert(destination: String = "res://client_data"): static func decode_string(bytes: PackedByteArray): - # TODO: use iconv to decode EUC-KR - return bytes.get_string_from_ascii() + Engine.print_error_messages = false + var string := bytes.get_string_from_multibyte_char("EUC-KR") + Engine.print_error_messages = true + if string.is_empty(): + return bytes.get_string_from_ascii() + return string diff --git a/extractor/rsm_format.gd b/extractor/rsm_format.gd index cf83af9..7f16368 100644 --- a/extractor/rsm_format.gd +++ b/extractor/rsm_format.gd @@ -39,10 +39,12 @@ var texture_count: int ## Length: [member texture_count] ## Byte Length: 40 +## Versions: [<2.3] var texture_names: Array[String] ## Byte Type: u8 ## Byte Length: 40 +## Versions: [<2.2] var root_node_name: String ## Byte Type: u32 [br] @@ -90,23 +92,24 @@ static func from_bytes(bytes: ByteStream) -> RSMFormat: rsm_format.texture_names = [] as Array[String] for _n in rsm_format.texture_count: - rsm_format.texture_names.append(bytes.get_string_from_utf8(40)) + rsm_format.texture_names.append(bytes.get_string_from_ro(40)) - rsm_format.root_node_name = bytes.get_string_from_utf8(40) + if version.lower_than(2, 2): + rsm_format.root_node_name = bytes.get_string_from_ro(40) if version.higher_than(2, 1): # >= 2.2 rsm_format.root_node_count = bytes.decode_u32() rsm_format.root_node_names = [] as Array[String] for _n in rsm_format.root_node_count: - rsm_format.root_node_names.append(bytes.get_string_from_utf8(40)) + rsm_format.root_node_names.append(bytes.get_string_from_ro(40)) rsm_format.node_count = bytes.decode_u32() rsm_format.nodes = [] as Array[ModelNode] for _n in rsm_format.node_count: rsm_format.nodes.append(ModelNode.from_bytes(bytes, version)) - print(inst_to_dict(rsm_format)) + #print(inst_to_dict(rsm_format)) #print(inst_to_dict(rsm_format.nodes[0].texture_coordinates[0])) #rsm_format.nodes[0].texture_coordinates.clear() #print(inst_to_dict(rsm_format.nodes[0].faces[0])) @@ -115,12 +118,16 @@ static func from_bytes(bytes: ByteStream) -> RSMFormat: return rsm_format -func convert() -> Node3D: - var node := Node3D.new() - node.name = root_node_name +func convert(data_path: String) -> Node3D: + var root_node := Node3D.new() + root_node.name = root_node_name #node.set_script(load("res://extractor/model.gd")) - return node + for model_node in nodes: + var node: Node = model_node.convert(texture_names, data_path) + root_node.add_child(node) + + return root_node class ModelNode: @@ -171,6 +178,7 @@ class ModelNode: ## Byte Type: f32 [br] ## Byte Length: 4 [br] ## Versions: [<2.2] + ## Type: Radiants var rotation_angle: float ## Byte Type: f32 [br] @@ -245,7 +253,7 @@ class ModelNode: var node = ModelNode.new() node.node_name = bytes.get_string_from_utf8(40) - node.parent_node_name = bytes.get_string_from_utf8(40) + node.parent_node_name = bytes.get_string_from_ro(40) if version.lower_than(2, 3): # < 2.3 node.texture_count = bytes.decode_u32() @@ -259,7 +267,7 @@ class ModelNode: node.texture_names = [] as Array[String] for _n in node.texture_name_count: - node.texture_names.append(bytes.get_string_from_utf8(40)) + node.texture_names.append(bytes.get_string_from_ro(40)) node.offset_matrix = [] as Array[Vector3] for _in in 3: @@ -345,6 +353,43 @@ class ModelNode: node.textures_keyframes.append(TexturesKeyframe.from_bytes(bytes)) return node + + + func convert(textures: Array[String], data_path: String): + var node := MeshInstance3D.new() + node.name = node_name + + node.translate(translation_2) + + if rotation_axis != Vector3.ZERO: + node.rotation = (rotation_axis * rotation_angle) * Vector3(1,-1,1) + + node.scale = scale + + var surface_tool := SurfaceTool.new() + surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES) + + var mesh := ArrayMesh.new() + for idx in faces.size(): + var face := faces[idx] + + surface_tool.add_triangle_fan( + PackedVector3Array(face.vertex_position_indices.map(func(item_idx): + return vertex_positions[item_idx]) + ), + PackedVector2Array(face.texture_coordinate_indices.map(func(item_idx): + return texture_coordinates[item_idx].coordinates) + ) + ) + surface_tool.commit(mesh) + + var material := StandardMaterial3D.new() + material.albedo_texture = load("%s/data/texture/%s" % [data_path, textures[face.texture_index]]) + mesh.surface_set_material(idx, material) + + node.mesh = mesh + + return node class TextureCoordinate: diff --git a/extractor/rsw_format.gd b/extractor/rsw_format.gd index 831a1cb..7e70850 100644 --- a/extractor/rsw_format.gd +++ b/extractor/rsw_format.gd @@ -117,7 +117,43 @@ func convert(name: String, data_path: String) -> Node3D: node.set_script(load("res://extractor/map.gd")) for resource in map_resources: - if resource is RSWFormat.SpatialAudioSource: + if resource is Animated3DModel: + var model_file_path := "%s/data/model/%s" % [data_path, resource.model_file] + if not FileAccess.file_exists(model_file_path): + continue + + var rsm := RSMFormat.from_bytes(ByteStream.from_bytes( + FileAccess.get_file_as_bytes(model_file_path) + )) + + var model_root := Node3D.new() + model_root.name = resource.name + model_root.position = resource.get_position() * Vector3(-1, 1, 1) + model_root.rotation_degrees = resource.get_rotation() * Vector3(-1,1,-1) + model_root.scale = resource.get_scale() + + node.add_child(model_root) + model_root.owner = node + + var model := rsm.convert(data_path) + model_root.add_child(model) + model.owner = node + for child in model.get_children(): + child.owner = node + + elif resource is DynamicLightSource: + continue + var light := OmniLight3D.new() + light.name = resource.name + light.position = resource.get_position() + light.light_color = resource.get_diffuse_color() + light.omni_range = resource.light_range + + node.add_child(light) + light.owner = node + + elif resource is SpatialAudioSource: + continue var audio_file_path := "%s/data/wav/%s" % [data_path, resource.audio_file] if not FileAccess.file_exists(audio_file_path): continue @@ -128,13 +164,23 @@ func convert(name: String, data_path: String) -> Node3D: audio.position = resource.get_position() audio.volume_linear = resource.volume_gain audio.max_distance = resource.audio_range + node.add_child(audio, true) audio.owner = node + + elif resource is ParticleEffectEmitter: + continue + var particles := CPUParticles3D.new() + particles.name = resource.name + particles.position = resource.get_position() + + node.add_child(particles, true) + particles.owner = node - var surface_tool := SurfaceTool.new() - for surface: GNDFormat.Surface in gnd.surfaces: - pass - #surface_tool.add_vertex() + var grid_map := gnd.convert(data_path) + grid_map.name = "Ground" + node.add_child(grid_map) + grid_map.owner = node return node @@ -312,14 +358,17 @@ class Animated3DModel extends MapResource: ## Byte Type: f32 [br] ## Byte Length: 4 + ## Type: Degrees var rotation_x: float ## Byte Type: f32 [br] ## Byte Length: 4 + ## Type: Degrees var rotation_y: float ## Byte Type: f32 [br] ## Byte Length: 4 + ## Type: Degrees var rotation_z: float ## Byte Type: f32 [br] @@ -350,11 +399,11 @@ class Animated3DModel extends MapResource: static func from_bytes(bytes: ByteStream) -> Animated3DModel: var resource = Animated3DModel.new() - resource.name = bytes.get_string_from_ascii(40) + resource.name = bytes.get_string_from_ro(40) resource.animation_type = bytes.decode_u32() resource.animation_speed_percent = bytes.decode_float() resource.collision_flags = bytes.decode_u32() - resource.model_file = bytes.get_string_from_ascii(80) + resource.model_file = bytes.get_string_from_ro(80) resource.root_node_name = bytes.get_string_from_utf8(80) resource.position_x = bytes.decode_float() resource.position_y = bytes.decode_float() @@ -415,7 +464,7 @@ class DynamicLightSource extends MapResource: static func from_bytes(bytes: ByteStream) -> DynamicLightSource: var resource = DynamicLightSource.new() - resource.name = bytes.get_string_from_ascii(80) + resource.name = bytes.get_string_from_ro(80) resource.position_x = bytes.decode_float() resource.position_y = bytes.decode_float() resource.position_z = bytes.decode_float() @@ -481,8 +530,8 @@ class SpatialAudioSource extends MapResource: static func from_bytes(bytes: ByteStream) -> SpatialAudioSource: var resource = SpatialAudioSource.new() - resource.name = bytes.get_string_from_ascii(80) - resource.audio_file = bytes.get_string_from_ascii(80) + resource.name = bytes.get_string_from_ro(80) + resource.audio_file = bytes.get_string_from_ro(80) resource.position_x = bytes.decode_float() resource.position_y = bytes.decode_float() resource.position_z = bytes.decode_float() @@ -546,7 +595,7 @@ class ParticleEffectEmitter extends MapResource: static func from_bytes(bytes: ByteStream) -> ParticleEffectEmitter: var resource = ParticleEffectEmitter.new() - resource.name = bytes.get_string_from_ascii(80) + resource.name = bytes.get_string_from_ro(80) resource.position_x = bytes.decode_float() resource.position_y = bytes.decode_float() resource.position_z = bytes.decode_float() |