summaryrefslogtreecommitdiff
path: root/extractor
diff options
context:
space:
mode:
Diffstat (limited to 'extractor')
-rw-r--r--extractor/action_format.gd184
-rw-r--r--extractor/actions.gd1
-rw-r--r--extractor/extractor_interface.gd45
-rw-r--r--extractor/extractor_interface.tscn98
-rw-r--r--extractor/gat_format.gd26
-rw-r--r--extractor/gnd_format.gd431
-rw-r--r--extractor/grf.gd262
-rw-r--r--extractor/map.gd34
-rw-r--r--extractor/rsm_format.gd653
-rw-r--r--extractor/rsw_format.gd699
-rw-r--r--extractor/version.gd20
11 files changed, 2248 insertions, 205 deletions
diff --git a/extractor/action_format.gd b/extractor/action_format.gd
index 97f6771..25a794e 100644
--- a/extractor/action_format.gd
+++ b/extractor/action_format.gd
@@ -49,7 +49,7 @@ func get_byte_length() -> int:
static func from_bytes(bytes: ByteStream) -> ActionFormat:
var action_format = ActionFormat.new()
- bytes.seek(2)
+ bytes.advance(action_format.signature.length())
@warning_ignore("shadowed_variable")
var version = Version.new()
@@ -82,6 +82,188 @@ static func from_bytes(bytes: ByteStream) -> ActionFormat:
return action_format
+func convert(name: String, sprites_path: String) -> Node2D:
+ var node := Node2D.new()
+ node.name = name
+ node.set_script(load("res://extractor/actions.gd"))
+
+ var animation_player := AnimationPlayer.new()
+ animation_player.name = "AnimationPlayer"
+ animation_player.unique_name_in_owner = true
+ node.add_child(animation_player)
+ animation_player.owner = node
+
+ var sprite_layers := CanvasGroup.new()
+ sprite_layers.name = "SpriteLayers"
+ sprite_layers.unique_name_in_owner = true
+
+ node.add_child(sprite_layers)
+ sprite_layers.owner = node
+
+ var track_properties = [
+ "animation",
+ "frame",
+ "speed_scale",
+ "position",
+ "self_modulate",
+ "scale",
+ "rotation_degrees",
+ "flip_h",
+ "visible",
+ ]
+
+ var sprite_frames := SpriteFrames.new()
+ #sprite_frames.add_animation("default")
+ for img_file_path in DirAccess.get_files_at(sprites_path):
+ if img_file_path.ends_with(".png"):
+ sprite_frames.add_frame("default", load("%s/%s" % [sprites_path, img_file_path]))
+
+ var animation_library := AnimationLibrary.new()
+
+ # get max number of sprite layers for all actions
+ var action_sprite_layers_max_count = actions.reduce(func(accum, action: ActionData):
+ return max(accum, action.motions.reduce(func(accum2, motion: Motion):
+ return max(accum2, motion.sprite_layer_count)
+ , 0))
+ , 0)
+
+ # add Nodes for each sprite layer
+ for sprite_layer_idx in action_sprite_layers_max_count:
+ var sprite = AnimatedSprite2D.new()
+ sprite.centered = false # 必要!!
+ sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
+ sprite.sprite_frames = sprite_frames
+ sprite.name = str(sprite_layer_idx).pad_zeros(3)
+ sprite_layers.add_child(sprite)
+ sprite.owner = node
+
+ for action_idx in actions.size():
+ var action: ActionData = actions[action_idx]
+ var frame_timing_base := ((frame_times[action_idx] * 24) / 1000)
+
+ # TODO: check why this is necessary
+ if sprites_path.contains("cursors") and action_idx == 0:
+ frame_timing_base = ((frame_times[action_idx] * 24 * 2) / 1000)
+
+ # add animation for each action
+ var animation := Animation.new()
+ animation.loop_mode = Animation.LOOP_LINEAR
+ animation.length = frame_timing_base * action.motion_count
+ animation_library.add_animation(str(action_idx).pad_zeros(3), animation)
+
+ # get max number of sprite layers for current action motions
+ var motion_sprite_layers_max_count = action.motions.reduce(func(accum, motion: Motion):
+ return max(accum, motion.sprite_layer_count)
+ , 0)
+
+ # add animation tracks for each sprite layer
+ for sprite_layer_idx in motion_sprite_layers_max_count:
+ var sprite := sprite_layers.get_child(sprite_layer_idx)
+ for property_idx in track_properties.size():
+ var track_idx = (sprite_layer_idx * track_properties.size()) + property_idx
+ animation.add_track(Animation.TYPE_VALUE, track_idx)
+ animation.value_track_set_update_mode(track_idx, Animation.UPDATE_DISCRETE)
+ animation.track_set_path(
+ track_idx,
+ "%s:%s" % ["SpriteLayers/" + sprite.name, track_properties[property_idx]]
+ )
+
+ for i in range(motion_sprite_layers_max_count, action_sprite_layers_max_count):
+ var sprite := sprite_layers.get_child(i)
+ var track_idx = animation.add_track(Animation.TYPE_VALUE)
+ animation.track_set_path(
+ track_idx,
+ "%s:visible" % ["SpriteLayers/" + sprite.name]
+ )
+ animation.track_insert_key(track_idx, 0.0, false)
+
+ # add animation tracks
+ for motion_idx in action.motions.size():
+ var motion: Motion = action.motions[motion_idx]
+
+ # TODO: no animations to speak of available ? sprite_index = -1
+ #if motion.event_id == -1:
+ #continue
+
+ var timing = motion_idx * frame_timing_base
+ var visible_key = 0
+
+ # add visible = false animation tracks to other sprite_layers
+ for i in motion_sprite_layers_max_count:
+ var track_idx = i * track_properties.size() + track_properties.find("visible")
+ visible_key = animation.track_insert_key(track_idx, timing, false)
+
+ for sprite_layer_idx in motion.sprite_layers.size():
+ var layer: SpriteLayer = motion.sprite_layers[sprite_layer_idx]
+
+ if layer.sprite_index == -1:
+ continue
+
+ var track_base_idx = sprite_layer_idx * track_properties.size()
+
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("animation"),
+ timing,
+ "default"
+ )
+
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("frame"),
+ timing,
+ layer.sprite_index
+ )
+
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("speed_scale"),
+ timing,
+ 1.0
+ )
+
+ var layer_image := sprite_frames.get_frame_texture("default", layer.sprite_index)
+ var position: Vector2 = layer.get_position() - ceil(layer_image.get_size() / 2) # for fixing half pixel drawing
+ var rotated = layer_image.get_size().rotated(deg_to_rad(layer.rotation_degrees))
+ var distance = layer_image.get_size() - rotated
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("position"),
+ timing,
+ position + (distance / 2)
+ )
+
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("self_modulate"),
+ timing,
+ layer.get_color()
+ )
+
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("scale"),
+ timing,
+ layer.get_scale()
+ )
+
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("rotation_degrees"),
+ timing,
+ layer.rotation_degrees
+ )
+
+ animation.track_insert_key(
+ track_base_idx + track_properties.find("flip_h"),
+ timing,
+ layer.flip_h
+ )
+
+ animation.track_set_key_value(
+ track_base_idx + track_properties.find("visible"),
+ visible_key,
+ true
+ )
+
+ animation_player.add_animation_library("", animation_library)
+
+ return node
+
+
class ActionData:
## Byte Type: u32 [br]
## Byte Length: 4
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/gat_format.gd b/extractor/gat_format.gd
index 213972a..7192cbc 100644
--- a/extractor/gat_format.gd
+++ b/extractor/gat_format.gd
@@ -42,33 +42,41 @@ static func from_bytes(bytes: ByteStream) -> GATFormat:
return gat_format
+enum TileFlags {
+ Walkable = 0b00000001,
+ Water = 0b00000010,
+ Snipable = 0b00000100,
+ Cliff = 0b00001000,
+}
+
+
class Tile:
## Byte Type: f32 [br]
## Byte Length: 4 [br]
- ## Orignal Coordinates_ (0, 0)
+ ## Orignal Coordinates: (0, 0)
var bottom_left_altitude: int
## Byte Type: f32 [br]
## Byte Length: 4 [br]
- ## Orignal Coordinates_ (1, 0)
+ ## Orignal Coordinates: (1, 0)
var bottom_right_altitude: int
## Byte Type: f32 [br]
## Byte Length: 4 [br]
- ## Orignal Coordinates_ (0, 1)
+ ## Orignal Coordinates: (0, 1)
var top_left_altitude: int
## Byte Type: f32 [br]
## Byte Length: 4 [br]
- ## Orignal Coordinates_ (1, 1)
+ ## Orignal Coordinates: (1, 1)
var top_right_altitude: int
- ## Byte Type: u32 ?[br]
- ## Byte Length: 4 ?
+ ## Byte Type: u8 [br]
+ ## Byte Length: 1
var terrain_type: int
- func get_height_map() -> Dictionary: # Dictionary[Vector2, int]
+ func get_height_map() -> Dictionary[Vector2, int]:
return {
Vector2(0, 0): top_left_altitude,
Vector2(1, 0): top_right_altitude,
@@ -84,6 +92,8 @@ class Tile:
tile.bottom_right_altitude = bytes.decode_float()
tile.top_left_altitude = bytes.decode_float()
tile.top_right_altitude = bytes.decode_float()
- tile.terrain_type = bytes.decode_u32()
+ tile.terrain_type = bytes.decode_u8()
+
+ bytes.advance(3) # unused
return tile
diff --git a/extractor/gnd_format.gd b/extractor/gnd_format.gd
new file mode 100644
index 0000000..c49bce6
--- /dev/null
+++ b/extractor/gnd_format.gd
@@ -0,0 +1,431 @@
+class_name GNDFormat
+
+
+const MAP_TILE_SIZE := 10.0
+
+
+## Byte Length: 4 [br]
+## GRAT
+var signature: String = "GRGN"
+
+## Byte Type: u8 [br]
+## Byte Length: 2
+var version: Version
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var width: int
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var height: int
+
+## Byte Type: f32 [br]
+## Byte Length: 4 [br]
+## Always 10.
+var scale: float
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var texture_count: int
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+## Always 80.
+var texture_path_length: int
+
+## Byte Length: [member texture_count] * [member texture_path_length] [br]
+var texture_paths: Array
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var light_map_slice_count: int
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var light_map_width: int
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var light_map_slice_height: int
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var light_map_cells_per_grid: int
+
+## Length: [member light_map_slice_count]
+var light_map_slices: Array
+
+## Byte Type: i32 [br]
+## Byte Length: 4
+var surface_count: int
+
+## Length: [member surface_count]
+var surfaces: Array[Surface]
+
+## Length: [member width] * [member height]
+var ground_mesh_cubes: Array[GroundMeshCube]
+
+
+static func from_bytes(bytes: ByteStream) -> GNDFormat:
+ var gnd_format = GNDFormat.new()
+
+ bytes.advance(gnd_format.signature.length())
+
+ @warning_ignore("shadowed_variable")
+ var version = Version.new()
+ version.major = bytes.decode_u8()
+ version.minor = bytes.decode_u8()
+ gnd_format.version = version
+
+ if version.minor > 7:
+ print(version)
+ return gnd_format
+
+ gnd_format.width = bytes.decode_s32()
+ gnd_format.height = bytes.decode_s32()
+ gnd_format.scale = bytes.decode_float()
+ gnd_format.texture_count = bytes.decode_s32()
+ gnd_format.texture_path_length = bytes.decode_s32()
+
+ gnd_format.texture_paths = []
+ for _n in gnd_format.texture_count:
+ gnd_format.texture_paths.append(
+ bytes.get_string_from_ro(gnd_format.texture_path_length)
+ )
+
+ gnd_format.light_map_slice_count = bytes.decode_s32()
+ gnd_format.light_map_width = bytes.decode_s32()
+ gnd_format.light_map_slice_height = bytes.decode_s32()
+ gnd_format.light_map_cells_per_grid = bytes.decode_s32()
+
+ #gnd_format.light_map_slices = []
+ #for _n in gnd_format.light_map_slice_count:
+ #gnd_format.light_map_slices.append(LightMapSlice.from_bytes(bytes))
+ bytes.advance(
+ gnd_format.light_map_slice_count *
+ gnd_format.light_map_width *
+ gnd_format.light_map_slice_height *
+ 4
+ )
+
+ gnd_format.surface_count = bytes.decode_s32()
+
+ gnd_format.surfaces = [] as Array[Surface]
+ for _n in gnd_format.surface_count:
+ gnd_format.surfaces.append(Surface.from_bytes(bytes))
+
+ gnd_format.ground_mesh_cubes = [] as Array[GroundMeshCube]
+ for _n in (gnd_format.width * gnd_format.height):
+ gnd_format.ground_mesh_cubes.append(GroundMeshCube.from_bytes(bytes))
+
+ #print(inst_to_dict(gnd_format))
+ 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
+ var pixel_format: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var width: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var height: int
+
+ ## Byte Length: [member width] * [member height]
+ var shadow_map_pixels: ByteStream
+
+ ## Byte Length: [member width] * [member height]
+ var light_map_pixels: ByteStream
+
+
+ static func from_bytes(bytes: ByteStream) -> LightMapSlice:
+ var slice = LightMapSlice.new()
+
+ slice.pixel_format = bytes.decode_s32()
+ slice.width = bytes.decode_s32()
+ slice.height = bytes.decode_s32()
+ slice.shadow_map_pixels = bytes.get_buffer(slice.width * slice.height)
+ slice.light_map_pixels = bytes.get_buffer(slice.width * slice.height)
+
+ #print(inst_to_dict(slice))
+ return slice
+
+
+class Surface:
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var u_bottom_left: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var u_bottom_right: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var u_top_left: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var u_top_right: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var v_bottom_left: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var v_bottom_right: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var v_top_left: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var v_top_right: float
+
+ ## Byte Type: i16 [br]
+ ## Byte Length: 2
+ var texture_index: int
+
+ ## Byte Type: i16 [br]
+ ## Byte Length: 2
+ var light_map_index: int
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 1
+ var vertex_color_blue: int
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 1
+ var vertex_color_green: int
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 1
+ var vertex_color_red: int
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 1
+ var vertex_color_alpha: int
+
+
+ func get_uvs() -> Dictionary[Vector2, Vector2]:
+ return {
+ Vector2(0, 0): Vector2(u_top_left, v_top_left),
+ Vector2(1, 0): Vector2(u_top_right, v_top_right),
+ Vector2(0, 1): Vector2(u_bottom_left, v_bottom_left),
+ Vector2(1, 1): Vector2(u_bottom_right, v_bottom_right),
+ }
+
+
+ func get_vertex_color() -> Color:
+ return Color8(
+ vertex_color_red,
+ vertex_color_green,
+ vertex_color_blue,
+ vertex_color_alpha
+ )
+
+
+ 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()
+
+ surface.u_bottom_left = bytes.decode_float()
+ surface.u_bottom_right = bytes.decode_float()
+ surface.u_top_left = bytes.decode_float()
+ surface.u_top_right = bytes.decode_float()
+ surface.v_bottom_left = bytes.decode_float()
+ surface.v_bottom_right = bytes.decode_float()
+ surface.v_top_left = bytes.decode_float()
+ surface.v_top_right = bytes.decode_float()
+ surface.texture_index = bytes.decode_s16()
+ surface.light_map_index = bytes.decode_s16()
+ surface.vertex_color_blue = bytes.decode_u8()
+ surface.vertex_color_green = bytes.decode_u8()
+ surface.vertex_color_red = bytes.decode_u8()
+ surface.vertex_color_alpha = bytes.decode_u8()
+
+ #print(inst_to_dict(surface))
+ return surface
+
+
+class GroundMeshCube:
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ ## upper left?
+ var bottom_left_height: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ ## upper right?
+ var bottom_right_height: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ ## lower left?
+ var top_left_height: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ ## lower right?
+ var top_right_height: float
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ ## top surface index?
+ var upwards_facing_surface_index: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ ## front surface index?
+ var northern_wall_surface_index: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ ## right surface index?
+ var eastern_wall_surface_index: int
+
+
+ static func from_bytes(bytes: ByteStream) -> GroundMeshCube:
+ var mesh = GroundMeshCube.new()
+
+ mesh.bottom_left_height = bytes.decode_float()
+ mesh.bottom_right_height = bytes.decode_float()
+ mesh.top_left_height = bytes.decode_float()
+ mesh.top_right_height = bytes.decode_float()
+ mesh.upwards_facing_surface_index = bytes.decode_s32()
+ mesh.northern_wall_surface_index = bytes.decode_s32()
+ mesh.eastern_wall_surface_index = bytes.decode_s32()
+
+ #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 9917a74..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,23 +190,30 @@ static func open(path: String):
return grf
-func extract(destination: String = "res://data"):
- for file_entry in file_entries:
+signal extracted_file(index: int)
+
+func extract(destination: String = "user://client_data"):
+ DirAccess.make_dir_recursive_absolute(destination)
+
+ 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)
- base_directory.make_dir_recursive("extracted/" + file_path.get_base_dir())
+ base_directory.make_dir_recursive(file_path.get_base_dir())
- var file = FileAccess.open("%s/extracted/%s" % [destination, file_path], FileAccess.WRITE_READ)
+ 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://data"):
+func convert(destination: String = "res://client_data"):
for file_entry in file_entries:
var file_path: String = file_entry.get_file_path()
var base_directory = DirAccess.open(destination)
- var base_directory_path = "extracted/%s" % file_path.get_base_dir()
+ var base_directory_path = "%s" % file_path.get_base_dir()
base_directory.make_dir_recursive(base_directory_path)
base_directory.change_dir(base_directory_path)
@@ -217,208 +224,71 @@ func convert(destination: String = "res://data"):
#DirAccess.make_dir_recursive_absolute(base_file_directory_path)
- var player_head_path_part = "¸Ó¸®Åë"
- var player_body_path_part = "¸öÅë"
- if file_path.ends_with(".spr") and file_path.contains(player_head_path_part):
+ # 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 = "몸통"
+
+ 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):
+ 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
- var scene := PackedScene.new()
- var scene_root := Node2D.new()
- scene_root.name = "Actions"
- scene_root.set_script(load("res://extractor/actions.gd"))
-
- var animation_player := AnimationPlayer.new()
- animation_player.name = "AnimationPlayer"
- animation_player.unique_name_in_owner = true
- scene_root.add_child(animation_player)
- animation_player.owner = scene_root
-
- var sprite_layers := CanvasGroup.new()
- sprite_layers.name = "SpriteLayers"
- sprite_layers.unique_name_in_owner = true
+ var action := ActionFormat.from_bytes(ByteStream.from_bytes(file_entry.get_contents(file_access)))
+ var scene_root := action.convert(file_name, base_file_directory_path)
- scene_root.add_child(sprite_layers)
- sprite_layers.owner = scene_root
-
- var track_properties = [
- "animation",
- "frame",
- "speed_scale",
- "position",
- "self_modulate",
- "scale",
- "rotation_degrees",
- "flip_h",
- "visible",
- ]
-
- var sprite_frames := SpriteFrames.new()
- #sprite_frames.add_animation("default")
- for img_file_path in DirAccess.get_files_at(base_file_directory_path):
- if img_file_path.ends_with(".png"):
- sprite_frames.add_frame("default", load("%s/%s" % [base_file_directory_path, img_file_path]))
-
- var animation_library := AnimationLibrary.new()
- var action_data := ActionFormat.from_bytes(ByteStream.from_bytes(file_entry.get_contents(file_access)))
-
- # get max number of sprite layers for all actions
- var action_sprite_layers_max_count = action_data.actions.reduce(func(accum, action: ActionFormat.ActionData):
- return max(accum, action.motions.reduce(func(accum2, motion: ActionFormat.Motion):
- return max(accum2, motion.sprite_layer_count)
- , 0))
- , 0)
-
- # add Nodes for each sprite layer
- for sprite_layer_idx in action_sprite_layers_max_count:
- var sprite = AnimatedSprite2D.new()
- sprite.centered = false # 必要!!
- sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
- sprite.sprite_frames = sprite_frames
- sprite.name = str(sprite_layer_idx).pad_zeros(3)
- sprite_layers.add_child(sprite)
- sprite.owner = scene_root
-
- for action_idx in action_data.actions.size():
- var action: ActionFormat.ActionData = action_data.actions[action_idx]
- var frame_timing_base := ((action_data.frame_times[action_idx] * 24) / 1000)
-
- if file_path.contains("cursors") and action_idx == 0:
- frame_timing_base = ((action_data.frame_times[action_idx] * 24 * 2) / 1000)
-
- # add animation for each action
- var animation := Animation.new()
- animation.loop_mode = Animation.LOOP_LINEAR
- animation.length = frame_timing_base * action.motion_count
- animation_library.add_animation(str(action_idx).pad_zeros(3), animation)
-
- # get max number of sprite layers for current action motions
- var motion_sprite_layers_max_count = action.motions.reduce(func(accum, motion: ActionFormat.Motion):
- return max(accum, motion.sprite_layer_count)
- , 0)
-
- # add animation tracks for each sprite layer
- for sprite_layer_idx in motion_sprite_layers_max_count:
- var sprite := sprite_layers.get_child(sprite_layer_idx)
- for property_idx in track_properties.size():
- var track_idx = (sprite_layer_idx * track_properties.size()) + property_idx
- animation.add_track(Animation.TYPE_VALUE, track_idx)
- animation.value_track_set_update_mode(track_idx, Animation.UPDATE_DISCRETE)
- animation.track_set_path(
- track_idx,
- "%s:%s" % ["SpriteLayers/" + sprite.name, track_properties[property_idx]]
- )
-
- for i in range(motion_sprite_layers_max_count, action_sprite_layers_max_count):
- var sprite := sprite_layers.get_child(i)
- var track_idx = animation.add_track(Animation.TYPE_VALUE)
- animation.track_set_path(
- track_idx,
- "%s:visible" % ["SpriteLayers/" + sprite.name]
- )
- animation.track_insert_key(track_idx, 0.0, false)
-
- # add animation tracks
- for motion_idx in action.motions.size():
- var motion: ActionFormat.Motion = action.motions[motion_idx]
-
- # TODO: no animations to speak of available ?
- if motion.event_id == -1:
- continue
-
- var timing = motion_idx * frame_timing_base
- var visible_key = 0
-
- # add visible = false animation tracks to other sprite_layers
- for i in motion_sprite_layers_max_count:
- var track_idx = i * track_properties.size() + track_properties.find("visible")
- visible_key = animation.track_insert_key(track_idx, timing, false)
-
- for sprite_layer_idx in motion.sprite_layers.size():
- var layer: ActionFormat.SpriteLayer = motion.sprite_layers[sprite_layer_idx]
-
- var track_base_idx = sprite_layer_idx * track_properties.size()
-
- animation.track_insert_key(
- track_base_idx + track_properties.find("animation"),
- timing,
- "default"
- )
-
- animation.track_insert_key(
- track_base_idx + track_properties.find("frame"),
- timing,
- layer.sprite_index
- )
-
- animation.track_insert_key(
- track_base_idx + track_properties.find("speed_scale"),
- timing,
- 1.0
- )
-
- var layer_image := sprite_frames.get_frame_texture("default", layer.sprite_index)
- var position: Vector2 = layer.get_position() - ceil(layer_image.get_size() / 2) # for fixing half pixel drawing
- var rotated = layer_image.get_size().rotated(deg_to_rad(layer.rotation_degrees))
- var distance = layer_image.get_size() - rotated
- animation.track_insert_key(
- track_base_idx + track_properties.find("position"),
- timing,
- position + (distance / 2)
- )
-
- animation.track_insert_key(
- track_base_idx + track_properties.find("self_modulate"),
- timing,
- layer.get_color()
- )
-
- animation.track_insert_key(
- track_base_idx + track_properties.find("scale"),
- timing,
- layer.get_scale()
- )
-
- animation.track_insert_key(
- track_base_idx + track_properties.find("rotation_degrees"),
- timing,
- layer.rotation_degrees
- )
-
- animation.track_insert_key(
- track_base_idx + track_properties.find("flip_h"),
- timing,
- layer.flip_h
- )
-
- animation.track_set_key_value(
- track_base_idx + track_properties.find("visible"),
- visible_key,
- true
- )
-
- animation_player.add_animation_library("", animation_library)
+ var scene := PackedScene.new()
scene.pack(scene_root)
# TODO: doesn't work if png is not imported via editor focus => run game twice
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")):
+ var rsw = RSWFormat.from_bytes(ByteStream.from_bytes(file_entry.get_contents(file_access)))
+ var scene_root := rsw.convert(file_name, "res://client_data")
+
+ var scene := PackedScene.new()
+ scene.pack(scene_root)
+ ResourceSaver.save(scene, "%s/%s/%s.tscn" % [destination, base_directory_path, file_name])
static func decode_string(bytes: PackedByteArray):
- return bytes.get_string_from_ascii()
- @warning_ignore("unreachable_code")
- # TODO: check unicode codepoints and parse accordingly
- var string = bytes.get_string_from_utf32()
- if string == "":
- string = bytes.get_string_from_utf16()
- if string == "":
- string = bytes.get_string_from_utf8()
- if string == "":
- string = 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/map.gd b/extractor/map.gd
new file mode 100644
index 0000000..c3a9713
--- /dev/null
+++ b/extractor/map.gd
@@ -0,0 +1,34 @@
+extends Node3D
+
+
+func _ready() -> void:
+ # add player
+ var map_server_login_success_packet: MapServerLoginSuccessPacket = Network.map_server.received_packets[MapServerLoginSuccessPacket.HEADER][0]
+ var initial_player_position: Vector2 = map_server_login_success_packet.get_position()
+
+ var player = preload("res://entities/player.tscn").instantiate()
+ player.position = Vector3(initial_player_position.x, 0, initial_player_position.y)
+ add_child(player)
+
+ # listen to packets
+ Network.map_server.received_packet.connect(func(packet: Packet):
+ if packet is ChangeMapPacket:
+ player.position.x = packet.get_position().x
+ player.position.z = packet.get_position().y
+ )
+
+ # play audio
+ for node: AudioStreamPlayer3D in find_children("se_*"):
+ node.play()
+
+ # add HUD TODO: add all HUD as HUD scene
+ var chat_window = preload("res://ui/chat_window.tscn").instantiate()
+ add_child(chat_window)
+
+ # TODO: load map.
+ # TODO: whatever else needs to be loaded after converting from rsw
+
+ var map_loaded_packet := MapLoadedPacket.new()
+ Network.map_server.send(map_loaded_packet)
+
+ # TODO: check which map server packets to send next
diff --git a/extractor/rsm_format.gd b/extractor/rsm_format.gd
new file mode 100644
index 0000000..7f16368
--- /dev/null
+++ b/extractor/rsm_format.gd
@@ -0,0 +1,653 @@
+class_name RSMFormat
+
+
+## Byte Length: 4 [br]
+## GRAT
+var signature: String = "GRSM"
+
+## Byte Type: u8 [br]
+## Byte Length: 2
+var version: Version
+
+## Byte Type: u32 [br]
+## Byte Length: 4
+var animation_length: int
+
+## Byte Type: u32 [br]
+## Byte Length: 4
+var shade_type: int
+
+## Byte Type: u8 [br]
+## Byte Length: 1
+## Versions: [>=1.4]
+var alpha: int
+
+## Byte Type: u8 [br]
+## Byte Length: 16
+## Versions: [<2.2]
+var reserved: PackedByteArray
+
+## Byte Type: f32 [br]
+## Byte Length: 4
+## Versions: [>=2.2]
+var frames_per_second: float
+
+## Byte Type: u32 [br]
+## Byte Length: 4
+## Versions: [<2.3]
+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]
+## Byte Length: 4
+## Versions: [>=2.2]
+var root_node_count: int
+
+## Length: [member root_node_count]
+## Byte Length: 40
+var root_node_names: Array[String]
+
+## Byte Type: u32 [br]
+## Byte Length: 4
+var node_count: int
+
+## Length: [member node_count]
+var nodes: Array[ModelNode]
+
+
+static func from_bytes(bytes: ByteStream) -> RSMFormat:
+ var rsm_format = RSMFormat.new()
+
+ bytes.advance(rsm_format.signature.length())
+
+ @warning_ignore("shadowed_variable")
+ var version = Version.new()
+ version.major = bytes.decode_u8()
+ version.minor = bytes.decode_u8()
+ rsm_format.version = version
+
+ rsm_format.animation_length = bytes.decode_u32()
+ rsm_format.shade_type = bytes.decode_u32()
+
+ if version.higher_than(1, 3): # >= 1.4
+ rsm_format.alpha = bytes.decode_u8()
+
+ if version.lower_than(2, 2): # < 2.2
+ rsm_format.reserved = bytes.get_buffer(16).bytes
+
+ if version.higher_than(2, 1): # >= 2.2
+ rsm_format.frames_per_second = bytes.decode_float()
+
+ if version.lower_than(2, 3): # < 2.3
+ rsm_format.texture_count = bytes.decode_u32()
+
+ rsm_format.texture_names = [] as Array[String]
+ for _n in rsm_format.texture_count:
+ rsm_format.texture_names.append(bytes.get_string_from_ro(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_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.nodes[0].texture_coordinates[0]))
+ #rsm_format.nodes[0].texture_coordinates.clear()
+ #print(inst_to_dict(rsm_format.nodes[0].faces[0]))
+ #rsm_format.nodes[0].faces.clear()
+ #print(inst_to_dict(rsm_format.nodes[0]))
+ return rsm_format
+
+
+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"))
+
+ 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:
+ ## Byte Type: u8 [br]
+ ## Byte Length: 40
+ var node_name: String
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 40
+ var parent_node_name: String
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ ## Versions: [<2.3]
+ var texture_count: int
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ ## Length: [member texture_count] [br]
+ ## Versions: [<2.3]
+ var texture_indices: Array[int]
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ ## Versions: [>=2.3]
+ var texture_name_count: int
+
+ ## Byte Length: 40 [br]
+ ## Length: [member texture_name_count] [br]
+ ## Versions: [>=2.3]
+ var texture_names: Array[String]
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 9 [br]
+ ## Length: 9 [br]
+ ## 3 x 3 Matrix. Each element represents a column.
+ var offset_matrix: Array[Vector3]
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 [br]
+ ## Versions: [<2.2]
+ var translation_1: Vector3
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 [br]
+ var translation_2: Vector3
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## Versions: [<2.2]
+ ## Type: Radiants
+ var rotation_angle: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 [br]
+ ## Versions: [<2.2]
+ var rotation_axis: Vector3
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 [br]
+ ## Versions: [<2.2]
+ var scale: Vector3
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var vertex_position_count: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 * [member vertex_position_count] [br]
+ ## Length: [member vertex_position_count]
+ var vertex_positions: Array[Vector3]
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var texture_coordinate_count: int
+
+ ## Length: [member texture_coordinate_count]
+ var texture_coordinates: Array[TextureCoordinate]
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var face_count: int
+
+ ## Length: [member face_count]
+ var faces: Array[Face]
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ ## Versions: [>=1.6]
+ var scale_keyframe_count: int
+
+ ## Length: [member scale_keyframe_count]
+ ## Versions: [>=1.6]
+ var scale_keyframes: Array[ScaleKeyframe]
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var rotation_keyframe_count: int
+
+ ## Length: [member scale_keyframe_count]
+ var rotation_keyframes: Array[RotationKeyframe]
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ ## Versions: [>=2.2]
+ var translation_keyframe_count: int
+
+ ## Length: [member scale_keyframe_count]
+ ## Versions: [>=2.2]
+ var translation_keyframes: Array[TranslationKeyframe]
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ ## Versions: [>=2.3]
+ var textures_keyframe_count: int
+
+ ## Length: [member scale_keyframe_count]
+ ## Versions: [>=2.3]
+ var textures_keyframes: Array[TexturesKeyframe]
+
+
+ static func from_bytes(bytes: ByteStream, version: Version) -> ModelNode:
+ var node = ModelNode.new()
+
+ node.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()
+
+ node.texture_indices = [] as Array[int]
+ for _n in node.texture_count:
+ node.texture_indices.append(bytes.decode_u32())
+
+ if version.higher_than(2, 2): # >= 2.3
+ node.texture_name_count = bytes.decode_u32()
+
+ node.texture_names = [] as Array[String]
+ for _n in node.texture_name_count:
+ node.texture_names.append(bytes.get_string_from_ro(40))
+
+ node.offset_matrix = [] as Array[Vector3]
+ for _in in 3:
+ node.offset_matrix.append(Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ ))
+
+ if version.lower_than(2, 2):
+ node.translation_1 = Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+
+ node.translation_2 = Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+
+ if version.lower_than(2, 2):
+ node.rotation_angle = bytes.decode_float()
+ node.rotation_axis = Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+ node.scale = Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+
+ node.vertex_position_count = bytes.decode_u32()
+ node.vertex_positions = [] as Array[Vector3]
+
+ for _n in node.vertex_position_count:
+ node.vertex_positions.append(Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ ))
+
+ node.texture_coordinate_count = bytes.decode_u32()
+ node.texture_coordinates = [] as Array[TextureCoordinate]
+
+ for _n in node.texture_coordinate_count:
+ node.texture_coordinates.append(TextureCoordinate.from_bytes(bytes, version))
+
+ node.face_count = bytes.decode_u32()
+ node.faces = [] as Array[Face]
+
+ for _n in node.face_count:
+ node.faces.append(Face.from_bytes(bytes, version))
+
+ if version.higher_than(1, 5): # >= 1.6
+ node.scale_keyframe_count = bytes.decode_u32()
+ node.scale_keyframes = [] as Array[ScaleKeyframe]
+
+ for _n in node.scale_keyframe_count:
+ node.scale_keyframes.append(ScaleKeyframe.from_bytes(bytes))
+
+ node.rotation_keyframe_count = bytes.decode_u32()
+ node.rotation_keyframes = [] as Array[RotationKeyframe]
+
+ for _n in node.rotation_keyframe_count:
+ node.rotation_keyframes.append(RotationKeyframe.from_bytes(bytes))
+
+ if version.higher_than(2, 1): # >= 2.2
+ node.translation_keyframe_count = bytes.decode_u32()
+ node.translation_keyframes = [] as Array[ScaleKeyframe]
+
+ for _n in node.translation_keyframe_count:
+ node.translation_keyframes.append(TranslationKeyframe.from_bytes(bytes))
+
+ if version.higher_than(2, 2): # >= 2.3
+ node.textures_keyframe_count = bytes.decode_u32()
+ node.textures_keyframes = [] as Array[TexturesKeyframe]
+
+ for _n in node.textures_keyframe_count:
+ 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:
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ ## Versions: [>=1.2]
+ var color: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 2 [br]
+ ## Note: possibly wrong if version < 1.2
+ var coordinates: Vector2
+
+
+ static func from_bytes(bytes: ByteStream, version: Version) -> TextureCoordinate:
+ var data = TextureCoordinate.new()
+
+ if version.higher_than(1, 1):
+ data.color = bytes.decode_u32()
+
+ data.coordinates = Vector2(
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+
+ return data
+
+
+class Face:
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ ## Versions: [>=2.2]
+ var length: int
+
+ ## Byte Type: u16 [br]
+ ## Byte Length: 2 * 3
+ ## Length: 3
+ var vertex_position_indices: Array[int]
+
+ ## Byte Type: u16 [br]
+ ## Byte Length: 2 * 3
+ ## Length: 3
+ var texture_coordinate_indices: Array[int]
+
+ ## Byte Type: u16 [br]
+ ## Byte Length: 2
+ var texture_index: int
+
+ ## Byte Type: u16 [br]
+ ## Byte Length: 2
+ var padding: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var two_sided: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var smooth_group: int
+
+ ## Byte Type: i32 [br]
+ ## Length: ([member length] - 24) / 4 [br]
+ ## Versions: [>=2.2]
+ # TODO: saturating_sub?
+ var smooth_group_extra: Array[int]
+
+
+ static func from_bytes(bytes: ByteStream, version: Version) -> Face:
+ var data = Face.new()
+
+ if version.higher_than(2, 1):
+ data.length = bytes.decode_u32()
+
+ data.vertex_position_indices = [] as Array[int]
+ for _in in 3:
+ data.vertex_position_indices.append(bytes.decode_u16())
+
+ data.texture_coordinate_indices = [] as Array[int]
+ for _in in 3:
+ data.texture_coordinate_indices.append(bytes.decode_u16())
+
+ data.texture_index = bytes.decode_u16()
+ data.padding = bytes.decode_u16()
+ data.two_sided = bytes.decode_s32()
+ data.smooth_group = bytes.decode_s32()
+
+ if version.higher_than(2, 1):
+ data.smooth_group_extra = [] as Array[int]
+
+ for _n in ((data.length - 24) / 4):
+ data.smooth_group_extra.append(bytes.decode_s32())
+
+ return data
+
+
+class ScaleKeyframe:
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ var frame: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 [br]
+ var scale: Vector3
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ var reserved: float
+
+
+ static func from_bytes(bytes: ByteStream) -> ScaleKeyframe:
+ var data = ScaleKeyframe.new()
+
+ data.frame = bytes.decode_u32()
+ data.scale = Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+ data.reserved = bytes.decode_float()
+
+ return data
+
+
+class RotationKeyframe:
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ var frame: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 [br]
+ var rotation: Vector4
+
+
+ static func from_bytes(bytes: ByteStream) -> RotationKeyframe:
+ var data = RotationKeyframe.new()
+
+ data.frame = bytes.decode_u32()
+ data.rotation = Vector4(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+
+ return data
+
+
+class TranslationKeyframe:
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ var frame: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 * 3 [br]
+ var translation: Vector3
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ var reserved: float
+
+
+ static func from_bytes(bytes: ByteStream) -> ScaleKeyframe:
+ var data = ScaleKeyframe.new()
+
+ data.frame = bytes.decode_u32()
+ data.translation = Vector3(
+ bytes.decode_float(),
+ bytes.decode_float(),
+ bytes.decode_float()
+ )
+ data.reserved = bytes.decode_float()
+
+ return data
+
+
+enum TextureOperation {
+ ## Texture translation on the X axis. The texture is tiled.
+ Translation_X,
+
+ ## Texture translation on the Y axis. The texture is tiled.
+ Translation_Y,
+
+ ## Texture multiplication on the X axis. The texture is tiled.
+ Scale_X,
+
+ ## Texture multiplication on the Y axis. The texture is tiled.
+ Scale_Y,
+
+ ## Texture rotation around (0, 0). The texture is not tiled.
+ Rotation,
+}
+
+
+class TexturesKeyframe:
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var texture_index: int
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var texture_keyframe_count: int
+
+ ## Length: [member texture_keyframe_count]
+ var texture_keyframes: Array[TextureKeyframe]
+
+
+ static func from_bytes(bytes: ByteStream) -> TexturesKeyframe:
+ var data = TexturesKeyframe.new()
+
+ data.texture_index = bytes.decode_u32()
+ data.texture_keyframe_count = bytes.decode_u32()
+
+ data.texture_keyframes = [] as Array[TextureKeyframe]
+ for _n in data.texture_keyframe_count:
+ data.texture_keyframes.append(TextureKeyframe.from_bytes(bytes))
+
+ return data
+
+
+class TextureKeyframe:
+ ## Byte Type: u8
+ var operation_type: TextureOperation
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var frame_count: int
+
+ ## Length: [member frame_count]
+ var texture_frames: Array[TextureFrame]
+
+
+ static func from_bytes(bytes: ByteStream) -> TextureKeyframe:
+ var data = TextureKeyframe.new()
+
+ data.operation_type = bytes.decode_u8()
+ data.frame_count = bytes.decode_u32()
+
+ data.texture_frames = [] as Array[TextureFrame]
+ for _n in data.frame_count:
+ data.texture_frames.append(TextureFrame.from_bytes(bytes))
+
+ return data
+
+
+class TextureFrame:
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4 [br]
+ var frame: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ var translation: float
+
+
+ static func from_bytes(bytes: ByteStream) -> TextureFrame:
+ var data = TextureFrame.new()
+
+ data.frame = bytes.decode_u32()
+ data.translation = bytes.decode_float()
+
+ return data
diff --git a/extractor/rsw_format.gd b/extractor/rsw_format.gd
new file mode 100644
index 0000000..7e70850
--- /dev/null
+++ b/extractor/rsw_format.gd
@@ -0,0 +1,699 @@
+class_name RSWFormat
+
+
+## Byte Length: 4 [br]
+## GRAT
+var signature: String = "GRSW"
+
+## Byte Type: u8 [br]
+## Byte Length: 2
+var version: Version
+
+## Byte Type: u8, u32 [br]
+## Byte Length: 1, 4
+## Versions: [2.2, 2.5]
+var build_number: int
+
+## Byte Type: u8 [br]
+## Byte Length: 1
+## Versions: [2.5]
+var unknown_render_flag: int
+
+## Byte Type: u8 [br]
+## Byte Length: 40
+var ini_file: String
+
+## Byte Type: u8 [br]
+## Byte Length: 40
+var gnd_file: String
+
+## Byte Type: u8 [br]
+## Byte Length: 40
+var gat_file: String
+
+## Byte Type: u8 [br]
+## Byte Length: 40
+## Versions: [1.4]
+var source_file: String
+
+var water_configuration: WaterConfiguration
+var lighting_parameters: LightingParameters
+
+## Byte Type: u32 [br]
+## Byte Length: 4
+var map_resource_count: int
+
+## Length: [member map_resource_count]
+var map_resources: Array[MapResource]
+
+var quad_tree
+
+
+static func from_bytes(bytes: ByteStream) -> RSWFormat:
+ var rsw_format = RSWFormat.new()
+
+ bytes.advance(rsw_format.signature.length())
+
+ @warning_ignore("shadowed_variable")
+ var version = Version.new()
+ version.major = bytes.decode_u8()
+ version.minor = bytes.decode_u8()
+ rsw_format.version = version
+
+ if version.major < 2:
+ print(version)
+ return rsw_format
+
+ if version.major >= 2 and version.minor >= 5:
+ rsw_format.build_number = bytes.decode_u32()
+ rsw_format.unknown_render_flag = bytes.decode_u8()
+ elif version.major >= 2 and version.minor >= 2:
+ rsw_format.build_number = bytes.decode_u8()
+
+ rsw_format.ini_file = bytes.get_string_from_utf8(40)
+ rsw_format.gnd_file = bytes.get_string_from_utf8(40)
+ rsw_format.gat_file = bytes.get_string_from_utf8(40)
+ rsw_format.source_file = bytes.get_string_from_utf8(40)
+
+ if version.major >= 2 and version.minor <= 5:
+ rsw_format.water_configuration = WaterConfiguration.from_bytes(bytes)
+
+ rsw_format.lighting_parameters = LightingParameters.from_bytes(bytes)
+
+ bytes.advance(16) # map boundaries - bounding box # TODO
+
+ rsw_format.map_resource_count = bytes.decode_u32()
+ rsw_format.map_resources = [] as Array[MapResource]
+ for _n in rsw_format.map_resource_count:
+ var resource_type = bytes.decode_u32()
+ var resource: MapResource
+
+ if resource_type == MapResourceType.Animated3DModel:
+ resource = Animated3DModel.from_bytes(bytes)
+ elif resource_type == MapResourceType.DynamicLightSource:
+ resource = DynamicLightSource.from_bytes(bytes)
+ elif resource_type == MapResourceType.SpatialAudioSource:
+ resource = SpatialAudioSource.from_bytes(bytes)
+ elif resource_type == MapResourceType.ParticleEffectEmitter:
+ resource = ParticleEffectEmitter.from_bytes(bytes)
+
+ rsw_format.map_resources.append(resource)
+
+ # TODO: quadtree
+
+ #print(inst_to_dict(rsw_format))
+ return rsw_format
+
+
+func convert(name: String, data_path: String) -> Node3D:
+ var gnd_file_path = "%s/data/%s" % [data_path, gnd_file]
+ var gnd = GNDFormat.from_bytes(ByteStream.from_bytes(FileAccess.get_file_as_bytes(gnd_file_path)))
+
+ var gat_file_path = "%s/data/%s" % [data_path, gat_file]
+ var gat = GATFormat.from_bytes(ByteStream.from_bytes(FileAccess.get_file_as_bytes(gat_file_path)))
+
+ var node := Node3D.new()
+ node.name = name
+ node.set_script(load("res://extractor/map.gd"))
+
+ for resource in map_resources:
+ 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
+
+ var audio = AudioStreamPlayer3D.new()
+ audio.stream = load(audio_file_path)
+ audio.name = resource.audio_file
+ 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 grid_map := gnd.convert(data_path)
+ grid_map.name = "Ground"
+ node.add_child(grid_map)
+ grid_map.owner = node
+
+ return node
+
+
+class WaterConfiguration:
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var water_level: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var water_type: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var wave_height: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var wave_speed: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var wave_pitch: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var water_animation_speed: int
+
+
+ static func from_bytes(bytes: ByteStream) -> WaterConfiguration:
+ var config = WaterConfiguration.new()
+
+ config.water_level = bytes.decode_float()
+ config.water_type = bytes.decode_u32()
+ config.wave_height = bytes.decode_float()
+ config.wave_speed = bytes.decode_float()
+ config.wave_pitch = bytes.decode_float()
+ config.water_animation_speed = bytes.decode_u32()
+
+ return config
+
+
+class LightingParameters:
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var longitude: int
+
+ ## Byte Type: i32 [br]
+ ## Byte Length: 4
+ var latitude: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var diffuse_color_r: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var diffuse_color_g: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var diffuse_color_b: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var ambient_color_r: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var ambient_color_g: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var ambient_color_b: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ ## Shadow Map Alpha
+ var intensity: int
+
+
+ func get_diffuse_color() -> Color:
+ return Color(diffuse_color_r, diffuse_color_g, diffuse_color_b)
+
+
+ func get_ambient_color() -> Color:
+ return Color(ambient_color_r, ambient_color_g, ambient_color_b)
+
+
+ static func from_bytes(bytes: ByteStream) -> LightingParameters:
+ var params = LightingParameters.new()
+
+ params.longitude = bytes.decode_s32()
+ params.latitude = bytes.decode_s32()
+ params.diffuse_color_r = bytes.decode_float()
+ params.diffuse_color_g = bytes.decode_float()
+ params.diffuse_color_b = bytes.decode_float()
+ params.ambient_color_r = bytes.decode_float()
+ params.ambient_color_g = bytes.decode_float()
+ params.ambient_color_b = bytes.decode_float()
+ params.intensity = bytes.decode_float()
+
+ return params
+
+
+class MapResource:
+ pass
+
+enum MapResourceType {
+ Animated3DModel = 1,
+ DynamicLightSource = 2,
+ SpatialAudioSource = 3,
+ ParticleEffectEmitter = 4,
+}
+
+enum ModelAnimationType {
+ None = 0,
+ Looping = 2,
+}
+
+enum ParticlePresetEffect {
+ None = -1,
+ Hit_1 = 1,
+ Hit_2 = 2,
+ Hit_3 = 3,
+ Torch = 47,
+ TunaParty = 1097,
+}
+
+enum QuadTreeQuadrant {
+ BottomLeft = 0,
+ BottomRight = 1,
+ TopLeft = 2,
+ TopRight = 3,
+}
+
+
+class Animated3DModel extends MapResource:
+ ## Byte Type: u8 [br]
+ ## Byte Length: 40
+ var name: String
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var animation_type: ModelAnimationType
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var animation_speed_percent: float
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var collision_flags: int
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 80
+ var model_file: String
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 80
+ var root_node_name: String
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_z: float
+
+ ## 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]
+ ## Byte Length: 4
+ var scale_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var scale_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var scale_z: float
+
+
+ func get_position() -> Vector3:
+ return Vector3(position_x, position_y, position_z)
+
+
+ func get_rotation() -> Vector3:
+ return Vector3(rotation_x, rotation_y, rotation_z)
+
+
+ func get_scale() -> Vector3:
+ return Vector3(scale_x, scale_y, scale_z)
+
+
+ static func from_bytes(bytes: ByteStream) -> Animated3DModel:
+ var resource = Animated3DModel.new()
+
+ 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_ro(80)
+ resource.root_node_name = bytes.get_string_from_utf8(80)
+ resource.position_x = bytes.decode_float()
+ resource.position_y = bytes.decode_float()
+ resource.position_z = bytes.decode_float()
+ resource.rotation_x = bytes.decode_float()
+ resource.rotation_y = bytes.decode_float()
+ resource.rotation_z = bytes.decode_float()
+ resource.scale_x = bytes.decode_float()
+ resource.scale_y = bytes.decode_float()
+ resource.scale_z = bytes.decode_float()
+
+ #print(inst_to_dict(resource))
+ return resource
+
+
+class DynamicLightSource extends MapResource:
+ ## Byte Type: u8 [br]
+ ## Byte Length: 80
+ var name: String
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_z: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var diffuse_color_r: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var diffuse_color_g: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var diffuse_color_b: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var light_range: float
+
+
+ func get_position() -> Vector3:
+ return Vector3(position_x, position_y, position_z)
+
+
+ func get_diffuse_color() -> Color:
+ return Color(diffuse_color_r, diffuse_color_g, diffuse_color_b)
+
+
+ static func from_bytes(bytes: ByteStream) -> DynamicLightSource:
+ var resource = DynamicLightSource.new()
+
+ 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()
+ resource.diffuse_color_r = bytes.decode_float()
+ resource.diffuse_color_g = bytes.decode_float()
+ resource.diffuse_color_b = bytes.decode_float()
+ resource.light_range = bytes.decode_float()
+
+ #print(inst_to_dict(resource))
+ return resource
+
+
+class SpatialAudioSource extends MapResource:
+ ## Byte Type: u8 [br]
+ ## Byte Length: 80
+ var name: String
+
+ ## Byte Type: u8 [br]
+ ## Byte Length: 80
+ var audio_file: String
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_z: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var volume_gain: float
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var width: int
+
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var height: int
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var audio_range: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var duration: float
+
+
+ func get_position() -> Vector3:
+ return Vector3(position_x, position_y, position_z)
+
+
+ func get_size() -> Vector2:
+ return Vector2(width, height)
+
+
+ static func from_bytes(bytes: ByteStream) -> SpatialAudioSource:
+ var resource = SpatialAudioSource.new()
+
+ 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()
+ resource.volume_gain = bytes.decode_float()
+ resource.width = bytes.decode_u32()
+ resource.height = bytes.decode_u32()
+ resource.audio_range = bytes.decode_float()
+ resource.duration = bytes.decode_float()
+
+ return resource
+
+
+class ParticleEffectEmitter extends MapResource:
+ ## Byte Type: u8 [br]
+ ## Byte Length: 80
+ var name: String
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var position_z: float
+
+ @warning_ignore("enum_variable_without_default")
+ ## Byte Type: u32 [br]
+ ## Byte Length: 4
+ var preset_effect: ParticlePresetEffect
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## in frames; 60 means 1 second
+ var emission_delay: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var launch_parameter_a: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var launch_parameter_b: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var launch_parameter_c: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var launch_parameter_d: float
+
+
+ func get_position() -> Vector3:
+ return Vector3(position_x, position_y, position_z)
+
+
+ static func from_bytes(bytes: ByteStream) -> ParticleEffectEmitter:
+ var resource = ParticleEffectEmitter.new()
+
+ 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()
+ resource.preset_effect = bytes.decode_u32()
+ resource.emission_delay = bytes.decode_float()
+ resource.launch_parameter_a = bytes.decode_float()
+ resource.launch_parameter_b = bytes.decode_float()
+ resource.launch_parameter_c = bytes.decode_float()
+ resource.launch_parameter_d = bytes.decode_float()
+
+ return resource
+
+
+class QuadTree:
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## coordinate of the corner at the lowest altitude
+ var bottom_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var bottom_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var bottom_z: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## coordinate of the corner at the highest altitude
+ var top_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var top_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var top_z: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## width of the bounding box (when interpreted as a cuboid)
+ var diameter_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## height of the bounding box (when interpreted as a cuboid)
+ var diameter_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## depth of the bounding box (when interpreted as a cuboid)
+ var diameter_z: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4 [br]
+ ## coordinate of the point in the exact center of the box
+ var center_x: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var center_y: float
+
+ ## Byte Type: f32 [br]
+ ## Byte Length: 4
+ var center_z: float
+
+ ## Length: 4 [br]
+ ## null if leaf node at sub-tree level 5 [br]
+ ## indexes are [enum QuadTreeQuadrant]
+ var quadrants: Array[QuadTree]
+
+
+ func get_bottom() -> Vector3:
+ return Vector3(bottom_x, bottom_y, bottom_z)
+
+
+ func get_top() -> Vector3:
+ return Vector3(top_x, top_y, top_z)
+
+
+ func get_diameter() -> Vector3:
+ return Vector3(diameter_x, diameter_y, diameter_z)
+
+
+ func get_center() -> Vector3:
+ return Vector3(center_x, center_y, center_z)
+
+
+ static func from_bytes(bytes: ByteStream) -> QuadTree:
+ var tree = QuadTree.new()
+
+ tree.bottom_x = bytes.decode_float()
+
+ return tree
+
+
+ static func from_bytes_recursive(bytes: ByteStream, tree: QuadTree, depth: int):
+ if depth == 5 or tree.quadrants.size() == 4:
+ pass
diff --git a/extractor/version.gd b/extractor/version.gd
index 679a8f0..ae97a10 100644
--- a/extractor/version.gd
+++ b/extractor/version.gd
@@ -11,5 +11,25 @@ var major: int
var minor: int
+func lower_than(compare_major: int, compare_minor: int) -> bool:
+ if (major > compare_major):
+ return false
+
+ if (major == compare_major):
+ return minor < compare_minor
+
+ return true
+
+
+func higher_than(compare_major: int, compare_minor: int) -> bool:
+ if (major > compare_major):
+ return true
+
+ if (major == compare_major):
+ return minor > compare_minor
+
+ return false
+
+
func _to_string() -> String:
return "%s.%s" % [major, minor]