diff --git a/scenes/Catapult.tscn b/scenes/Catapult.tscn index 4c87f1be..e38525c3 100644 --- a/scenes/Catapult.tscn +++ b/scenes/Catapult.tscn @@ -124,7 +124,7 @@ margin_bottom = 91.0 ]] margin_top = 97.0 margin_right = 784.0 -margin_bottom = 675.0 +margin_bottom = 559.0 tab_align = 0 script = ExtResource( 16 ) @@ -588,6 +588,7 @@ current_dir = "/mnt/data/Godot/Catapult/Project" current_path = "/mnt/data/Godot/Catapult/Project/" [node name="Fonts" type="VBoxContainer" parent="Main/Tabs"] +visible = false anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 7.5 @@ -867,7 +868,6 @@ margin_right = 769.0 margin_bottom = 539.0 [node name="Settings" type="VBoxContainer" parent="Main/Tabs"] -visible = false anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 7.5 @@ -910,19 +910,28 @@ hint_tooltip = "Shorten release names to just the date and number Short names save screen space but may be less clear." text = "Shorten names of experimental releases" -[node name="ShowDebug" type="CheckButton" parent="Main/Tabs/Settings"] +[node name="ShowObsoleteMods" type="CheckButton" parent="Main/Tabs/Settings"] margin_top = 140.0 margin_right = 769.0 margin_bottom = 169.0 +hint_tooltip = "Show mods that come with the game but are marked +as \"obsolete\". Normally, such mods are not considered +as installed." +text = "List obsolete mods in installed" + +[node name="ShowDebug" type="CheckButton" parent="Main/Tabs/Settings"] +margin_top = 175.0 +margin_right = 769.0 +margin_bottom = 204.0 hint_tooltip = "Shows the Debug tab with some testing functions and enables debug messages in the status pane (useful for troubleshooting)." text = "Debug mode" [node name="NumReleases" type="HBoxContainer" parent="Main/Tabs/Settings"] -margin_top = 175.0 +margin_top = 210.0 margin_right = 769.0 -margin_bottom = 204.0 +margin_bottom = 239.0 hint_tooltip = "How many latest releases will be requested from GitHub after starting the launcher, choosing the game or clicking Refresh." @@ -942,9 +951,9 @@ value = 30.0 rounded = true [node name="ScaleOverride" type="HBoxContainer" parent="Main/Tabs/Settings"] -margin_top = 210.0 +margin_top = 245.0 margin_right = 769.0 -margin_bottom = 239.0 +margin_bottom = 274.0 hint_tooltip = "Use this if the automatically inferred UI scale looks too large or too small on your system. (Automatic scale relies on system-reported @@ -979,14 +988,14 @@ editable = false suffix = "%" [node name="HSeparator" type="HSeparator" parent="Main/Tabs/Settings"] -margin_top = 245.0 +margin_top = 280.0 margin_right = 769.0 -margin_bottom = 253.0 +margin_bottom = 288.0 [node name="Migration" type="VBoxContainer" parent="Main/Tabs/Settings"] -margin_top = 259.0 +margin_top = 294.0 margin_right = 769.0 -margin_bottom = 381.0 +margin_bottom = 416.0 hint_tooltip = "Choose which types of game data to carry over into the new game version when updating." @@ -1134,7 +1143,7 @@ size_flags_horizontal = 4 text = "Print a random Tip of the Day" [node name="Log" type="RichTextLabel" parent="Main"] -margin_top = 681.0 +margin_top = 565.0 margin_right = 784.0 margin_bottom = 984.0 focus_mode = 2 @@ -1203,10 +1212,10 @@ __meta__ = { } [connection signal="meta_clicked" from="Main/GameInfo/Description" to="." method="_on_Description_meta_clicked"] +[connection signal="tab_changed" from="Main/Tabs" to="." method="_on_Tabs_tab_changed"] [connection signal="tab_changed" from="Main/Tabs" to="Main/Tabs/Mods" method="_on_Tabs_tab_changed"] -[connection signal="tab_changed" from="Main/Tabs" to="Main/Tabs/Fonts" method="_on_Tabs_tab_changed"] [connection signal="tab_changed" from="Main/Tabs" to="Main/Tabs/Soundpacks" method="_on_Tabs_tab_changed"] -[connection signal="tab_changed" from="Main/Tabs" to="." method="_on_Tabs_tab_changed"] +[connection signal="tab_changed" from="Main/Tabs" to="Main/Tabs/Fonts" method="_on_Tabs_tab_changed"] [connection signal="item_selected" from="Main/Tabs/Game/Builds/BuildsList" to="." method="_on_BuildsList_item_selected"] [connection signal="pressed" from="Main/Tabs/Game/Builds/BtnRefresh" to="." method="_on_BtnRefresh_pressed"] [connection signal="pressed" from="Main/Tabs/Game/BtnInstall" to="." method="_on_BtnInstall_pressed"] @@ -1248,6 +1257,7 @@ __meta__ = { [connection signal="toggled" from="Main/Tabs/Settings/PrintTips" to="Main/Tabs/Settings" method="_on_PrintTips_toggled"] [connection signal="toggled" from="Main/Tabs/Settings/UpdateToSame" to="Main/Tabs/Settings" method="_on_UpdateToSame_toggled"] [connection signal="toggled" from="Main/Tabs/Settings/ShortenNames" to="Main/Tabs/Settings" method="_on_ShortenNames_toggled"] +[connection signal="toggled" from="Main/Tabs/Settings/ShowObsoleteMods" to="Main/Tabs/Settings" method="_on_ShowObsoleteMods_toggled"] [connection signal="toggled" from="Main/Tabs/Settings/ShowDebug" to="Main/Tabs/Settings" method="_on_ShowDebug_toggled"] [connection signal="value_changed" from="Main/Tabs/Settings/NumReleases/sbNumReleases" to="Main/Tabs/Settings" method="_on_sbNumReleases_value_changed"] [connection signal="toggled" from="Main/Tabs/Settings/ScaleOverride/cbScaleOverrideEnable" to="Main/Tabs/Settings" method="_on_cbScaleOverrideEnable_toggled"] diff --git a/scripts/ModManager.gd b/scripts/ModManager.gd index 84f93764..830ecf63 100644 --- a/scripts/ModManager.gd +++ b/scripts/ModManager.gd @@ -34,32 +34,32 @@ onready var _downloader = $"../Downloader" onready var _workdir = OS.get_executable_path().get_base_dir() -var installed: Array = [] setget , _get_installed -var available: Array = [] setget , _get_available +var installed: Dictionary = {} setget , _get_installed +var available: Dictionary = {} setget , _get_available -func _get_installed() -> Array: +func _get_installed() -> Dictionary: - if installed == []: + if len(installed) == 0: refresh_installed() return installed -func _get_available() -> Array: +func _get_available() -> Dictionary: - if available == []: + if len(available) == 0: refresh_available() return available -func parse_mods_dir(mods_dir: String) -> Array: +func parse_mods_dir(mods_dir: String) -> Dictionary: if not Directory.new().dir_exists(mods_dir): - return [] + return {} - var result = [] + var result = {} for subdir in _fshelper.list_dir(mods_dir): var f = File.new() @@ -90,10 +90,10 @@ func parse_mods_dir(mods_dir: String) -> Array: else: info["id"] = info["name"] - result.append({ + result[info["id"]] = { "location": mods_dir + "/" + subdir, "modinfo": info - }) + } break f.close() @@ -113,41 +113,63 @@ func _strip_html_tags(text: String) -> String: s = s.replace(m.get_string(), "") return s - -func _sorting_comparison(a: Dictionary, b: Dictionary) -> bool: - - return (a["modinfo"]["name"].nocasecmp_to(b["modinfo"]["name"]) == -1) +func mod_status(id: String) -> int: + + # Returns mod installed status: + # 0 - not installed; + # 1 - installed; + # 2 - stock mod; + # 3 - stock mod but obsolete; + # 4 - installed with modified ID. + + if id + "__" in installed: + return 4 + elif id in installed: + if installed[id]["is_stock"]: + if installed[id]["is_obsolete"]: + return 3 + else: + return 2 + else: + return 1 + else: + return 0 -func refresh_installed(sort_by_name = true): +func refresh_installed(): var gamedir = _workdir + "/" + _settings.read("game") + "/current" - installed = [] + installed = {} + var non_stock := {} if Directory.new().dir_exists(gamedir + "/mods"): - var non_stock = parse_mods_dir(gamedir + "/mods") - for mod in non_stock: - mod["is_stock"] = false - installed.append_array(non_stock) + non_stock = parse_mods_dir(gamedir + "/mods") + for id in non_stock: + non_stock[id]["is_stock"] = false - var stock = parse_mods_dir(gamedir + "/data/mods") - for mod in stock: - mod["is_stock"] = true - installed.append_array(stock) + var stock := parse_mods_dir(gamedir + "/data/mods") + for id in stock: + stock[id]["is_stock"] = true + if ("obsolete" in stock[id]["modinfo"]) and (stock[id]["modinfo"]["obsolete"] == true): + stock[id]["is_obsolete"] = true + else: + stock[id]["is_obsolete"] = false + + for id in non_stock: + installed[id] = non_stock[id] + installed[id]["is_stock"] = false + installed[id]["is_obsolete"] = false - if sort_by_name: - installed.sort_custom(self, "_sorting_comparison") + for id in stock: + installed[id] = stock[id] -func refresh_available(sort_by_name = true): +func refresh_available(): var mod_repo = _workdir + "/" + _settings.read("game") + "/mod_repo" available = parse_mods_dir(mod_repo) - - if sort_by_name: - available.sort_custom(self, "_sorting_comparison") func _delete_mod(mod_id: String) -> void: @@ -156,13 +178,8 @@ func _delete_mod(mod_id: String) -> void: # Have to introduce an artificial delay, otherwise the engine becomes very # crash-happy when processing large numbers of mods. - var mod = null - for item in installed: - if item["modinfo"]["id"] == mod_id: - mod = item - break - - if mod: + if mod_id in installed: + var mod = installed[mod_id] _fshelper.rm_dir(mod["location"]) yield(_fshelper, "rm_dir_done") emit_signal("status_message", "Deleted %s" % mod["modinfo"]["name"]) @@ -183,7 +200,10 @@ func delete_mods(mod_ids: Array) -> void: emit_signal("mod_deletion_started") for id in mod_ids: - _delete_mod(id) + if mod_status(id) == 4: + _delete_mod(id + "__") + else: + _delete_mod(id) yield(self, "_done_deleting_mod") refresh_installed() @@ -197,15 +217,22 @@ func _install_mod(mod_id: String) -> void: var mods_dir = _workdir + "/" + _settings.read("game") + "/current/mods" - var mod = null - for item in available: - if item["modinfo"]["id"] == mod_id: - mod = item - break - - if mod: + if mod_id in available: + var mod = available[mod_id] + _fshelper.copy_dir(mod["location"], mods_dir) yield(_fshelper, "copy_dir_done") + + if (mod_id in installed) and (installed[mod_id]["is_obsolete"] == true): + emit_signal("status_message", "There is already an obsoleted mod with ID %s. [i]%s[/i] will be installed with modified ID and name to avoid collisions." + % [mod_id, mod["modinfo"]["name"]]) + var modinfo = mod["modinfo"].duplicate() + modinfo["id"] += "__" + modinfo["name"] += "*" + var f = File.new() + f.open(mods_dir.plus_file(mod["location"].get_file()).plus_file("modinfo.json"), File.WRITE) + f.store_string(JSON.print(modinfo, " ")) + emit_signal("status_message", "Installed %s" % mod["modinfo"]["name"]) else: emit_signal("status_message", "Could not find mod with ID \"%s\"" % mod_id, Enums.MSG_ERROR) diff --git a/scripts/ModsUI.gd b/scripts/ModsUI.gd index e742e813..0aa7d0f6 100644 --- a/scripts/ModsUI.gd +++ b/scripts/ModsUI.gd @@ -20,21 +20,22 @@ onready var _lbl_repo = $HBox/Available/Label onready var _dlg_reinstall = $ModReinstallDialog onready var _dlg_del_multiple = $DeleteMultipleDialog -var _gamedir = "" -var _installed_mods_view = [] -var _available_mods_view = [] +var _gamedir := "" +var _installed_mods_view := [] +var _available_mods_view := [] -var _mods_to_delete = [] -var _mods_to_install = [] -var _ids_to_install = [] -var _ids_to_reinstall = [] +var _mods_to_delete := [] +var _mods_to_install := [] +var _ids_to_delete := [] +var _ids_to_install := [] +var _ids_to_reinstall := [] func _populate_list_with_mods(mods_array: Array, list: ItemList) -> void: list.clear() for mod in mods_array: - list.add_item(mod["modinfo"]["name"]) + list.add_item(mod["name"]) if "location" in mod: var tooltip = "Location: " + mod["location"] list.set_item_tooltip(list.get_item_count() - 1, tooltip) @@ -44,16 +45,39 @@ func reload_installed() -> void: var hidden_mods = 0 var show_stock = _settings.read("show_stock_mods") + var show_obsolete = _settings.read("show_obsolete_mods") - if show_stock: - _installed_mods_view = _mods.installed - else: - _installed_mods_view.clear() - for mod in _mods.installed: - if not mod["is_stock"]: - _installed_mods_view.append(mod) - else: + _installed_mods_view.clear() + + for id in _mods.installed: + + var mod = _mods.installed[id] + var show: bool + + var status = _mods.mod_status(id) + if status in [0, 1, 4]: + show = true + elif status == 3: + if show_obsolete: + if show_stock: + show = true + else: + hidden_mods += 1 + elif status == 2: + show = show_stock + if !show: hidden_mods += 1 + + if show: + _installed_mods_view.append({ + "id": id, + "name": mod["modinfo"]["name"], + "location": mod["location"] + }) + if (show_obsolete) and (status == 3): + _installed_mods_view[-1]["name"] += " [obsolete]" + + _installed_mods_view.sort_custom(self, "_sorting_comparison") _btn_delete.disabled = true @@ -64,28 +88,40 @@ func reload_installed() -> void: hidden_str = " (%s hidden)" % hidden_mods _lbl_installed.text = "Installed%s:" % hidden_str - if show_stock: - for i in len(_installed_mods_view): - if _installed_mods_view[i]["is_stock"]: - _installed_list.set_item_custom_fg_color(i, Color(0.5, 0.5, 0.5)) - # TODO: Get color from the theme instead. + for i in len(_installed_mods_view): + var id = _installed_mods_view[i]["id"] + if _mods.installed[id]["is_stock"]: + _installed_list.set_item_custom_fg_color(i, Color(0.5, 0.5, 0.5)) + # TODO: Get color from the theme instead. func reload_available() -> void: var include_installed = _settings.read("show_installed_mods_in_available") var hidden_mods = 0 + + _available_mods_view.clear() - if include_installed: - _available_mods_view = _mods.available - else: - _available_mods_view.clear() - for mod in _mods.available: - if not _is_mod_installed(mod["modinfo"]): - _available_mods_view.append(mod) - else: - hidden_mods += 1 + for id in _mods.available: + var mod = _mods.available[id] + var show: bool + if _mods.mod_status(id) in [0, 3]: + show = true + else: + show = include_installed + + if show: + _available_mods_view.append({ + "id": id, + "name": mod["modinfo"]["name"], + "location": mod["location"] + }) + else: + hidden_mods += 1 + + _available_mods_view.sort_custom(self, "_sorting_comparison") + var hidden_str = "" if hidden_mods > 0: hidden_str = " (%s hidden)" % hidden_mods @@ -94,10 +130,10 @@ func reload_available() -> void: _populate_list_with_mods(_available_mods_view, _available_list) - if include_installed: - for i in len(_available_mods_view): - if _is_mod_installed(_available_mods_view[i]["modinfo"]): - _available_list.set_item_custom_fg_color(i, Color(0.5, 0.5, 0.5)) + for i in len(_available_mods_view): + var id = _available_mods_view[i]["id"] + if _mods.mod_status(id) in [1, 2, 4]: + _available_list.set_item_custom_fg_color(i, Color(0.5, 0.5, 0.5)) if _available_list.get_item_count() == 0: _btn_add_all.disabled = true @@ -106,18 +142,9 @@ func reload_available() -> void: _btn_add_all.disabled = false -func _is_mod_installed(modinfo: Dictionary) -> int: - # Returns 0 if the mod is not installed, 1 if installed, - # and 2 if it is a stock mod. +func _sorting_comparison(a: Dictionary, b: Dictionary) -> bool: - for mod in _mods.installed: - if mod["modinfo"]["id"] == modinfo["id"]: - if mod["is_stock"]: - return 2 - else: - return 1 - - return 0 + return (a["name"].nocasecmp_to(b["name"]) == -1) func _array_to_text_list(array) -> String: @@ -200,12 +227,14 @@ func _on_InstalledList_multi_selected(index: int, selected: bool) -> void: elif len(selection) > 0: active_idx = selection.max() - _lbl_mod_info.bbcode_text = _make_mod_info_string(_installed_mods_view[active_idx]) + var active_id = _installed_mods_view[active_idx]["id"] + _lbl_mod_info.bbcode_text = _make_mod_info_string(_mods.installed[active_id]) _lbl_mod_info.scroll_to_line(0) var only_stock_selected = true for idx in selection: - if not _installed_mods_view[idx]["is_stock"]: + var mod_id = _installed_mods_view[idx]["id"] + if not _mods.installed[mod_id]["is_stock"]: only_stock_selected = false break @@ -224,19 +253,21 @@ func _on_AvailableList_multi_selected(index: int, selected: bool) -> void: elif len(selection) > 0: active_idx = selection.max() - var only_stock_selected = true + var active_id = _available_mods_view[active_idx]["id"] + _lbl_mod_info.bbcode_text = _make_mod_info_string(_mods.available[active_id]) + _lbl_mod_info.scroll_to_line(0) + + var only_non_installable_selected = true for idx in selection: - if not _is_mod_installed(_available_mods_view[idx]["modinfo"]) == 2: - only_stock_selected = false + var mod_id = _available_mods_view[idx]["id"] + if (not mod_id in _mods.installed) or (_mods.installed[mod_id]["is_obsolete"]): + only_non_installable_selected = false break - if (len(selection) == 0) or (only_stock_selected): + if (len(selection) == 0) or (only_non_installable_selected): _btn_add.disabled = true else: _btn_add.disabled = false - - _lbl_mod_info.bbcode_text = _make_mod_info_string(_available_mods_view[active_idx]) - _lbl_mod_info.scroll_to_line(0) func _on_BtnDownloadKenan_pressed() -> void: @@ -253,8 +284,9 @@ func _on_BtnDelete_pressed() -> void: var skipped_mods = 0 for index in selection: - if not _installed_mods_view[index]["is_stock"]: - _mods_to_delete.append(_installed_mods_view[index]["modinfo"]["id"]) + var id = _installed_mods_view[index]["id"] + if not _mods.installed[id]["is_stock"]: + _mods_to_delete.append(id) else: skipped_mods += 1 @@ -290,28 +322,35 @@ func _on_BtnAddSelectedMod_pressed() -> void: var selection = _available_list.get_selected_items() _mods_to_install = [] var num_stock = 0 - + for index in selection: - var mod = _available_mods_view[index] - var status = _is_mod_installed(mod["modinfo"]) + var id = _available_mods_view[index]["id"] + var status = _mods.mod_status(id) if status == 2: num_stock += 1 - _mods_to_install.append({"id": mod["modinfo"]["id"], "installed_status": status}) - + else: + _mods_to_install.append(id) + if num_stock == 1: emit_signal("status_message", "One mod already comes with the game, so it will not be installed.") elif num_stock > 1: emit_signal("status_message", "%s mods already come with the game, so they will not be installed." % num_stock) - - _ids_to_install = [] - _ids_to_reinstall = [] - for item in _mods_to_install: - match item["installed_status"]: - 0: - _ids_to_install.append(item["id"]) - 1: - _ids_to_reinstall.append(item["id"]) - + + _ids_to_install = [] # What to install from scratch. + _ids_to_delete = [] # What to delete before reinstalling. + _ids_to_reinstall = [] # What to install again after deleteion. + for mod_id in _mods_to_install: + + var status = _mods.mod_status(mod_id) + if status == 4: + _ids_to_delete.append(mod_id + "__") + _ids_to_reinstall.append(mod_id) + elif status == 1: + _ids_to_delete.append(mod_id) + _ids_to_reinstall.append(mod_id) + elif status in [0, 3]: + _ids_to_install.append(mod_id) + if len(_ids_to_reinstall) > 0: _dlg_reinstall.open(len(_ids_to_reinstall)) else: @@ -328,7 +367,7 @@ func _on_BtnAddAllMods_pressed() -> void: func _do_mod_installation() -> void: - if len(_ids_to_reinstall) > 0: + if len(_ids_to_delete) > 0: _mods.delete_mods(_ids_to_reinstall) yield(_mods, "mod_deletion_finished") _mods.install_mods(_ids_to_install + _ids_to_reinstall) diff --git a/scripts/SettingsUI.gd b/scripts/SettingsUI.gd index 23d7de96..fddfb225 100644 --- a/scripts/SettingsUI.gd +++ b/scripts/SettingsUI.gd @@ -26,6 +26,7 @@ func _ready() -> void: $PrintTips.pressed = _settings.read("print_tips_of_the_day") $UpdateToSame.pressed = _settings.read("update_to_same_build_allowed") $ShortenNames.pressed = _settings.read("shorten_release_names") + $ShowObsoleteMods.pressed = _settings.read("show_obsolete_mods") $ShowDebug.pressed = _settings.read("debug_mode") $NumReleases/sbNumReleases.value = _settings.read("num_releases_to_request") as int @@ -62,6 +63,11 @@ func _on_ShortenNames_toggled(button_pressed: bool) -> void: _settings.store("shorten_release_names", button_pressed) +func _on_ShowObsoleteMods_toggled(button_pressed: bool) -> void: + + _settings.store("show_obsolete_mods", button_pressed) + + func _on_ShowDebug_toggled(button_pressed: bool) -> void: _settings.store("debug_mode", button_pressed) diff --git a/scripts/settings_manager.gd b/scripts/settings_manager.gd index 844ec87d..4832eb7e 100644 --- a/scripts/settings_manager.gd +++ b/scripts/settings_manager.gd @@ -17,6 +17,7 @@ const _HARDCODED_DEFAULTS = { "ui_scale_override_enabled": false, "show_stock_mods": false, "show_installed_mods_in_available": false, + "show_obsolete_mods": false, "show_stock_sound": false, "font_preview_cyrillic": false, "show_game_desc": true,