## see https://ragnarokresearchlab.github.io/file-formats/grf class_name GRF var header: Header class Header: const BYTE_LENGTH := 46 ## Byte Length: 15 ## Master of Magic var signature: String = "Master of Magic" ## Byte Length: 15 var encryption: PackedByteArray ## Byte Type: u32 ## Byte Length: 4 var file_table_offset: int ## Byte Type: u32 ## Byte Length: 4 var scrambling_seed: int ## Byte Type: u32 ## Byte Length: 4 var scrambled_file_count: int ## Byte Type: u32 ## Byte Length: 4 var version: int func get_file_count() -> int: return scrambled_file_count - scrambling_seed - 7 static func from_bytes(bytes: PackedByteArray): var header = Header.new() header.signature = bytes.slice(0, 15).get_string_from_utf8() header.encryption = bytes.slice(15, 15 + 15) header.file_table_offset = bytes.decode_u32(30) header.scrambling_seed = bytes.decode_u32(34) header.scrambled_file_count = bytes.decode_u32(38) header.version = bytes.decode_u32(42) return header var file_table: FileTable class FileTable: const BYTE_LENGTH := 8 ## Byte Type: u32 ## Byte Length: 4 var compressed_size: int ## Byte Type: u32 ## Byte Length: 4 var decompressed_size: int var compressed_record_headers: PackedByteArray var decompressed_record_headers: PackedByteArray func generate_compressed_record_headers(grf_file: FileAccess): compressed_record_headers = grf_file.get_buffer(compressed_size) func decompress(): decompressed_record_headers = compressed_record_headers.decompress( decompressed_size, FileAccess.CompressionMode.COMPRESSION_DEFLATE ) static func from_bytes(bytes: PackedByteArray): var file_table = FileTable.new() file_table.compressed_size = bytes.decode_u32(0) file_table.decompressed_size = bytes.decode_u32(4) return file_table var file_entries: Array[FileEntry] class FileEntry: const BYTE_LENGTH := 17 var file_name: String ## Byte Type: u32 ## Byte Length: 4 var compressed_size: int ## Byte Type: u32 ## Byte Length: 4 var byte_aligned_size: int ## Byte Type: u32 ## Byte Length: 4 var decompressed_size: int ## Byte Type: u8 ## Byte Length: 1 var file_type: int ## Byte Type: u32 ## Byte Length: 4 var offset: int func get_byte_length() -> int: return BYTE_LENGTH #+ file_name.to_ascii_buffer().size() func get_file_path() -> String: var parts = file_name.split("\\") return "/".join(parts) func get_contents(grf_file: FileAccess): var previous_position = grf_file.get_position() grf_file.seek(Header.BYTE_LENGTH + offset) var buffer = grf_file.get_buffer(byte_aligned_size) var contents = buffer.decompress( decompressed_size, FileAccess.CompressionMode.COMPRESSION_DEFLATE ) grf_file.seek(previous_position) return contents @warning_ignore("shadowed_variable") static func from_bytes_with_filename(bytes: PackedByteArray, file_name: String): #print(file_name) var file_entry = FileEntry.new() file_entry.file_name = file_name file_entry.compressed_size = bytes.decode_u32(0) file_entry.byte_aligned_size = bytes.decode_u32(4) file_entry.decompressed_size = bytes.decode_u32(8) file_entry.file_type = bytes.decode_u8(12) file_entry.offset = bytes.decode_u32(13) return file_entry var file_access: FileAccess static func open(path: String): var grf = GRF.new() grf.file_access = FileAccess.open(path, FileAccess.ModeFlags.READ) grf.header = GRF.Header.from_bytes(grf.file_access.get_buffer(Header.BYTE_LENGTH)) grf.file_access.seek(Header.BYTE_LENGTH + grf.header.file_table_offset) grf.file_table = FileTable.from_bytes(grf.file_access.get_buffer(8)) grf.file_table.generate_compressed_record_headers(grf.file_access) grf.file_table.decompress() grf.file_entries = [] as Array[FileEntry] var file_entry_offset = 0 while file_entry_offset < grf.file_table.decompressed_record_headers.size(): var file_name_size = grf.file_table.decompressed_record_headers.find(0, file_entry_offset) + 1 - file_entry_offset var file_entry = FileEntry.from_bytes_with_filename( grf.file_table.decompressed_record_headers.slice( file_entry_offset + file_name_size, file_entry_offset + file_name_size + FileEntry.BYTE_LENGTH ), GRF.decode_string(grf.file_table.decompressed_record_headers.slice( file_entry_offset, file_entry_offset + file_name_size )) ) grf.file_entries.append(file_entry) file_entry_offset += file_entry.get_byte_length() + file_name_size return grf func extract(destination: String = "res://data"): for file_entry in file_entries: 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()) var file = FileAccess.open("%s/extracted/%s" % [destination, file_path], FileAccess.WRITE_READ) file.store_buffer(file_entry.get_contents(file_access)) func convert(destination: String = "res://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() base_directory.make_dir_recursive(base_directory_path) base_directory.change_dir(base_directory_path) var file_name = file_path.get_file().substr(0, file_path.get_file().length() - (file_path.get_extension().length() + 1) ) var base_file_directory_path := "%s/%s" % [base_directory.get_current_dir(), file_name] #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_body_path_part) and file_path.contains("NIGHT_WATCH"): var sprite = SpriteFormat.from_bytes(file_entry.get_contents(file_access)) print(file_path, (sprite.palette_image_count), sprite.version) sprite.save_to_file(base_file_directory_path) elif file_path.ends_with(".act") and 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 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) # TODO: set animation max length # 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] 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) 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) 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() return string