commit 44b4d847e5b490946991c4d0a89da3ae16818ba3 Author: Thea Schöbl Date: Thu May 4 00:32:54 2023 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..32585cb --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# MHLIB + +A project to make the old classic Moorhuhn Games playable +on modern systems via an engine reimplementation, written +in Rust and Godot. + +Most of the Moorhuhn Games are structured quite similarly, +which makes supporting them relatively easy. + diff --git a/godot/.gitattributes b/godot/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/godot/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/godot/.gitignore b/godot/.gitignore new file mode 100644 index 0000000..4709183 --- /dev/null +++ b/godot/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/godot/icon.png b/godot/icon.png new file mode 100644 index 0000000..e660ec3 Binary files /dev/null and b/godot/icon.png differ diff --git a/godot/icon.png.import b/godot/icon.png.import new file mode 100644 index 0000000..4a02b34 --- /dev/null +++ b/godot/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c1vqk21x7qa8t" +path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/godot/mhjnr.gdextension b/godot/mhjnr.gdextension new file mode 100644 index 0000000..99f902e --- /dev/null +++ b/godot/mhjnr.gdextension @@ -0,0 +1,12 @@ +[configuration] +entry_symbol = "gdext_rust_init" + +[libraries] +linux.debug.x86_64 = "res://../rust/target/debug/libmhjnr.so" +linux.release.x86_64 = "res://../rust/target/release/libmhjnr.so" +windows.debug.x86_64 = "res://../rust/target/debug/mhjnr.dll" +windows.release.x86_64 = "res://../rust/target/release/mhjnr.dll" +macos.debug = "res://../rust/target/debug/mhjnr.dylib" +macos.release = "res://../rust/target/release/mhjnr.dylib" +macos.debug.arm64 = "res://../rust/target/aarch64-apple-darwin/debug/mhjnr.dylib" +macos.release.arm64 = "res://../rust/target/aarch64-apple-darwin/release/mhjnr.dylib" \ No newline at end of file diff --git a/godot/mhjnr/Camera2D.gd b/godot/mhjnr/Camera2D.gd new file mode 100644 index 0000000..49691c2 --- /dev/null +++ b/godot/mhjnr/Camera2D.gd @@ -0,0 +1,60 @@ +extends CharacterBody2D + +@export var max_jumps: int = 2 +@export_range(0, 4000, 1, "suffix:px/s") var speed: float = 400 +@export_range(0, 4000, 1, "suffix:px/s") var terminal_velocity: float = 2000 +@export_range(0, 4000, 1, "suffix:px/s") var jump_speed: float = 900 +@export_range(0, 4000, 1, "suffix:px/s²") var acceleration: float = 800 +@export_range(0, 4000, 1, "suffix:px/s²") var deceleration: float = 1000 + +@onready var state_machine: AnimationNodeStateMachinePlayback = %AnimationTree["parameters/playback"] +@onready var jumps: int = max_jumps +@onready var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity") + +func is_running(): + return Input.is_action_pressed("Move Left") or Input.is_action_pressed("Move Right") + +func is_on_ledge(): + return is_on_floor() and not is_running() and not %Slip.is_colliding() + +func did_jump(): + return Input.is_action_just_pressed("Move Up") and jumps > 0 + +func _ready() -> void: + apply_floor_snap() + +func clamp_dir(value: float, dir: float): + return clampf(value, min(dir, 0.0), max(dir, 0.0)) + +func _physics_process(delta: float) -> void: + velocity.y += gravity * delta + velocity.y = minf(velocity.y, terminal_velocity) + + var vertical: float = Input.get_axis("Move Down", "Move Up") + var horizontal: float = Input.get_axis("Move Left", "Move Right") + + velocity + if is_on_floor(): + jumps = max_jumps + if did_jump(): + if jumps != max_jumps: + state_machine.start(state_machine.get_current_node(), true) + jumps -= 1 + velocity.y = -jump_speed + + var max_speed: float = speed * horizontal + + velocity.x += acceleration * horizontal * delta + if is_running(): + velocity.x = clamp_dir(velocity.x, max_speed) + %animations.flip_h = horizontal > 0.0 + else: + velocity.x -= clamp_dir(deceleration * velocity.x * delta, velocity.x) + + if is_on_ledge(): + jumps = 0 + var direction: float = 1.0 if %SlipTestFront.is_colliding() else -1.0 + velocity.x += 10_000 * delta * direction + + if move_and_slide() and is_on_wall(): + velocity.x = 0.0 diff --git a/godot/mhjnr/Moorhuhn.tscn b/godot/mhjnr/Moorhuhn.tscn new file mode 100644 index 0000000..399b76b --- /dev/null +++ b/godot/mhjnr/Moorhuhn.tscn @@ -0,0 +1,648 @@ +[gd_scene load_steps=61 format=3 uid="uid://ctoj2a102rs6f"] + +[ext_resource type="Script" path="res://mhjnr/Camera2D.gd" id="1_nngds"] +[ext_resource type="SpriteFrames" path="datafile://data/player/sprites.txt" id="2_valkm"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_mob27"] +size = Vector2(96, 85) + +[sub_resource type="Animation" id="Animation_7sfno"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"mh_runW"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [0] +} + +[sub_resource type="Animation" id="Animation_v01jo"] +resource_name = "crawl" +length = 1.00001 +loop_mode = 1 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_crawlWCycle"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667, 0.533333, 0.6, 0.666667, 0.733333, 0.8, 0.866667, 0.933333, 1), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] +} + +[sub_resource type="Animation" id="Animation_jmosv"] +resource_name = "crawl_end" +length = 0.400007 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_crawlWEnd"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6] +} + +[sub_resource type="Animation" id="Animation_8i52u"] +resource_name = "crawl_start" +length = 0.266673 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_crawlWStart"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4] +} + +[sub_resource type="Animation" id="Animation_f4p3b"] +resource_name = "idle" +length = 4.00001 +loop_mode = 1 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_stayW_idle"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 1, 1.06667, 1.13333, 1.2, 1.26667, 1.33333, 2.26667, 2.33333, 2.4, 2.46667, 2.53333, 2.6, 2.66667, 3.6, 3.66667, 3.73333, 3.8, 3.86667, 3.93333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20] +} + +[sub_resource type="Animation" id="Animation_kugay"] +resource_name = "jump" +length = 0.266673 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_stayW_jumpCycle"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4] +} + +[sub_resource type="Animation" id="Animation_kbk86"] +resource_name = "jump_end" +length = 0.53334 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_stayW_jumpEnd+particle"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667, 0.533333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7, 8] +} + +[sub_resource type="Animation" id="Animation_0uslx"] +resource_name = "jump_start" +length = 0.33334 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_stayW_jumpStart"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5] +} + +[sub_resource type="Animation" id="Animation_bifhq"] +resource_name = "run" +length = 0.600007 +loop_mode = 1 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"mh_runW"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667, 0.533333, 0.6), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +} + +[sub_resource type="Animation" id="Animation_517vc"] +resource_name = "run_end" +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_runW_stop+particle"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667, 0.533333, 0.6, 0.666667, 0.733333, 0.8, 0.866667, 0.933333, 1), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] +} + +[sub_resource type="Animation" id="Animation_1mxgl"] +resource_name = "run_end_fall" +length = 1.13334 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"mh_fall_down"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667, 0.533333, 0.6, 0.666667, 0.733333, 0.8, 0.866667, 0.933333, 1, 1.06667, 1.13333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] +} + +[sub_resource type="Animation" id="Animation_fmxky"] +resource_name = "run_jump" +length = 0.466673 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_runW_jump"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7] +} + +[sub_resource type="Animation" id="Animation_l4g2q"] +resource_name = "run_smash_wall" +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:animation") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"mh_hit_smashedWall"] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:frame") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667, 0.533333, 0.6, 0.666667, 0.733333, 0.8, 0.866667, 0.933333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15] +} + +[sub_resource type="Animation" id="Animation_khe4g"] +resource_name = "run_start" +length = 0.53334 +step = 0.0666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.0666667, 0.133333, 0.2, 0.266667, 0.333333, 0.4, 0.466667, 0.533333), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [0, 1, 2, 3, 4, 5, 6, 7, 8] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:animation") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [&"MH_runW_1.stepStart"] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_jvkhi"] +_data = { +"RESET": SubResource("Animation_7sfno"), +"crawl": SubResource("Animation_v01jo"), +"crawl_end": SubResource("Animation_jmosv"), +"crawl_start": SubResource("Animation_8i52u"), +"idle": SubResource("Animation_f4p3b"), +"jump": SubResource("Animation_kugay"), +"jump_end": SubResource("Animation_kbk86"), +"jump_start": SubResource("Animation_0uslx"), +"run": SubResource("Animation_bifhq"), +"run_end": SubResource("Animation_517vc"), +"run_end_fall": SubResource("Animation_1mxgl"), +"run_jump": SubResource("Animation_fmxky"), +"run_smash_wall": SubResource("Animation_l4g2q"), +"run_start": SubResource("Animation_khe4g") +} + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_u60at"] +animation = &"RESET" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_qt2l3"] +animation = &"run" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_ohkqh"] +animation = &"run_end" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_s2hko"] +animation = &"run_smash_wall" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_t7lts"] +animation = &"run_start" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_xgmeb"] +advance_mode = 2 +advance_expression = "not is_running()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_x0rjg"] +advance_mode = 2 +advance_expression = "is_on_wall()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_orvkk"] +switch_mode = 2 +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_mtp52"] +switch_mode = 2 +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_yfk7a"] +switch_mode = 2 +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_p4gp5"] +advance_mode = 2 +advance_expression = "not is_running() or is_on_wall()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_suygg"] +advance_mode = 2 +advance_expression = "is_running()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_750mf"] +advance_mode = 2 +advance_expression = "velocity.x != 0" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_0ymww"] +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_gi4av"] +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_h8tv8"] +states/End/position = Vector2(668, 134) +states/RESET/node = SubResource("AnimationNodeAnimation_u60at") +states/RESET/position = Vector2(367, -131) +states/Start/position = Vector2(258, -131) +states/run/node = SubResource("AnimationNodeAnimation_qt2l3") +states/run/position = Vector2(668, -56) +states/run_end/node = SubResource("AnimationNodeAnimation_ohkqh") +states/run_end/position = Vector2(668, 32) +states/run_smash_wall/node = SubResource("AnimationNodeAnimation_s2hko") +states/run_smash_wall/position = Vector2(865, -131) +states/run_start/node = SubResource("AnimationNodeAnimation_t7lts") +states/run_start/position = Vector2(495, -131) +transitions = ["run", "run_end", SubResource("AnimationNodeStateMachineTransition_xgmeb"), "run", "run_smash_wall", SubResource("AnimationNodeStateMachineTransition_x0rjg"), "run_smash_wall", "End", SubResource("AnimationNodeStateMachineTransition_orvkk"), "run_end", "End", SubResource("AnimationNodeStateMachineTransition_mtp52"), "run_start", "run", SubResource("AnimationNodeStateMachineTransition_yfk7a"), "run_start", "End", SubResource("AnimationNodeStateMachineTransition_p4gp5"), "run_end", "run_start", SubResource("AnimationNodeStateMachineTransition_suygg"), "run_smash_wall", "run_start", SubResource("AnimationNodeStateMachineTransition_750mf"), "Start", "RESET", SubResource("AnimationNodeStateMachineTransition_0ymww"), "RESET", "run_start", SubResource("AnimationNodeStateMachineTransition_gi4av")] +graph_offset = Vector2(77, -267) + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_wubix"] +animation = &"jump" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_1p554"] +animation = &"idle" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_q376h"] +animation = &"jump_start" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_kxajv"] +animation = &"jump_end" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_io5l5"] +animation = &"run_jump" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_mgepe"] +animation = &"run_end_fall" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_khcbs"] +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_6878b"] +advance_mode = 2 +advance_expression = "velocity.y > 0.0" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_1lxrg"] +advance_mode = 2 +advance_expression = "is_on_floor()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_5me5o"] +switch_mode = 2 +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_nrhuk"] +switch_mode = 2 +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_og56a"] +advance_mode = 2 +advance_expression = "is_running() and velocity.x != 0.0" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_do5xa"] +advance_mode = 2 +advance_expression = "is_on_ledge()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_okk45"] +switch_mode = 2 +advance_mode = 2 + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_r33lm"] +advance_mode = 2 +advance_expression = "is_on_floor() and not is_on_ledge()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_mqs3h"] +advance_mode = 2 +advance_expression = "did_jump()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_jd8pa"] +advance_mode = 2 +advance_expression = "velocity.y > 0.0" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_c8u27"] +advance_mode = 2 +advance_expression = "did_jump()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_3lran"] +advance_mode = 2 +advance_expression = "not is_running()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_8e8p2"] +advance_mode = 2 +advance_expression = "is_on_floor()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_aivqa"] +advance_mode = 2 +advance_expression = "is_running()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_pp0po"] +advance_mode = 2 +advance_expression = "is_on_ledge()" + +[sub_resource type="AnimationNodeStateMachineTransition" id="AnimationNodeStateMachineTransition_rh1ge"] +advance_mode = 2 +advance_expression = "did_jump()" + +[sub_resource type="AnimationNodeStateMachine" id="AnimationNodeStateMachine_lv3mp"] +states/End/position = Vector2(893, 119) +states/Run/node = SubResource("AnimationNodeStateMachine_h8tv8") +states/Run/position = Vector2(592, 119) +states/Start/position = Vector2(73, 119) +states/fall/node = SubResource("AnimationNodeAnimation_wubix") +states/fall/position = Vector2(430, -168) +states/idle/node = SubResource("AnimationNodeAnimation_1p554") +states/idle/position = Vector2(235, 119) +states/jump/node = SubResource("AnimationNodeAnimation_q376h") +states/jump/position = Vector2(144, -19) +states/jump_end/node = SubResource("AnimationNodeAnimation_kxajv") +states/jump_end/position = Vector2(235, -168) +states/run_jump/node = SubResource("AnimationNodeAnimation_io5l5") +states/run_jump/position = Vector2(592, -40) +states/slip/node = SubResource("AnimationNodeAnimation_mgepe") +states/slip/position = Vector2(421, 58) +transitions = ["Start", "idle", SubResource("AnimationNodeStateMachineTransition_khcbs"), "jump", "fall", SubResource("AnimationNodeStateMachineTransition_6878b"), "fall", "jump_end", SubResource("AnimationNodeStateMachineTransition_1lxrg"), "jump_end", "idle", SubResource("AnimationNodeStateMachineTransition_5me5o"), "Run", "idle", SubResource("AnimationNodeStateMachineTransition_nrhuk"), "idle", "Run", SubResource("AnimationNodeStateMachineTransition_og56a"), "idle", "slip", SubResource("AnimationNodeStateMachineTransition_do5xa"), "slip", "fall", SubResource("AnimationNodeStateMachineTransition_okk45"), "slip", "jump_end", SubResource("AnimationNodeStateMachineTransition_r33lm"), "idle", "jump", SubResource("AnimationNodeStateMachineTransition_mqs3h"), "idle", "fall", SubResource("AnimationNodeStateMachineTransition_jd8pa"), "Run", "run_jump", SubResource("AnimationNodeStateMachineTransition_c8u27"), "run_jump", "fall", SubResource("AnimationNodeStateMachineTransition_3lran"), "run_jump", "Run", SubResource("AnimationNodeStateMachineTransition_8e8p2"), "jump_end", "Run", SubResource("AnimationNodeStateMachineTransition_aivqa"), "jump_end", "slip", SubResource("AnimationNodeStateMachineTransition_pp0po"), "jump_end", "jump", SubResource("AnimationNodeStateMachineTransition_rh1ge")] +graph_offset = Vector2(-106, -229) + +[sub_resource type="AnimationNodeStateMachinePlayback" id="AnimationNodeStateMachinePlayback_udssx"] + +[sub_resource type="AnimationNodeStateMachinePlayback" id="AnimationNodeStateMachinePlayback_n5wvc"] + +[node name="Moorhuhn" type="CharacterBody2D"] +script = ExtResource("1_nngds") +jump_speed = 610.0 + +[node name="Slip" type="RayCast2D" parent="."] +unique_name_in_owner = true +target_position = Vector2(0, 100) + +[node name="SlipTestFront" type="RayCast2D" parent="."] +unique_name_in_owner = true +position = Vector2(-55, 0) +target_position = Vector2(0, 100) + +[node name="Collision" type="CollisionShape2D" parent="."] +unique_name_in_owner = true +position = Vector2(0, 40) +shape = SubResource("RectangleShape2D_mob27") + +[node name="animations" type="AnimatedSprite2D" parent="."] +unique_name_in_owner = true +sprite_frames = ExtResource("2_valkm") +animation = &"mh_runW" + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +root_node = NodePath("../animations") +playback_process_mode = 0 +libraries = { +"": SubResource("AnimationLibrary_jvkhi") +} + +[node name="AnimationTree" type="AnimationTree" parent="."] +unique_name_in_owner = true +tree_root = SubResource("AnimationNodeStateMachine_lv3mp") +anim_player = NodePath("../AnimationPlayer") +advance_expression_base_node = NodePath("..") +active = true +process_callback = 0 +parameters/playback = SubResource("AnimationNodeStateMachinePlayback_udssx") +parameters/Run/playback = SubResource("AnimationNodeStateMachinePlayback_n5wvc") diff --git a/godot/mhjnr/camera.gd b/godot/mhjnr/camera.gd new file mode 100644 index 0000000..487382a --- /dev/null +++ b/godot/mhjnr/camera.gd @@ -0,0 +1,7 @@ +extends Camera2D + +var player: CharacterBody2D + +func _process(delta: float) -> void: + position = player.position + pass diff --git a/godot/mhjnr/level.gd b/godot/mhjnr/level.gd new file mode 100644 index 0000000..e3a431e --- /dev/null +++ b/godot/mhjnr/level.gd @@ -0,0 +1,61 @@ +@tool +extends Node2D + +@export var level_id: int = 1 + +func _ready() -> void: + var player = preload("res://mhjnr/Moorhuhn.tscn").instantiate() + add_child(player) + %Camera.player = player + %HudLevel.text = "Level %d" % level_id + player.position = Vector2(200, 10) + var camera_rect: Rect2i + + var level: ObjectScript = load("datafile://data/level%02d/settings/level.txt" % level_id) + for data in level.static_objects: + match data.resource_type: + "LevelSettings": + %LevelTimer.start(data.props["levelTime"]) + "TiledLayer": + var scene = load("datafile://data/level%02d/layers/%s.dat" % [level_id, data.name.to_lower()]) + var tiles: TileMap = scene.instantiate() + tiles.name = data.name + tiles.visible = data.props["is visible"] == 1 + + var used = tiles.get_used_rect() + used.position *= tiles.cell_quadrant_size + used.size *= tiles.cell_quadrant_size + camera_rect = used if camera_rect == null else camera_rect.merge(used) + + var scroll_speed: Vector2 = data.props["scroll speed"] + if scroll_speed.is_equal_approx(Vector2(1, 1)): + add_child(tiles) + else: + var parallax = ParallaxLayer.new() + parallax.name = data.name + parallax.visible = data.props["is visible"] == 1 + parallax.motion_scale = data.props["scroll speed"] + %parallax.add_child(parallax) + parallax.add_child(tiles) + + %Camera.limit_left = camera_rect.position.x + %Camera.limit_top = camera_rect.position.y + %Camera.limit_right = camera_rect.position.x + camera_rect.size.x + %Camera.limit_bottom = camera_rect.position.y + camera_rect.size.y + + %WorldBoundLeft.position.x = camera_rect.position.x + %WorldBoundTop.position.y = camera_rect.position.y + %WorldBoundRight.position.x = camera_rect.position.x + camera_rect.size.x + %WorldBoundBottom.position.y = camera_rect.position.y + camera_rect.size.y + + #var enemies: ObjectScript = load("datafile://data/level%02d/layers/%s.dat" % [level_id, name.to_lower()]) + #for object in enemies.dynamic_objects: + # match object.props["subType"]: + ## 1: create_movable(object) + # 0: create_enemy(object) + +func create_enemy(data: ObjectData): + pass + +func create_movable(data: ObjectData): + pass diff --git a/godot/mhjnr/level.tscn b/godot/mhjnr/level.tscn new file mode 100644 index 0000000..42c7325 --- /dev/null +++ b/godot/mhjnr/level.tscn @@ -0,0 +1,142 @@ +[gd_scene load_steps=14 format=3 uid="uid://bgb4avgjexp4t"] + +[ext_resource type="Script" path="res://mhjnr/level.gd" id="1_dfqgf"] +[ext_resource type="Theme" uid="uid://ks2uyxqg6u4k" path="res://mhjnr/theme.tres" id="3_a2fmg"] +[ext_resource type="Script" path="res://mhjnr/camera.gd" id="3_e6xoo"] +[ext_resource type="Texture2D" path="datafile://data/set1/sprites/hud_live.bmp" id="4_4bu8b"] +[ext_resource type="Texture2D" path="datafile://data/set1/sprites/hud_shield.bmp" id="5_6bu8b"] +[ext_resource type="Texture2D" path="datafile://data/set1/sprites/hud_bullet.bmp" id="5_rxbck"] + +[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_j78b3"] +normal = Vector2(0, 1) + +[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_3mxdl"] +normal = Vector2(-1, 0) + +[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_p4tp1"] +normal = Vector2(1, 0) + +[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_ixcvh"] + +[sub_resource type="GDScript" id="GDScript_nmpfh"] +resource_name = "TimeLabel" +script/source = "extends Label + +func _process(delta: float) -> void: + var time_left = %LevelTimer.time_left + text = \"%02d:%02d\" % [int(time_left / 60), int(time_left) % 60] +" + +[sub_resource type="AtlasTexture" id="AtlasTexture_glgkj"] +atlas = ExtResource("5_6bu8b") +region = Rect2(0, 0, 136, 42) +filter_clip = true + +[sub_resource type="AtlasTexture" id="AtlasTexture_2nrpq"] +atlas = ExtResource("4_4bu8b") +region = Rect2(0, 0, 36, 32) +filter_clip = true + +[node name="level" type="Node2D"] +script = ExtResource("1_dfqgf") +level_id = 2 + +[node name="Camera" type="Camera2D" parent="."] +unique_name_in_owner = true +process_callback = 0 +limit_smoothed = true +position_smoothing_enabled = true +position_smoothing_speed = 10.0 +drag_horizontal_enabled = true +drag_vertical_enabled = true +script = ExtResource("3_e6xoo") + +[node name="parallax" type="ParallaxBackground" parent="."] +unique_name_in_owner = true + +[node name="WorldBounds" type="StaticBody2D" parent="."] + +[node name="WorldBoundTop" type="CollisionShape2D" parent="WorldBounds"] +unique_name_in_owner = true +shape = SubResource("WorldBoundaryShape2D_j78b3") + +[node name="WorldBoundRight" type="CollisionShape2D" parent="WorldBounds"] +unique_name_in_owner = true +shape = SubResource("WorldBoundaryShape2D_3mxdl") + +[node name="WorldBoundLeft" type="CollisionShape2D" parent="WorldBounds"] +unique_name_in_owner = true +shape = SubResource("WorldBoundaryShape2D_p4tp1") + +[node name="KillFloor" type="Area2D" parent="."] + +[node name="WorldBoundBottom" type="CollisionShape2D" parent="KillFloor"] +unique_name_in_owner = true +shape = SubResource("WorldBoundaryShape2D_ixcvh") + +[node name="LevelTimer" type="Timer" parent="."] +unique_name_in_owner = true + +[node name="HUD" type="CanvasLayer" parent="."] + +[node name="MarginContainer" type="MarginContainer" parent="HUD"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 32 +theme_override_constants/margin_top = 32 +theme_override_constants/margin_right = 32 +theme_override_constants/margin_bottom = 32 + +[node name="GridContainer" type="GridContainer" parent="HUD/MarginContainer"] +layout_mode = 2 +columns = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="HUD/MarginContainer/GridContainer"] +layout_mode = 2 + +[node name="Score" type="Label" parent="HUD/MarginContainer/GridContainer/VBoxContainer"] +layout_mode = 2 +text = "0035570" + +[node name="Time" type="Label" parent="HUD/MarginContainer/GridContainer"] +layout_mode = 2 +size_flags_horizontal = 6 +theme = ExtResource("3_a2fmg") +text = "04:36" +horizontal_alignment = 1 +vertical_alignment = 1 +script = SubResource("GDScript_nmpfh") + +[node name="HudLevel" type="Label" parent="HUD/MarginContainer/GridContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Level 4" + +[node name="shield" type="TextureRect" parent="HUD/MarginContainer/GridContainer"] +layout_mode = 2 +size_flags_vertical = 10 +texture = SubResource("AtlasTexture_glgkj") + +[node name="Lives" type="HBoxContainer" parent="HUD/MarginContainer/GridContainer"] +layout_mode = 2 +size_flags_horizontal = 6 +size_flags_vertical = 8 + +[node name="Label" type="Label" parent="HUD/MarginContainer/GridContainer/Lives"] +layout_mode = 2 +text = "4" + +[node name="TextureRect" type="TextureRect" parent="HUD/MarginContainer/GridContainer/Lives"] +layout_mode = 2 +texture = SubResource("AtlasTexture_2nrpq") + +[node name="HBoxContainer" type="HBoxContainer" parent="HUD/MarginContainer/GridContainer"] +layout_mode = 2 +size_flags_vertical = 8 + +[node name="TextureRect" type="TextureRect" parent="HUD/MarginContainer/GridContainer/HBoxContainer"] +layout_mode = 2 +texture = ExtResource("5_rxbck") diff --git a/godot/mhjnr/theme.tres b/godot/mhjnr/theme.tres new file mode 100644 index 0000000..a7b34dc --- /dev/null +++ b/godot/mhjnr/theme.tres @@ -0,0 +1,6 @@ +[gd_resource type="Theme" format=3 uid="uid://ks2uyxqg6u4k"] + +[ext_resource type="FontFile" path="datafile://data/fonts/menufont.bmp" id="9_6bx8b"] + +[resource] +/fonts/menufont = ExtResource("9_6bx8b") diff --git a/godot/project.godot b/godot/project.godot new file mode 100644 index 0000000..c96e96e --- /dev/null +++ b/godot/project.godot @@ -0,0 +1,50 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="MHJNR" +run/main_scene="res://mhjnr/level.tscn" +config/features=PackedStringArray("4.0", "GL Compatibility") +boot_splash/bg_color=Color(0, 0, 0, 1) +boot_splash/image="res://icon.png" +boot_splash/fullsize=false + +[input] + +"Move Up"={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +"Move Down"={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +"Move Right"={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +"Move Left"={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} + +[physics] + +2d/default_gravity=2000.0 + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/icon.blend b/icon.blend new file mode 100644 index 0000000..bf58117 Binary files /dev/null and b/icon.blend differ diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..2ab338f --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,3 @@ +/target +.idea +mhjnr.iml diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..944fc4f --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,832 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "binrw" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272caaf6e0bfb7d508c0606e541e2c68f85c0d6352b62d0b299924eed59fe384" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4b28c1e534d96213c8966bb9240095757aa0909128985f97d16afd2e7257a8" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "exr" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.6.2", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glam" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c" + +[[package]] +name = "godot" +version = "0.1.0" +source = "git+https://github.com/godot-rust/gdext?branch=master#5d5132cffecc584f9c9ed1b55306dcbb60ec3972" +dependencies = [ + "godot-core", + "godot-macros", +] + +[[package]] +name = "godot-bindings" +version = "0.1.0" +source = "git+https://github.com/godot-rust/gdext?branch=master#5d5132cffecc584f9c9ed1b55306dcbb60ec3972" +dependencies = [ + "godot4-prebuilt", +] + +[[package]] +name = "godot-codegen" +version = "0.1.0" +source = "git+https://github.com/godot-rust/gdext?branch=master#5d5132cffecc584f9c9ed1b55306dcbb60ec3972" +dependencies = [ + "godot-bindings", + "heck", + "nanoserde", + "proc-macro2", + "quote", +] + +[[package]] +name = "godot-core" +version = "0.1.0" +source = "git+https://github.com/godot-rust/gdext?branch=master#5d5132cffecc584f9c9ed1b55306dcbb60ec3972" +dependencies = [ + "glam", + "godot-codegen", + "godot-ffi", +] + +[[package]] +name = "godot-ffi" +version = "0.1.0" +source = "git+https://github.com/godot-rust/gdext?branch=master#5d5132cffecc584f9c9ed1b55306dcbb60ec3972" +dependencies = [ + "godot-bindings", + "godot-codegen", + "paste", +] + +[[package]] +name = "godot-macros" +version = "0.1.0" +source = "git+https://github.com/godot-rust/gdext?branch=master#5d5132cffecc584f9c9ed1b55306dcbb60ec3972" +dependencies = [ + "proc-macro2", + "quote", + "venial", +] + +[[package]] +name = "godot4-prebuilt" +version = "0.0.0" +source = "git+https://github.com/godot-rust/godot4-prebuilt?branch=4.0.1#f9e8cfec0ec565201801b02160ef836800746617" + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mhjnr" +version = "0.1.0" +dependencies = [ + "base64", + "binrw", + "godot", + "image", + "itertools", + "serde", + "serde-xml-rs", + "unicode-segmentation", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nanoserde" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755e7965536bc54d7c9fba2df5ada5bf835b0443fd613f0a53fa199a301839d3" +dependencies = [ + "nanoserde-derive", +] + +[[package]] +name = "nanoserde-derive" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7a94da6c6181c35d043fc61c43ac96d3a5d739e7b8027f77650ba41504d6ab" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "png" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "venial" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61584a325b16f97b5b25fcc852eb9550843a251057a5e3e5992d2376f3df4bb2" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..fb136b0 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "mhjnr" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "mhjnr-viewer" +path = "src/main.rs" + +[lib] +name = "mhjnr" +crate-type = ["cdylib", "lib"] + +[dependencies] +itertools = "0.10.5" +unicode-segmentation = "1.10.1" +binrw = "0.11.1" +serde = {version = "1.0.160", features = ["derive"]} +serde-xml-rs = "0.6.0" +image = "0.24.6" +base64 = "0.21.0" +godot = { git = "https://github.com/godot-rust/gdext", branch = "master" } diff --git a/rust/src/formats/datafile.rs b/rust/src/formats/datafile.rs new file mode 100644 index 0000000..5cc6514 --- /dev/null +++ b/rust/src/formats/datafile.rs @@ -0,0 +1,46 @@ +use binrw::{binread, NullString}; +use std::collections::HashMap; + +#[binread] +#[br(little, magic = b"MHJNR")] +#[derive(Debug)] +pub struct Datafile { + #[br(align_after = 0x20)] + pub edition: Edition, + + #[br(temp)] + pub count: u32, + #[br(align_after = 0x20)] + pub unk1: u32, + + #[br(count = count)] + pub files: Vec, +} + +#[binread] +#[derive(Debug)] +pub enum Edition { + #[br(magic = b"-XS")] + Xs, + #[br(magic = b"-XXL")] + Xxl, +} + +#[derive(Debug)] +#[binread] +pub struct FileEntry { + #[br(pad_size_to = 0x68)] + pub name: NullString, + pub pos: u32, + #[br(pad_after = 0x10)] + pub len: u32, +} + +impl Datafile { + pub fn into_index(self) -> HashMap { + self.files + .into_iter() + .map(|entry| (entry.name.to_string(), entry)) + .collect() + } +} diff --git a/rust/src/formats/level.rs b/rust/src/formats/level.rs new file mode 100644 index 0000000..9612497 --- /dev/null +++ b/rust/src/formats/level.rs @@ -0,0 +1,46 @@ +use binrw::prelude::*; +use binrw::{BinRead, Error}; +use image; +use image::error::{DecodingError, ImageFormatHint}; +use image::{ImageError, ImageResult, Rgb, RgbImage}; +use std::io::Cursor; + +#[binrw] +#[brw(little)] +pub struct LevelTile { + pub index: u8, + pub id: u8, +} + +#[binrw] +#[brw(little)] +pub struct LevelLayer { + pub tile_count: u32, + pub width: u32, + pub height: u32, + pub unknown_2: u32, + #[br(count = width * height)] + pub tiles: Vec, +} + +pub fn level_tile_data_to_image(tile_data: &[u8]) -> ImageResult { + let mut cursor = Cursor::new(tile_data); + let layer = LevelLayer::read(&mut cursor).map_err(to_decoding_err)?; + + let mut image = RgbImage::new(layer.width, layer.height); + for y in 0..layer.height { + for x in 0..layer.width { + let tile = LevelTile::read(&mut cursor).map_err(to_decoding_err)?; + image.put_pixel(x, y, Rgb([tile.id, tile.index, 0])); + } + } + + Ok(image) +} + +fn to_decoding_err(err: Error) -> ImageError { + ImageError::Decoding(DecodingError::new( + ImageFormatHint::Name(String::from("mhjnr_layer")), + err, + )) +} diff --git a/rust/src/formats/mod.rs b/rust/src/formats/mod.rs new file mode 100644 index 0000000..db9b1ae --- /dev/null +++ b/rust/src/formats/mod.rs @@ -0,0 +1,115 @@ +use crate::formats::datafile::FileEntry; +use crate::formats::level::LevelLayer; +use crate::formats::rle::RleImage; +use crate::formats::sprites::Sprites; +use crate::formats::txt::{decrypt_txt, DecryptError}; +use crate::formats::ui_xml::UiTag; +use binrw::BinRead; +use std::ffi::OsStr; +use std::fmt::Debug; +use std::io::{Cursor, Read, Seek, SeekFrom}; +use std::path::Path; + +pub mod datafile; +pub mod level; +pub mod rle; +pub mod sprites; +pub mod txt; +pub mod ui_xml; + +pub enum DatafileFile { + Txt(String), + Level(LevelLayer), + Sprites(Vec), + RleSprite(Box), + Bitmap(Vec), + Vorbis(Vec), + TileCollision(String), + Ui(UiTag), +} + +pub enum Error { + Deserialization, + UnknownFormat(String), + UnknownError, + Custom(String), + DecryptError(DecryptError), +} + +fn custom_err(e: T) -> Error +where + T: Debug, +{ + Error::Custom(format!("{:#?}", e)) +} + +pub fn load_data(entry: &FileEntry, reader: &mut R) -> Result +where + R: Read + Seek, +{ + reader + .seek(SeekFrom::Start(entry.pos as u64)) + .map_err(custom_err)?; + let mut data = vec![0u8; entry.len as usize]; + reader.read_exact(&mut data).map_err(custom_err)?; + + let name = entry.name.to_string(); + let path = Path::new(&name); + + match path + .extension() + .and_then(OsStr::to_str) + .ok_or(Error::Custom("No extension".to_string()))? + { + "dat" => Ok(DatafileFile::Level( + LevelLayer::read(&mut Cursor::new(data)).map_err(custom_err)?, + )), + "rle" => Ok(DatafileFile::RleSprite(Box::new( + RleImage::read(&mut Cursor::new(data)).map_err(custom_err)?, + ))), + "bmp" => Ok(DatafileFile::Bitmap(data)), + "ogg" => Ok(DatafileFile::Vorbis(data)), + "xml" => { + serde_xml_rs::from_str::(String::from_utf8(data).map_err(custom_err)?.as_str()) + .map_err(custom_err) + .map(DatafileFile::Ui) + } + "txt" => { + let stem = path + .file_stem() + .and_then(OsStr::to_str) + .ok_or(Error::Custom("Stem".to_string()))?; + let decr = decrypt_txt(data.into_iter()).map_err(|e| Error::DecryptError(e))?; + if stem.starts_with("tile_collision") { + Ok(DatafileFile::TileCollision(decr)) + } else if stem == "sprites" { + Ok(DatafileFile::Sprites( + Sprites::parse(decr.as_str()).map_err(custom_err)?, + )) + } else { + Ok(DatafileFile::Txt(decr)) + } + } + /*Some("rle") => { + let image: RleImage = RleImage::read(&mut Cursor::new(data)).unwrap(); + let path = Path::new(dat_path).with_file_name("res.gif"); + println!("{:?}", path); + let mut encoder = GifEncoder::new( + OpenOptions::new() + .create(true) + .write(true) + .open(path) + .unwrap(), + ); + encoder.set_repeat(Repeat::Infinite).unwrap(); + encoder.try_encode_frames(image.into_frames()).unwrap(); + } + Some("dat") => { + let image = level_tile_data_to_image(&data).unwrap(); + let path = Path::new(dat_path).with_file_name("res.png"); + println!("{:?}", path); + image.save_with_format(path, ImageFormat::Png).unwrap(); + }*/ + ext => Err(Error::UnknownFormat(ext.to_string())), + } +} diff --git a/rust/src/formats/rle.rs b/rust/src/formats/rle.rs new file mode 100644 index 0000000..60f1f39 --- /dev/null +++ b/rust/src/formats/rle.rs @@ -0,0 +1,131 @@ +use binrw::prelude::*; +use binrw::Endian; +use image::error::{DecodingError, ImageFormatHint}; +use image::{AnimationDecoder, Delay, Frame, Frames, ImageBuffer, ImageError}; +use std::io::{Read, Seek}; +use std::time::Duration; + +#[binread] +#[br(little, magic = 0x67u32)] +pub struct RleImage { + pub hash: u64, + pub color_table: [[u8; 4]; 512], + pub width: u32, + pub height: u32, + pub numerator: u32, + pub denominator: u32, + #[br(temp)] + pub frame_count: u32, + #[br(count = frame_count)] + pub frames: Vec, +} + +#[binread] +#[br(little)] +pub struct RleLayer { + pub width: u32, + pub height: u32, + pub left: u32, + pub top: u32, + pub numerator: u32, + pub denominator: u32, + pub data_size: u32, + pub unknown3: u32, + #[br(args(width * height), parse_with = parse_rle)] + pub data: Vec, +} + +pub fn parse_rle( + reader: &mut R, + endian: Endian, + (size,): (u32,), +) -> BinResult> { + let mut data = Vec::with_capacity(size as usize); + + while data.len() != size as usize { + let count: i8 = reader.read_type(endian)?; + if count > 0 { + let value: u8 = reader.read_type(endian)?; + for _ in 0..count { + data.push(value); + } + } else { + for _ in 0..-count { + data.push(reader.read_type(endian)?); + } + } + } + + Ok(data) +} + +impl RleImage { + pub fn get_image_data(&self, layer: &RleLayer) -> Vec { + let mut data = Vec::::with_capacity(self.width as usize * self.height as usize * 4); + let mut i = 0; + for y in 0..self.height { + for x in 0..self.width { + if y < layer.top + || y >= layer.top + layer.height + || x < layer.left + || x >= layer.left + layer.width + { + data.push(0); + data.push(0); + data.push(0); + data.push(0); + } else { + let color = self.color_table[layer.data[i] as usize]; + i += 1; + data.push(color[2]); + data.push(color[1]); + data.push(color[0]); + data.push(if color[2] == 0 && color[1] == 0 && color[0] == 0 { + 0 + } else { + 255 + }); + } + } + } + + data + } +} + +impl<'a> AnimationDecoder<'a> for RleImage { + fn into_frames(self) -> Frames<'a> { + Frames::new(Box::new(self.frames.into_iter().map(move |frame| { + let buffer = ImageBuffer::from_raw( + frame.width, + frame.height, + frame + .data + .into_iter() + .flat_map(|it| bgra_to_rgba(self.color_table[it as usize])) + .collect(), + ) + .ok_or(to_rle_image_err(std::fmt::Error::default()))?; + Ok(Frame::from_parts( + buffer, + frame.left, + frame.top, + Delay::from_saturating_duration(Duration::from_millis(80)), + )) + }))) + } +} + +pub fn bgra_to_rgba(pixel: [u8; 4]) -> [u8; 4] { + [pixel[2], pixel[1], pixel[0], pixel[3]] +} + +fn to_rle_image_err(err: T) -> ImageError +where + T: Into>, +{ + ImageError::Decoding(DecodingError::new( + ImageFormatHint::Name(String::from("mhjnr_rle")), + err, + )) +} diff --git a/rust/src/formats/sprites.rs b/rust/src/formats/sprites.rs new file mode 100644 index 0000000..9fb57a7 --- /dev/null +++ b/rust/src/formats/sprites.rs @@ -0,0 +1,75 @@ +#[derive(Debug)] +pub struct Sprites { + pub name: String, + pub sprite_type: SpriteType, + pub file_name: String, + pub render_mode: RenderMode, + pub frames: Option, +} + +#[derive(Debug)] +pub enum Error { + InvalidData, + UnknownEnum(String), +} + +impl Sprites { + pub fn parse(string: &str) -> Result, Error> { + string + .split('\n') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(Sprites::parse_single) + .collect() + } + + pub fn parse_single(string: &str) -> Result { + let mut components = string.split_whitespace(); + + Ok(Sprites { + file_name: components.next().ok_or(Error::InvalidData)?.to_string(), + sprite_type: match components.next().ok_or(Error::InvalidData)? { + "anim_rle" => SpriteType::AnimRle, + "anim" => SpriteType::Anim, + "static" => SpriteType::Static, + e => return Err(Error::UnknownEnum(e.to_string())), + }, + name: components.next().ok_or(Error::InvalidData)?.to_string(), + render_mode: match components.next().ok_or(Error::InvalidData)? { + "normx" => RenderMode::NormX, + "flipx" => RenderMode::FlipX, + e => return Err(Error::UnknownEnum(e.to_string())), + }, + frames: if let Some(c) = components.next() { + Some(match c { + "nocrop" => CropMode::NoCrop, + x => x + .parse::() + .map(CropMode::FrameCount) + .map_err(|e| Error::UnknownEnum(e.to_string()))?, + }) + } else { + None + }, + }) + } +} + +#[derive(Debug)] +pub enum CropMode { + FrameCount(i32), + NoCrop, +} + +#[derive(Debug)] +pub enum RenderMode { + NormX, + FlipX, +} + +#[derive(Debug)] +pub enum SpriteType { + Static, + Anim, + AnimRle, +} diff --git a/rust/src/formats/txt.rs b/rust/src/formats/txt.rs new file mode 100644 index 0000000..eee0bf5 --- /dev/null +++ b/rust/src/formats/txt.rs @@ -0,0 +1,68 @@ +use std::num::ParseIntError; +use std::string::FromUtf8Error; + +#[derive(Debug)] +pub enum DecryptError { + FromUtf8Error(FromUtf8Error), + ParseIntError(ParseIntError), +} + +impl From for DecryptError { + fn from(e: FromUtf8Error) -> DecryptError { + DecryptError::FromUtf8Error(e) + } +} + +impl From for DecryptError { + fn from(e: ParseIntError) -> DecryptError { + DecryptError::ParseIntError(e) + } +} + +/// Decrypts txt files contained inside the dat file +pub fn decrypt_txt(buffer: I) -> Result +where + I: Iterator, +{ + let mut key = 0x1234u16; + + String::from_utf8( + buffer + .map(|char| { + let decr = char ^ key as u8; + key = key.wrapping_mul(3).wrapping_add(2); + decr + }) + .map(|char| (((char >> 1) ^ (char << 1)) & 0x55) ^ (char << 1)) + .collect(), + ) + .map_err(DecryptError::from) +} + +/// Parses a hex string to a Vec +fn from_hex(line: &str) -> Result, ParseIntError> { + (0..line.len()) + .step_by(2) + .map(|i| u8::from_str_radix(line.get(i..=i + 1).unwrap_or(""), 16)) + .collect() +} + +/// This function is applied to *exposed* txt files, +/// such as the player profile or high scores +/// +/// If the file is contained in the datafile, it has +/// to first be decrypted normally and then again +/// with this function. +pub fn decrypt_exposed_txt(contents: String) -> Result { + contents + .split_terminator("\r\n") + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .map(from_hex) + .map(|line| decrypt_txt(line.map_err(DecryptError::from)?.into_iter())) + .collect::, _>>() + .map(|l| l.join("\r\n")) +} + +#[cfg(test)] +mod tests {} diff --git a/rust/src/formats/ui_xml.rs b/rust/src/formats/ui_xml.rs new file mode 100644 index 0000000..7af13f9 --- /dev/null +++ b/rust/src/formats/ui_xml.rs @@ -0,0 +1,77 @@ +use serde::de::Error; +use serde::{Deserialize, Deserializer}; + +#[derive(Debug, Deserialize)] +pub enum UiTag { + Menu(UiMenu), + Image(UiImage), + TextButton(UiTextButton), +} + +#[derive(Debug, Deserialize)] +pub struct UiMenu { + pub selected: String, + #[serde(rename = "OnBack")] + pub on_back: String, + #[serde(rename = "$value")] + pub children: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct UiImage { + pub texture: String, + #[serde(deserialize_with = "deserialize_vec2")] + pub position: [i32; 2], + #[serde(deserialize_with = "deserialize_vec2")] + pub size: [i32; 2], + #[serde(rename = "fademode")] + pub fade_mode: FadeMode, +} + +#[derive(Debug, Deserialize)] +pub struct UiTextButton { + pub name: Option, + pub text: String, + #[serde(deserialize_with = "deserialize_vec2")] + pub position: [i32; 2], + #[serde(rename = "halign")] + pub horizontal_align: HorizontalAlign, + #[serde(rename = "fademode")] + pub fade_mode: FadeMode, + #[serde(rename = "OnSelect")] + pub on_select: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum HorizontalAlign { + Center, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FadeMode { + None, +} + +fn deserialize_vec2<'de, D>(deserializer: D) -> Result<[i32; 2], D::Error> +where + D: Deserializer<'de>, +{ + let buf = String::deserialize(deserializer)?; + let mut values: Vec> = buf + .split(',') + .into_iter() + .map(|value| { + // there's some typos so we have to cover that... + value.split_ascii_whitespace().collect::>()[0] + .trim() + .parse::() + .map_err(|err| Error::custom(err.to_string())) + }) + .collect(); + let y = values.pop().ok_or(Error::custom("InvalidField"))??; + let x = values.pop().ok_or(Error::custom("InvalidField"))??; + + Ok([x, y]) +} diff --git a/rust/src/godot/datafile.rs b/rust/src/godot/datafile.rs new file mode 100644 index 0000000..ce07342 --- /dev/null +++ b/rust/src/godot/datafile.rs @@ -0,0 +1,272 @@ +use crate::formats; +use crate::formats::datafile::{Datafile, FileEntry}; +use crate::formats::sprites::{CropMode, RenderMode, SpriteType}; +use crate::formats::{load_data, DatafileFile}; +use crate::godot::font::load_bitmap_font; +use crate::godot::game_object::parse_game_object; +use crate::godot::image::{load_bmp_as_image_texture, load_rle_as_sprite_frames}; +use crate::godot::sprites::load_sprite_frames; +use crate::godot::tile_map::{create_tile_map, TileCollision}; +use crate::godot::ui::convert_ui; +use binrw::BinRead; +use godot::engine::global::Error; +use godot::engine::image::Format; +use godot::engine::resource_loader::CacheMode; +use godot::engine::resource_saver::SaverFlags; +use godot::engine::utilities::{printerr, prints}; +use godot::engine::{ + AtlasTexture, AudioStream, AudioStreamOggVorbis, DirAccess, OggPacketSequence, + PlaceholderTexture2D, SpriteFrames, +}; +use godot::engine::{Image, PckPacker}; +use godot::engine::{ImageTexture, ProjectSettings}; +use godot::engine::{ResourceFormatLoader, ResourceSaver}; +use godot::engine::{ResourceFormatLoaderVirtual, ResourceLoader}; +use godot::prelude::*; +use itertools::Itertools; +use std::collections::HashMap; +use std::fs::File; +use std::str::FromStr; + +const DAT_PATH: &str = "E:\\Games\\Schatzjäger\\data\\datafile.dat"; + +#[derive(GodotClass)] +#[class(base=ResourceFormatLoader)] +pub struct DatafileLoader { + pub datafile_table: HashMap, + + #[base] + pub base: Base, +} + +fn convert_path(path: &GodotString) -> String { + path.to_string() + .strip_prefix("datafile://") + .map(|it| it.replace('/', "\\")) + .expect("Invalid path") +} + +#[godot_api] +impl DatafileLoader { + fn save_to_cache(&self, resource: Gd, path: String) { + let cache_path = self.get_cache_path(path); + match DirAccess::make_dir_recursive_absolute(cache_path.rsplit_once('/').unwrap().0.into()) + { + Error::OK => (), + error => printerr(error.to_variant(), &[]), + } + ResourceSaver::singleton().save(resource, cache_path.into(), SaverFlags::FLAG_NONE); + } + + fn get_cache_path(&self, path: String) -> String { + format!( + "{}/.cache/{}", + DAT_PATH + .replace('\\', "/") + .strip_suffix("datafile.dat") + .unwrap(), + path.replace('\\', "/") + ) + } + + fn retrieve_cache(&self, path: String) -> Option> + where + T: GodotClass + Inherits, + { + let cache_path = self.get_cache_path(path); + let type_hint = T::CLASS_NAME; + if !ResourceLoader::singleton().exists(cache_path.clone().into(), type_hint.into()) { + return None; + } + ResourceLoader::singleton() + .load( + cache_path.into(), + type_hint.into(), + CacheMode::CACHE_MODE_REUSE, + ) + .map(|it| it.cast()) + } +} + +#[godot_api] +impl ResourceFormatLoaderVirtual for DatafileLoader { + fn init(base: Base) -> Self { + let mut file = File::open(DAT_PATH).unwrap(); + let datafile_table = Datafile::read(&mut file).unwrap().into_index(); + + DatafileLoader { + base, + datafile_table, + } + } + + fn get_recognized_extensions(&self) -> PackedStringArray { + PackedStringArray::from(&[ + "xml".into(), + "txt".into(), + "rle".into(), + "bmp".into(), + "dat".into(), + ]) + } + + fn recognize_path(&self, path: GodotString, _type: StringName) -> bool { + path.to_string().starts_with("datafile://") + } + + fn get_resource_type(&self, path: GodotString) -> GodotString { + if path.to_string().ends_with(".dat") { + "PackedScene".into() + } else { + "Resource".into() + } + } + + fn get_resource_script_class(&self, _path: GodotString) -> GodotString { + GodotString::from_str("").unwrap() + } + + fn exists(&self, path: GodotString) -> bool { + self.datafile_table + .contains_key(convert_path(&path).as_str()) + } + + fn get_classes_used(&self, _path: GodotString) -> PackedStringArray { + PackedStringArray::from(&[]) + } + + fn load( + &self, + path: GodotString, + _original_path: GodotString, + _use_sub_threads: bool, + _cache_mode: i64, + ) -> Variant { + let datafile_path = convert_path(&path); + if let Some(resource) = self.retrieve_cache::(format!( + "{}.{}", + datafile_path, + if datafile_path.ends_with(".xml") || datafile_path.ends_with("dat") { + "scn" + } else { + "res" + } + )) { + return resource.to_variant(); + } + + if let Some(target) = self.datafile_table.get(datafile_path.as_str()) { + let mut file = File::open(DAT_PATH).unwrap(); + match load_data(target, &mut file) { + Ok(DatafileFile::Level(level)) => { + let level_id = datafile_path + .split_terminator('\\') + .find(|i| i.starts_with("level")) + .map(|lvl| u32::from_str(lvl.strip_prefix("level").unwrap()).unwrap()) + .unwrap(); + let tile_map = create_tile_map(level, level_id); + + self.save_to_cache(tile_map.share().upcast(), format!("{}.scn", datafile_path)); + tile_map.to_variant() + } + Ok(DatafileFile::Txt(txt)) => { + let game_object = parse_game_object(txt); + self.save_to_cache( + game_object.share().upcast(), + format!("{}.res", datafile_path), + ); + game_object.to_variant() + } + Ok(DatafileFile::Ui(ui)) => { + let ui = convert_ui(ui); + let mut scene = PackedScene::new(); + scene.pack(ui.upcast()); + + self.save_to_cache(scene.share().upcast(), format!("{}.scn", datafile_path)); + scene.to_variant() + } + Ok(DatafileFile::Vorbis(vorbis)) => { + let mut audio = AudioStreamOggVorbis::new(); + audio.set_loop(true); + let mut packet = OggPacketSequence::new(); + packet.set_packet_data(Array::from(&[Array::from(&[PackedByteArray::from( + vorbis.as_slice(), + ) + .to_variant()])])); + audio.set_packet_sequence(packet); + audio.to_variant() + } + Ok(DatafileFile::RleSprite(rle)) => load_rle_as_sprite_frames(*rle).to_variant(), + Ok(DatafileFile::Sprites(sprites)) => { + let sprite_frames = load_sprite_frames(sprites, path); + + self.save_to_cache( + sprite_frames.share().upcast(), + format!("{}.res", datafile_path), + ); + sprite_frames.to_variant() + } + Ok(DatafileFile::Bitmap(data)) => { + let gd_image = match load_bmp_as_image_texture(data) { + Ok(image) => image, + Err(err) => return err.to_variant(), + }; + + if datafile_path.contains("\\fonts\\") { + let font = load_bitmap_font(gd_image); + + self.save_to_cache( + font.share().upcast(), + format!("{}.tres", datafile_path), + ); + font.to_variant() + } else { + let mut texture = ImageTexture::new(); + texture.set_image(gd_image); + + self.save_to_cache( + texture.share().upcast(), + format!("{}.res", datafile_path), + ); + texture.to_variant() + } + } + Ok(DatafileFile::TileCollision(collision)) => { + let tile_collision = Gd::new(TileCollision { + collision: collision + .chars() + .filter_map(|c| c.to_digit(10)) + .map(|d| d as u8) + .collect(), + }); + + // No need to save this to cache, we only use this internally + /*self.save_to_cache( + tile_collision.share().upcast(), + format!("{}.res", datafile_path), + );*/ + tile_collision.to_variant() + } + Err(formats::Error::UnknownFormat(ext)) => { + printerr(format!("Unknown format <{}>", ext).to_variant(), &[]); + Error::ERR_FILE_UNRECOGNIZED.to_variant() + } + Err(formats::Error::Deserialization) => { + printerr("Failed to deserialize".to_variant(), &[]); + Error::ERR_FILE_CORRUPT.to_variant() + } + Err(formats::Error::Custom(message)) => { + printerr(message.to_variant(), &[]); + Error::ERR_BUG.to_variant() + } + _ => { + printerr("Unknown error".to_variant(), &[]); + Error::ERR_BUG.to_variant() + } + } + } else { + printerr("File not found".to_variant(), &[]); + Error::ERR_FILE_NOT_FOUND.to_variant() + } + } +} diff --git a/rust/src/godot/font.rs b/rust/src/godot/font.rs new file mode 100644 index 0000000..9e7646a --- /dev/null +++ b/rust/src/godot/font.rs @@ -0,0 +1,88 @@ +use godot::builtin::{Rect2, Vector2, Vector2i}; +use godot::engine::{FontFile, Image}; +use godot::prelude::utilities::{print_verbose, prints}; +use godot::prelude::{Color, Gd, Share, ToVariant}; +use std::ops::Index; +use unicode_segmentation::UnicodeSegmentation; + +const CHARSET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜabcdefghijklmnopqrstuvwxyzäöüß0123456789,;.:!?\ + +-*/=<>()[]{}\"$%&#~_’^@|¡¿™©®º¹²³ªÀÁÂÃÅÆÇÈÉÊËÌÍÎÏIÐGÑÒÓÔÕŒØSŠÙÚÛÝÞŸŽàáâãåæçèéêëìíî\ + ïiðgñòóôõœøsšùúûýþÿž£¥ƒ¤¯¦¬¸¨·§×¢±÷µ«»"; + +pub fn load_bitmap_font(image: Gd) -> Gd { + let mut font_chars = CHARSET.as_bytes().iter(); + + let mut font_file = FontFile::new(); + + let chroma_key = Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.0, + }; + + let mut was_empty_column = true; + let mut char_x = 0; + let mut char_width = 0; + let char_height = image.get_height(); + let char_y = 0; + + let base_size = Vector2i { x: 16, y: 0 }; + + font_file.set_texture_image(0, base_size, 0, image.share()); + + for x in 0..image.get_width() { + let is_empty_column = (0..image.get_height()).all(|y| image.get_pixel(x, y).a == 0.0); + + if !was_empty_column && is_empty_column { + let char = font_chars.next().expect("Font has too many characters!"); + let glyph = *char as i64; + /*let mut glyph = 0i64; + for (i, c) in char.bytes().rev().enumerate() { + glyph |= (c as i64) << (i * 8); + }*/ + + let glyph_offset = Vector2 { + x: char_x as f32, + y: char_y as f32, + }; + let glyph_size = Vector2 { + x: char_width as f32, + y: char_height as f32, + }; + + prints( + "Glyph".to_variant(), + &[ + (*char as char).to_string().to_variant(), + glyph_offset.to_variant(), + glyph_size.to_variant(), + ], + ); + + // font_file.set_glyph_offset(0, base_size, glyph, glyph_offset); + font_file.set_glyph_size(0, base_size, glyph, glyph_size); + font_file.set_glyph_uv_rect( + 0, + base_size, + glyph, + Rect2 { + position: glyph_offset, + size: glyph_size, + }, + ); + font_file.set_glyph_texture_idx(0, base_size, glyph, 0); + } else if was_empty_column && !is_empty_column { + char_x = x; + char_width = 0; + } + + char_width += 1; + was_empty_column = is_empty_column; + } + + font_file.set_font_name("menufont".into()); + // font_file.set_cache_ascent(0, base_size.x, ) + + font_file +} diff --git a/rust/src/godot/game_object.rs b/rust/src/godot/game_object.rs new file mode 100644 index 0000000..de9416a --- /dev/null +++ b/rust/src/godot/game_object.rs @@ -0,0 +1,138 @@ +use godot::engine::Resource; +use godot::prelude::*; +use itertools::Itertools; +use std::str::FromStr; + +#[derive(GodotClass)] +#[class(base=Resource, init)] +pub struct ObjectScript { + #[export] + pub dynamic_objects: Array>, + #[export] + pub static_objects: Array>, + #[base] + base: Base, +} + +#[godot_api] +impl ObjectScript {} + +#[derive(GodotClass)] +#[class(base=Resource, init)] +pub struct ObjectData { + #[export] + pub class_type: GodotString, + #[export] + pub resource_type: GodotString, + #[export] + pub name: GodotString, + #[export] + pub props: Dictionary, + #[export] + pub children: Array>, + #[base] + base: Base, +} + +#[godot_api] +impl ObjectData {} + +pub fn parse_game_object(contents: String) -> Gd { + Gd::::with_base(|base| { + let mut object_script = ObjectScript { + dynamic_objects: Array::new(), + static_objects: Array::new(), + base, + }; + + let mut lines = contents + .lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .filter(|l| !l.starts_with('#')); + + while let Some(line) = lines.next() { + match line { + "DYNAMIC OBJECT START" => { + object_script.dynamic_objects.push(read_object(&mut lines)) + } + "OBJECT START" => object_script.static_objects.push(read_object(&mut lines)), + l => eprintln!("TODO: {}", l), + }; + } + + object_script + }) +} + +pub fn read_object<'s, I>(lines: &mut I) -> Gd +where + I: Iterator, +{ + let class_type = lines + .next() + .unwrap() + .strip_prefix("class type:") + .unwrap() + .trim() + .trim_matches('"'); + let (resource_type, name) = lines + .next() + .unwrap() + .splitn(2, ']') + .map(|x| x.trim()) + .collect_tuple::<(&str, &str)>() + .unwrap(); + + Gd::::with_base(|base| { + let mut object_data = ObjectData { + class_type: class_type.into(), + resource_type: resource_type + .trim_start_matches('[') + .trim_end_matches(']') + .into(), + name: name.trim_matches('"').into(), + props: Dictionary::new(), + children: Array::new(), + base, + }; + + lines.next(); + loop { + match lines.next().unwrap() { + "}" => break, + l => { + let (_, key, value) = l + .splitn(3, '"') + .map(|x| x.trim()) + .collect_tuple::<(&str, &str, &str)>() + .unwrap(); + let values = value + .split_whitespace() + .map(|s| f32::from_str(s).unwrap()) + .collect_vec(); + object_data.props.insert( + key, + match values.len() { + 1 => values[0].to_variant(), + 2 => Vector2 { + x: values[0], + y: values[1], + } + .to_variant(), + 3 => Vector3 { + x: values[0], + y: values[1], + z: values[2], + } + .to_variant(), + _ => panic!(), + }, + ); + } + } + } + + object_data + }) +} diff --git a/rust/src/godot/image.rs b/rust/src/godot/image.rs new file mode 100644 index 0000000..6f21b9f --- /dev/null +++ b/rust/src/godot/image.rs @@ -0,0 +1,57 @@ +use crate::formats::rle::RleImage; +use godot::builtin::{Color, PackedByteArray}; +use godot::engine::global::Error; +use godot::engine::image::Format; +use godot::engine::{Image, ImageTexture, SpriteFrames}; +use godot::obj::Gd; + +const FPS: f64 = 15.0; + +pub fn load_rle_as_sprite_frames(rle: RleImage) -> Gd { + let mut frames = SpriteFrames::new(); + + frames.set_animation_loop("default".into(), true); + frames.set_animation_speed("default".into(), FPS); + + for frame in rle.frames.iter() { + let mut image = Image::new(); + image.set_data( + rle.width as i64, + rle.height as i64, + false, + Format::FORMAT_RGBA8, + PackedByteArray::from(rle.get_image_data(frame).as_slice()), + ); + image.fix_alpha_edges(); + + let mut texture = ImageTexture::new(); + texture.set_image(image); + frames.add_frame("default".into(), texture.upcast(), 1.0, 0); + } + + frames +} + +pub fn load_bmp_as_image_texture(data: Vec) -> Result, Error> { + let mut image = Image::new(); + + match image.load_bmp_from_buffer(data.as_slice().into()) { + Error::OK => { + for x in 0..image.get_width() { + for y in 0..image.get_height() { + if image.get_pixel(x, y).is_equal_approx(Color { + r: 1.0, + g: 0.0, + b: 1.0, + a: 1.0, + }) { + image.set_pixel(x, y, Color::TRANSPARENT_BLACK); + } + } + } + image.fix_alpha_edges(); + Ok(image) + } + error => Err(error), + } +} diff --git a/rust/src/godot/mod.rs b/rust/src/godot/mod.rs new file mode 100644 index 0000000..eb2a971 --- /dev/null +++ b/rust/src/godot/mod.rs @@ -0,0 +1,7 @@ +pub mod datafile; +pub mod font; +pub mod game_object; +pub mod image; +pub mod sprites; +pub mod tile_map; +pub mod ui; diff --git a/rust/src/godot/sprites.rs b/rust/src/godot/sprites.rs new file mode 100644 index 0000000..aca5405 --- /dev/null +++ b/rust/src/godot/sprites.rs @@ -0,0 +1,133 @@ +use crate::formats::sprites::{CropMode, RenderMode, Sprites}; +use godot::builtin::{GodotString, Rect2, StringName, ToVariant, Vector2}; +use godot::engine::utilities::printerr; +use godot::engine::{ + load, AtlasTexture, ImageTexture, PlaceholderTexture2D, ResourceLoader, SpriteFrames, +}; +use godot::obj::{Gd, Share}; +use godot::prelude::GodotClass; + +const FPS: f64 = 15.0; +const SPRITE_EXTENSIONS: &[&str] = &["bmp", "rle"]; + +pub fn load_sprite_frames(sprites: Vec, path: GodotString) -> Gd { + let dir = path + .to_string() + .strip_suffix("/sprites.txt") + .unwrap() + .to_string(); + let mut sprite_frames = SpriteFrames::new(); + for sprite in sprites.into_iter() { + if let RenderMode::FlipX = sprite.render_mode { + continue; + } + sprite_frames.add_animation(StringName::from(&sprite.name)); + sprite_frames.set_animation_speed(StringName::from(&sprite.name), FPS); + + match select_from_extensions(&dir, &sprite.file_name) { + Some((path, "rle")) => extract_rle_frames(&mut sprite_frames, &sprite, path), + Some((path, "bmp")) => extract_bitmap_frames(&mut sprite_frames, &sprite, path), + Some(_) | None => { + printerr( + format!("Missing sprite '{}'", sprite.file_name).to_variant(), + &[], + ); + let texture = PlaceholderTexture2D::new(); + sprite_frames.add_frame( + StringName::from(&sprite.name), + texture.upcast(), + 60.0 / FPS, + 0, + ); + } + } + } + + sprite_frames +} + +/// Loads an RLE file as SpriteFrames and extracts +/// its frames into `sprite_frames` +fn extract_rle_frames(sprite_frames: &mut SpriteFrames, sprite: &Sprites, path: String) { + let frames: Gd = load(path); + for frame_idx in 0..frames.get_frame_count("default".into()) { + sprite_frames.add_frame( + StringName::from(&sprite.name), + frames + .get_frame_texture("default".into(), frame_idx) + .unwrap(), + 60.0 / FPS, + 0, + ); + } +} + +/// Loads a bitmap and extracts its frames into `sprite_frames` +/// creates an atlas if there are multiple frames. +fn extract_bitmap_frames(sprite_frames: &mut SpriteFrames, sprite: &Sprites, path: String) { + let texture: Gd = load(path); + + let frame_count = if let Some(CropMode::FrameCount(frame_count)) = sprite.frames { + frame_count + } else { + 1 + }; + + if frame_count > 1 { + let height = texture.get_height(); + let width = texture.get_width(); + let frame_height = height / frame_count as i64; + + for i in 0..frame_count as i64 { + let mut atlas = AtlasTexture::new(); + atlas.set_atlas(texture.share().upcast()); + atlas.set_region(Rect2 { + position: Vector2 { + x: 0.0, + y: (i * frame_height) as f32, + }, + size: Vector2 { + x: width as f32, + y: frame_height as f32, + }, + }); + + sprite_frames.add_frame( + StringName::from(&sprite.name), + atlas.upcast(), + 60.0 / FPS, + 0, + ); + } + } else { + sprite_frames.add_frame( + StringName::from(&sprite.name), + texture.upcast(), + 60.0 / FPS, + 0, + ); + } +} + +/// Selects the extension based on which file exists +fn select_from_extensions(dir: &str, file_name: &str) -> Option<(String, &'static str)> { + SPRITE_EXTENSIONS + .iter() + .map(|ext| { + ( + format!("{}/sprites/{}.{}", dir, file_name.to_lowercase(), ext), + *ext, + ) + }) + .find(|(path, ext)| { + ResourceLoader::singleton().exists( + path.clone().into(), + match *ext { + "rle" => SpriteFrames::CLASS_NAME.to_string(), + "bmp" => ImageTexture::CLASS_NAME.to_string(), + _ => panic!(), + } + .into(), + ) + }) +} diff --git a/rust/src/godot/tile_map.rs b/rust/src/godot/tile_map.rs new file mode 100644 index 0000000..8a381fb --- /dev/null +++ b/rust/src/godot/tile_map.rs @@ -0,0 +1,139 @@ +use crate::formats::level::LevelLayer; +use godot::engine::global::Error; +use godot::engine::utilities::{clampi, printerr}; +use godot::engine::{load, PackedScene}; +use godot::engine::{ImageTexture, TileSet}; +use godot::engine::{TileMap, TileSetAtlasSource}; +use godot::prelude::*; +use godot::prelude::{Gd, PackedByteArray, Share, ToVariant}; + +pub fn create_tile_map(layer: LevelLayer, level_id: u32) -> Gd { + let mut tile_set = TileSet::new(); + tile_set.set_tile_size(Vector2i { x: 32, y: 32 }); + tile_set.add_physics_layer(0); + let mut map = TileMap::new_alloc(); + map.set_tileset(tile_set.share()); + map.set_quadrant_size(32); + + for x in 0..layer.width { + for y in 0..layer.height { + let tile = &layer.tiles[(y * layer.width + x) as usize]; + if tile.id == 0 { + continue; + } + if !tile_set.has_source(tile.id as i64) { + let atlas_id = tile.id as u32 + 1; + let atlas = load_atlas(1, atlas_id, layer.tile_count); + tile_set.add_source(atlas.share().upcast(), tile.id as i64); + add_collision(atlas, level_id, atlas_id); + } + map.set_cell( + 0, + Vector2i { + x: x as i32, + y: y as i32, + }, + tile.id as i64, + Vector2i { + x: clampi(tile.index as i64 % 16, 0, 15) as i32, + y: clampi(tile.index as i64 / 16, 0, 15) as i32, + }, + 0, + ); + } + } + + let mut scene = PackedScene::new(); + let error = scene.pack(map.upcast()); + match error { + Error::OK => (), + e => printerr(e.to_variant(), &[]), + } + scene +} + +#[derive(GodotClass)] +#[class(base=Resource, init)] +pub struct TileCollision { + #[export] + pub collision: PackedByteArray, +} + +#[godot_api] +impl TileCollision {} + +fn add_collision(atlas: Gd, level_id: u32, atlas_id: u32) { + let tile_collision: Gd = load(format!( + "datafile://data/level{:02}/tile_collision_{:02}.txt", + level_id, atlas_id + )); + let width = atlas.get_atlas_grid_size().x; + let height = atlas.get_atlas_grid_size().y; + + let tile_width = atlas.get_texture_region_size().x as f32 / 2.0; + let tile_height = atlas.get_texture_region_size().y as f32 / 2.0; + let collision = &[ + Vector2 { + x: -tile_width, + y: -tile_height, + }, + Vector2 { + x: -tile_width, + y: tile_height, + }, + Vector2 { + x: tile_width, + y: tile_height, + }, + Vector2 { + x: tile_width, + y: -tile_height, + }, + ]; + + for x in 0..width { + for y in 0..height { + let collision_data = tile_collision + .bind() + .collision + .get((y * width + x) as usize); + let mut data = atlas.get_tile_data(Vector2i { x, y }, 0).unwrap(); + if collision_data & 0x1 != 0 { + data.add_collision_polygon(0); + data.set_collision_polygon_points(0, 0, PackedVector2Array::from(collision)); + } else if collision_data & 0xfe != 0 { + printerr( + format!("Missing collision info for {}", collision_data).to_variant(), + &[], + ); + } + } + } +} + +fn load_atlas(set_id: u32, atlas_id: u32, tile_count: u32) -> Gd { + let mut atlas = TileSetAtlasSource::new(); + let tex: Gd = load(format!( + "datafile://data/set{}/sprites/tiles_{:02}.bmp", + set_id, atlas_id, + )); + let region_size = (tile_count as f32).sqrt(); + debug_assert_eq!(tex.get_width(), tex.get_height()); + debug_assert_eq!(region_size, region_size.trunc()); + + let tile_size = (tex.get_width() / region_size as i64) as i32; + + atlas.set_texture(tex.upcast()); + atlas.set_texture_region_size(Vector2i { + x: tile_size, + y: tile_size, + }); + + for x in 0..region_size as i32 { + for y in 0..region_size as i32 { + atlas.create_tile(Vector2i { x, y }, Vector2i { x: 1, y: 1 }); + } + } + + atlas +} diff --git a/rust/src/godot/ui.rs b/rust/src/godot/ui.rs new file mode 100644 index 0000000..856cc63 --- /dev/null +++ b/rust/src/godot/ui.rs @@ -0,0 +1,58 @@ +use crate::formats::ui_xml::{HorizontalAlign, UiTag}; +use godot::builtin::{GodotString, Vector2}; +use godot::engine::global::HorizontalAlignment; +use godot::engine::node::InternalMode; +use godot::engine::{Button, Container, Control, TextureRect}; +use godot::prelude::*; + +pub fn convert_ui(ui: UiTag) -> Gd { + match ui { + UiTag::Menu(menu) => { + let mut gd_menu = Container::new_alloc(); + for child in menu.children { + gd_menu.add_child( + convert_ui(child).upcast(), + false, + InternalMode::INTERNAL_MODE_FRONT, + ); + } + gd_menu.upcast() + } + UiTag::Image(image) => { + let mut gd_image = TextureRect::new_alloc(); + gd_image.set_position( + Vector2 { + x: image.position[0] as f32, + y: image.position[1] as f32, + }, + false, + ); + gd_image.set_size( + Vector2 { + x: image.size[0] as f32, + y: image.size[1] as f32, + }, + false, + ); + gd_image.upcast() + } + UiTag::TextButton(button) => { + let mut gd_button = Button::new_alloc(); + gd_button.set_position( + Vector2 { + x: button.position[0] as f32, + y: button.position[1] as f32, + }, + false, + ); + gd_button.set_text_alignment(match button.horizontal_align { + HorizontalAlign::Center => HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER, + }); + if let Some(name) = button.name { + gd_button.set_name(GodotString::from(name)); + } + gd_button.set_text(GodotString::from(button.text)); + gd_button.upcast() + } + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..6be175a --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,40 @@ +use crate::godot::datafile::DatafileLoader; +use ::godot::engine::class_macros::auto_register_classes; +use ::godot::engine::{ResourceFormatLoaderVirtual, ResourceLoader}; +use ::godot::init::{gdextension, ExtensionLayer}; +use ::godot::prelude::{ExtensionLibrary, Gd, InitHandle, InitLevel, Share}; + +pub mod formats; +pub mod godot; + +struct Main {} + +#[gdextension] +unsafe impl ExtensionLibrary for Main { + fn load_library(handle: &mut InitHandle) -> bool { + handle.register_layer(InitLevel::Editor, ResourceLoaderLayer { datafile: None }); + true + } +} + +struct ResourceLoaderLayer { + pub datafile: Option>, +} + +impl ExtensionLayer for ResourceLoaderLayer { + fn initialize(&mut self) { + auto_register_classes(); + + self.datafile = Some(Gd::::with_base(DatafileLoader::init)); + + ResourceLoader::singleton() + .add_resource_format_loader(self.datafile.as_ref().unwrap().share().upcast(), true); + } + + fn deinitialize(&mut self) { + if let Some(datafile) = &self.datafile { + ResourceLoader::singleton().remove_resource_format_loader(datafile.share().upcast()); + self.datafile = None; + } + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs new file mode 100644 index 0000000..324a53d --- /dev/null +++ b/rust/src/main.rs @@ -0,0 +1,130 @@ +use binrw::{BinRead, NullString}; +use image::codecs::gif::{GifEncoder, Repeat}; +use image::{AnimationDecoder, ImageFormat}; +use mhjnr::formats::datafile::Datafile; +use mhjnr::formats::level::level_tile_data_to_image; +use mhjnr::formats::rle::RleImage; +use mhjnr::formats::sprites::Sprites; +use mhjnr::formats::txt::{decrypt_exposed_txt, decrypt_txt}; +use mhjnr::formats::ui_xml::UiTag; +use serde_xml_rs::from_str; +use std::ffi::OsStr; +use std::fs; +use std::fs::{File, OpenOptions}; +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +fn extract(datafile: &Datafile, file: &mut File) { + let target = "E:\\Games\\Schatzjäger\\data3"; + + for entry in &datafile.files { + let file_name = format!("{}\\{}", target, entry.name); + fs::create_dir_all(file_name.rsplit_once('\\').unwrap().0).unwrap(); + + file.seek(SeekFrom::Start(entry.pos as u64)).unwrap(); + let mut data = vec![0u8; entry.len as usize]; + file.read_exact(&mut data).unwrap(); + + if entry.name.to_string().ends_with(".txt") { + let mut contents = decrypt_txt(data.into_iter()).unwrap(); + /*if entry + .name + .to_string() + .split('\\') + .collect::>() + .len() + == 1 + { + contents = decrypt_exposed_txt(contents).unwrap(); + }*/ + File::create(file_name) + .unwrap() + .write_all(contents.as_bytes()) + .unwrap(); + } else if entry.name.to_string().ends_with(".rle") { + let image: RleImage = RleImage::read(&mut Cursor::new(data)).unwrap(); + let mut encoder = GifEncoder::new( + OpenOptions::new() + .create(true) + .write(true) + .open(format!( + "{}.{}", + file_name.strip_suffix(".rle").unwrap(), + ".gif" + )) + .unwrap(), + ); + encoder.set_repeat(Repeat::Infinite).unwrap(); + encoder.try_encode_frames(image.into_frames()).unwrap(); + } else { + File::create(file_name) + .unwrap() + .write_all(data.as_slice()) + .unwrap(); + } + } +} + +fn main() { + let file_name = Some(NullString::from("data\\loading\\sprites.txt")); + let dat_path = "E:\\Games\\Schatzjäger\\data\\datafile.dat"; + + let mut file = File::open(dat_path).unwrap(); + let dat: Datafile = Datafile::read(&mut file).unwrap(); + println!("{:#?}", dat); + + extract(&dat, &mut file); + + /*if let Some(file_name) = file_name { + let target = dat.files.iter().find(|it| it.name == file_name).unwrap(); + file.seek(SeekFrom::Start(target.pos as u64)).unwrap(); + let mut data = vec![0u8; target.len as usize]; + file.read_exact(&mut data).unwrap(); + + match Path::new(&file_name.to_string()) + .extension() + .and_then(OsStr::to_str) + { + Some("xml") => println!( + "{:#?}", + from_str::(String::from_utf8(data).unwrap().as_str()) + ), + Some("txt") => { + if false { + /*let decr = decrypt_txt(&mut data); + let entries: String = decrypt_exposed_txt(decr);*/ + let decr = decrypt_txt(data.into_iter()).unwrap(); + println!("{}", &decr); + let sprites = Sprites::parse(decr.as_str()).unwrap(); + println!("{:#?}", sprites); + } else { + println!("{}", decrypt_txt(data.into_iter()).unwrap()) + } + } + Some("rle") => { + let image: RleImage = RleImage::read(&mut Cursor::new(data)).unwrap(); + let path = Path::new(dat_path).with_file_name("res.gif"); + println!("{:?}", path); + let mut encoder = GifEncoder::new( + OpenOptions::new() + .create(true) + .write(true) + .open(path) + .unwrap(), + ); + encoder.set_repeat(Repeat::Infinite).unwrap(); + encoder.try_encode_frames(image.into_frames()).unwrap(); + } + Some("dat") => { + let image = level_tile_data_to_image(&data).unwrap(); + let path = Path::new(dat_path).with_file_name("res.png"); + println!("{:?}", path); + image.save_with_format(path, ImageFormat::Png).unwrap(); + } + Some(ext) => eprintln!("Unknown file extension <{}>", ext), + None => eprintln!("Failed to read"), + } + }*/ +} + +// pub fn decr2()