diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da5aba5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Godot 4+ specific ignores +.godot/ +/android/ +bin/ diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..23eda07 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,39 @@ +[preset.0] + +name="Linux" +platform="Linux" +runnable=true +advanced_options=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="bin/linux/godot_launcher.x86_64" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=true +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +binary_format/architecture="x86_64" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/usr/bin/env bash +export DISPLAY=:0 +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash +kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\") +rm -rf \"{temp_dir}\"" diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..10279c1 Binary files /dev/null and b/icon.png differ diff --git a/icon.png.import b/icon.png.import new file mode 100644 index 0000000..770e148 --- /dev/null +++ b/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://4funfxvuy736" +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/icon.svg b/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..4423298 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2coajxj33jhm" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..7b5a658 --- /dev/null +++ b/project.godot @@ -0,0 +1,22 @@ +; 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="GodotLauncher" +run/main_scene="res://scenes/main.tscn" +config/features=PackedStringArray("4.3", "Forward Plus") +boot_splash/show_image=false +config/icon="res://icon.png" + +[display] + +window/size/viewport_width=640 +window/size/viewport_height=720 diff --git a/scenes/choice.tscn b/scenes/choice.tscn new file mode 100644 index 0000000..a5741d5 --- /dev/null +++ b/scenes/choice.tscn @@ -0,0 +1,20 @@ +[gd_scene format=3 uid="uid://cbt3yb2kuvefu"] + +[node name="HBoxContainer" type="HBoxContainer"] + +[node name="Name" type="Label" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +text = "v4.1.2" + +[node name="OpenButton" type="Button" parent="."] +layout_mode = 2 +text = " Open " + +[node name="UninstallButton" type="Button" parent="."] +layout_mode = 2 +text = " Uninstall " + +[node name="ArgsButton" type="Button" parent="."] +layout_mode = 2 +text = " Change Args " diff --git a/scenes/main.tscn b/scenes/main.tscn new file mode 100644 index 0000000..226979c --- /dev/null +++ b/scenes/main.tscn @@ -0,0 +1,136 @@ +[gd_scene load_steps=2 format=3 uid="uid://3h5re4sw8b54"] + +[ext_resource type="Script" path="res://scripts/main.gd" id="1_yc2qs"] + +[node name="PanelContainer" type="TabContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +current_tab = 0 +tabs_visible = false +script = ExtResource("1_yc2qs") + +[node name="NormalView" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 32 +theme_override_constants/margin_top = 32 +theme_override_constants/margin_right = 32 +theme_override_constants/margin_bottom = 23 +metadata/_tab_index = 0 + +[node name="Root" type="VBoxContainer" parent="NormalView"] +layout_mode = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="NormalView/Root"] +layout_mode = 2 +size_flags_vertical = 3 +follow_focus = true +horizontal_scroll_mode = 0 + +[node name="MarginContainer" type="MarginContainer" parent="NormalView/Root/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 12 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 12 + +[node name="ChoiceContainer" type="VBoxContainer" parent="NormalView/Root/ScrollContainer/MarginContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="NewButton" type="Button" parent="NormalView/Root"] +layout_mode = 2 +text = "New Version" + +[node name="NewView" type="MarginContainer" parent="."] +visible = false +layout_mode = 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 +metadata/_tab_index = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="NewView"] +layout_mode = 2 + +[node name="Label" type="Label" parent="NewView/VBoxContainer"] +layout_mode = 2 +text = "Godot Version" +vertical_alignment = 2 + +[node name="VersionOption" type="OptionButton" parent="NewView/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +selected = 0 +item_count = 1 +popup/item_0/text = "Loading..." + +[node name="Label2" type="Label" parent="NewView/VBoxContainer"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +text = "C# Enabled" +vertical_alignment = 2 + +[node name="MonoOption" type="OptionButton" parent="NewView/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +selected = 0 +item_count = 2 +popup/item_0/text = "Regular" +popup/item_1/text = "Mono (C#)" +popup/item_1/id = 1 + +[node name="Status" type="Label" parent="NewView/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +horizontal_alignment = 1 +vertical_alignment = 1 + +[node name="CreateButton" type="Button" parent="NewView/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 8 +text = "Create" + +[node name="DoneButton" type="Button" parent="NewView/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 8 +text = "Done" + +[node name="EditArgsView" type="MarginContainer" parent="."] +visible = false +layout_mode = 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 +metadata/_tab_index = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="EditArgsView"] +layout_mode = 2 + +[node name="Label" type="Label" parent="EditArgsView/VBoxContainer"] +layout_mode = 2 +text = "Arguments" + +[node name="ArgsEdit" type="TextEdit" parent="EditArgsView/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Button" type="Button" parent="EditArgsView/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 8 +text = "Done" + +[connection signal="pressed" from="NormalView/Root/NewButton" to="." method="_open_create_menu"] +[connection signal="pressed" from="NewView/VBoxContainer/CreateButton" to="." method="_create_version"] +[connection signal="pressed" from="NewView/VBoxContainer/DoneButton" to="." method="set_current_tab" binds= [0]] +[connection signal="pressed" from="EditArgsView/VBoxContainer/Button" to="." method="_modify_args"] diff --git a/scripts/main.gd b/scripts/main.gd new file mode 100644 index 0000000..1e249f0 --- /dev/null +++ b/scripts/main.gd @@ -0,0 +1,216 @@ +extends TabContainer + +const SAVE_PATH: String = "user://save.json" + +var choice_res: PackedScene = preload("res://scenes/choice.tscn") +@onready var choice_container: Node = %ChoiceContainer +@onready var version_option: OptionButton = %VersionOption +@onready var mono_option: OptionButton = %MonoOption +@onready var status: Label = %Status +@onready var args_edit: TextEdit = %ArgsEdit + +var save: Dictionary = {} +var downloads: Dictionary = {} +var current_args_modify: Dictionary = {} + + +func _ready(): + DirAccess.make_dir_recursive_absolute("user://versions") + + if FileAccess.file_exists(SAVE_PATH): + save = JSON.parse_string(FileAccess.get_file_as_string(SAVE_PATH)) + + _load_save() + + +func _load_save() -> void: + if !save.has("versions"): + save.versions = [] + + for child in choice_container.get_children(): + child.queue_free() + + for version in save.versions: + var choice: Node = choice_res.instantiate() + choice.get_node("Name").text = version.name + choice.get_node("OpenButton").pressed.connect(_on_open.bind(version)) + choice.get_node("UninstallButton").pressed.connect(_on_uninstall.bind(version)) + choice.get_node("ArgsButton").pressed.connect(_on_args.bind(version)) + + choice_container.add_child(choice) + + +func _save_save() -> void: + var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE) + file.store_string(JSON.stringify(save)) + file.close() + + +func _on_open(version: Dictionary) -> void: + var dir = DirAccess.open("user://versions/" + version.name) + if dir == null: + print("An error occurred when trying to access the path.") + return + + var file: String = _search_path(dir, func(file_name: String): + return file_name.ends_with(".x86_64") or file_name.ends_with(".64") + ) + + var executable: String = ProjectSettings.globalize_path(dir.get_current_dir().path_join(file)) + var args: String = version.args.replace("\n", " ") + if args.contains("%command%"): + args = args.replace("%command%", executable) + else: + args = executable + " " + args + + print("Running command: " + args) + + OS.execute_with_pipe("bash", ["-c", "cd ~;" + args]) + + get_tree().quit(0) + + +func _search_path(dir: DirAccess, on_file: Callable, previous: String = "") -> String: + if dir == null: + print("An error occurred when trying to access the path.") + return "" + + var dirs: Array = [] + + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + if dir.current_is_dir(): + dirs.append(file_name) + else: + if on_file.call(previous.path_join(file_name)): + return previous.path_join(file_name) + + file_name = dir.get_next() + + for other in dirs: + var new_path = previous.path_join(other) + print("opening ", new_path) + var new_dir = DirAccess.open(dir.get_current_dir().path_join(new_path)) + var result = _search_path(new_dir, on_file, new_path) + if result != "//": + return result + + return "//" + + + +func _on_uninstall(version: Dictionary) -> void: + OS.execute("rm", ["-r", ProjectSettings.globalize_path("user://versions".path_join(version.name))]) + + save.versions.erase(version) + + _save_save() + _load_save() + + +func _open_create_menu() -> void: + current_tab = 1 + + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_recv_versions) + + var error = http_request.request("https://api.github.com/repos/godotengine/godot/releases") + if error != OK: + push_error("An error occurred in the HTTP request.") + + +func _recv_versions(_result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + var json: Array = JSON.parse_string(body.get_string_from_utf8()) + + for release: Dictionary in json: + var r_name: String = release.tag_name + var stable_download = null + var mono_download = null + + for asset: Dictionary in release.assets: + if asset.name.contains("linux.x86_64") or asset.name.contains("linux_x86_64") or \ + asset.name.contains("x11.64") or asset.name.contains("x11_64"): + if asset.name.contains("mono"): + mono_download = asset.browser_download_url + else: + stable_download = asset.browser_download_url + + downloads[r_name] = { + "name": r_name, + "stable": stable_download, + "mono": mono_download + } + + # Add versions to options but sorted + version_option.clear() + + var downs: Array = downloads.values() + downs.sort_custom(func(a, b): return a.name > b.name) + + for down in downs: + version_option.add_item(down.name) + + +func _create_version() -> void: + status.text = "downloading godot" + + var r_name: String = version_option.get_item_text(version_option.selected) + var version: Dictionary = downloads[r_name] + var mono: bool = mono_option.get_selected_id() == 1 + var download_url: String = version.mono if mono else version.stable + + if mono: + r_name += "_mono" + + var download_dir: String = "user://versions/" + r_name + + DirAccess.make_dir_recursive_absolute(download_dir) + + var http_request = HTTPRequest.new() + add_child(http_request) + http_request.download_file = download_dir.path_join("godot_zip.zip") + http_request.request_completed.connect(_recv_download.bind(download_dir, r_name)) + + var error = http_request.request(download_url) + if error != OK: + push_error("An error occurred in the HTTP request.") + + +func _recv_download(_result: int, _response_code: int, _headers: PackedStringArray, _body: PackedByteArray, \ + download_dir: String, r_name: String) -> void: + status.text = "extracting godot" + + get_tree().process_frame.connect(_extract_file.bind(download_dir, r_name), CONNECT_ONE_SHOT) + + +func _extract_file(download_dir: String, r_name: String) -> void: + var dir: String = ProjectSettings.globalize_path(download_dir) + OS.execute("bash", ["-c", "cd " + dir + ";unzip godot_zip.zip"]) + DirAccess.remove_absolute(dir.path_join("godot_zip.zip")) + + save.versions.append({ + "name": r_name, + "args": "" + }) + + _save_save() + _load_save() + + status.text = "done" + + +func _on_args(version: Dictionary) -> void: + current_args_modify = version + args_edit.text = version.args + current_tab = 2 + + +func _modify_args() -> void: + current_args_modify.args = args_edit.text + + _save_save() + _load_save() + + current_tab = 0