From 659e9f3ce852a8f383f434d6f1006df997833588 Mon Sep 17 00:00:00 2001 From: Jeod <47716344+JeodC@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:32:42 -0400 Subject: [PATCH] Add true portable mode (#259) --- README.md | 1 + Scenes/Levels/AchievementMenu.tscn | 10 ++-- Scenes/Prefabs/UI/SettingsMenu.tscn | 12 ++--- Scripts/Classes/Components/ResourceGetter.gd | 4 +- Scripts/Classes/Components/ResourceSetter.gd | 4 +- .../Classes/Components/ResourceSetterNew.gd | 4 +- Scripts/Classes/Editor/LevelEditor.gd | 2 - .../Entities/Objects/CheckpointFlag.gd | 2 +- Scripts/Classes/Entities/Player.gd | 8 ++-- Scripts/Classes/Singletons/AudioManager.gd | 4 +- Scripts/Classes/Singletons/Global.gd | 48 +++++++++++++++++-- Scripts/Classes/Singletons/SaveManager.gd | 21 ++++---- Scripts/Classes/Singletons/SettingsManager.gd | 4 +- Scripts/Classes/Singletons/SpeedrunHandler.gd | 8 ++-- Scripts/Parts/LevelSaver.gd | 3 +- Scripts/Parts/PlayerSprite.gd | 2 +- Scripts/Parts/ResourcePackLoader.gd | 10 ++-- Scripts/Parts/ResourcePackTemplateCreator.gd | 14 +++--- Scripts/UI/CharacterSelect.gd | 29 ++++++----- Scripts/UI/CustomLevelList.gd | 13 ++--- Scripts/UI/CustomLevelMenu.gd | 1 - Scripts/UI/LssLevelInfo.gd | 12 ++--- addons/mod_loader/api/log.gd | 12 ++++- addons/mod_loader/api/profile.gd | 9 +++- addons/mod_loader/internal/cache.gd | 9 +++- addons/mod_loader/internal/path.gd | 19 +++++++- addons/mod_loader/setup/setup_log.gd | 2 +- addons/mod_tool/global/store.gd | 5 +- 28 files changed, 176 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 8ac629a..6b7d428 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A Remake / Celebration of the original 'Super Mario Bros.' games. Features new l - Custom Characters - Add in your own characters to use in game. - Fully Open Source! - Level Share Square Partnered +- Portable mode by creating `portable.txt` in the executable directory ## Downloading diff --git a/Scenes/Levels/AchievementMenu.tscn b/Scenes/Levels/AchievementMenu.tscn index bb3294d..dc5d20f 100644 --- a/Scenes/Levels/AchievementMenu.tscn +++ b/Scenes/Levels/AchievementMenu.tscn @@ -42,7 +42,7 @@ script/source = "extends AchievementProgressCalculator @export var campaign := \"SMB1\" func get_progress() -> int: - var save = JSON.parse_string(FileAccess.open(\"user://saves/\" + campaign + \".sav\", FileAccess.READ).get_as_text()) + var save = JSON.parse_string(FileAccess.open(Global.config_path.path_join(\"saves/\" + campaign + \".sav\"), FileAccess.READ).get_as_text()) var levels_finished := 0 for world in 8: for level in 4: @@ -58,7 +58,7 @@ script/source = "extends AchievementProgressCalculator @export var campaign := \"SMB1\" func get_progress() -> int: - var save = JSON.parse_string(FileAccess.open(\"user://saves/\" + campaign + \".sav\", FileAccess.READ).get_as_text()) + var save = JSON.parse_string(FileAccess.open(Global.config_path.path_join(\"saves/\" + campaign + \".sav\"), FileAccess.READ).get_as_text()) var levels_finished := 0 for i in save[\"ClearedBooLevels\"]: if int(i) > 0: @@ -72,7 +72,7 @@ script/source = "extends AchievementProgressCalculator @export var campaign := \"SMB1\" func get_progress() -> int: - var save = JSON.parse_string(FileAccess.open(\"user://saves/\" + campaign + \".sav\", FileAccess.READ).get_as_text()) + var save = JSON.parse_string(FileAccess.open(Global.config_path.path_join(\"saves/\" + campaign + \".sav\"), FileAccess.READ).get_as_text()) var levels_finished := 0 for i in save[\"ClearedBooLevels\"]: if int(i) >= 5: @@ -103,7 +103,7 @@ func get_progress() -> int: print(SpeedrunHandler.best_level_any_times) if SpeedrunHandler.best_level_any_times[x] <= SpeedrunHandler.LEVEL_GOLD_ANY_TIMES[campaign][x] * SpeedrunHandler.MEDAL_CONVERSIONS[medal_index] and SpeedrunHandler.best_level_any_times[x] > 0: medal_amount += 1 - var save = JSON.parse_string(FileAccess.open(\"user://saves/\" + campaign + \".sav\", FileAccess.READ).get_as_text()) + var save = JSON.parse_string(FileAccess.open(Global.config_path.path_join(\"saves/\" + campaign + \".sav\"), FileAccess.READ).get_as_text()) if save.get(\"BestWarplessTime\", -1) <= SpeedrunHandler.GOLD_WARPLESS_TIMES[campaign] and save.get(\"BestWarplessTime\", -1) > 0: medal_amount += 1 if save.get(\"BestAnyTime\", -1) <= SpeedrunHandler.GOLD_ANY_TIMES[campaign] and save.get(\"BestAnyTime\", -1) > 0: @@ -116,7 +116,7 @@ script/source = "extends AchievementProgressCalculator func get_progress() -> int: var p_amount := 0 - var save = JSON.parse_string(FileAccess.open(\"user://saves/SMBANN.sav\", FileAccess.READ). get_as_text()) + var save = JSON.parse_string(FileAccess.open(Global.config_path.path_join(\"saves/SMBANN.sav\"), FileAccess.READ).get_as_text()) for i in save[\"Ranks\"]: if i == \"P\": p_amount += 1 diff --git a/Scenes/Prefabs/UI/SettingsMenu.tscn b/Scenes/Prefabs/UI/SettingsMenu.tscn index f639270..56429e2 100644 --- a/Scenes/Prefabs/UI/SettingsMenu.tscn +++ b/Scenes/Prefabs/UI/SettingsMenu.tscn @@ -104,31 +104,31 @@ func delete_story(campaign := \"SMB1\") -> void: save_json[i] = SaveManager.SAVE_TEMPLATE[i] if save_json.has(\"Ranks\"): save_json[\"Ranks\"] = \"ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ\" - SaveManager.write_save_to_file(save_json, \"user://saves/\" + campaign + \".sav\") + SaveManager.write_save_to_file(save_json, Global.config_path.path_join(\"saves/\" + campaign + \".sav\")) SaveManager.apply_save(save_json) func delete_challenge(campaign := \"SMB1\") -> void: var save_json = SaveManager.load_save(campaign) for i in [\"ChallengeScores\", \"RedCoins\"]: save_json[i] = SaveManager.SAVE_TEMPLATE[i] - SaveManager.write_save_to_file(save_json, \"user://saves/\" + campaign + \".sav\") + SaveManager.write_save_to_file(save_json, Global.config_path.path_join(\"saves/\" + campaign + \".sav\")) SaveManager.apply_save(save_json) func delete_boo(campaign := \"SMB1\") -> void: var save_json = SaveManager.load_save(campaign) for i in [\"ClearedBooLevels\", \"BooBestTimes\"]: save_json[i] = SaveManager.SAVE_TEMPLATE[i] - SaveManager.write_save_to_file(save_json, \"user://saves/\" + campaign + \".sav\") + SaveManager.write_save_to_file(save_json, Global.config_path.path_join(\"saves/\" + campaign + \".sav\")) SaveManager.apply_save(save_json) func delete_marathon(campaign := \"SMB1\") -> void: var save_json = SaveManager.load_save(campaign) for i in [\"BestAnyTime\", \"BestWarplessTime\"]: save_json[i] = SaveManager.SAVE_TEMPLATE[i] - SaveManager.write_save_to_file(save_json, \"user://saves/\" + campaign + \".sav\") + SaveManager.write_save_to_file(save_json, Global.config_path.path_join(\"saves/\" + campaign + \".sav\")) SaveManager.apply_save(save_json) - for i in DirAccess.get_files_at(\"user://marathon_recordings/\" + campaign + \"/\"): - DirAccess.remove_absolute(\"user://marathon_recordings/\" + campaign + \"/\" + i) + for i in DirAccess.get_files_at(Global.config_path.path_join(\"marathon_recordings/\" + campaign + \"/\")): + DirAccess.remove_absolute(Global.config_path.path_join(\"marathon_recordings/\"+ campaign + \"/\" + i)) for world in 8: for level in 4: SpeedrunHandler.best_level_warpless_times[world][level] = -1 diff --git a/Scripts/Classes/Components/ResourceGetter.gd b/Scripts/Classes/Components/ResourceGetter.gd index cb305f8..4063a62 100644 --- a/Scripts/Classes/Components/ResourceGetter.gd +++ b/Scripts/Classes/Components/ResourceGetter.gd @@ -26,7 +26,7 @@ func get_resource(resource: Resource) -> Resource: if original_resource is Texture: var new_resource = null - if path.contains("user://"): + if path.contains(Global.config_path): new_resource = ImageTexture.create_from_image(Image.load_from_file(path)) else: new_resource = load(path) @@ -64,7 +64,7 @@ func send_to_cache(resource_path := "", resource_to_cache: Resource = null) -> v func get_resource_path(resource_path := "") -> String: for i in Settings.file.visuals.resource_packs: - var test = resource_path.replace("res://Assets/", "user://resource_packs/" + i + "/") + var test = resource_path.replace("res://Assets/", Global.config_path.path_join("resource_packs/" + i + "/")) if FileAccess.file_exists(test): return test return resource_path diff --git a/Scripts/Classes/Components/ResourceSetter.gd b/Scripts/Classes/Components/ResourceSetter.gd index e38a33c..4e02d31 100644 --- a/Scripts/Classes/Components/ResourceSetter.gd +++ b/Scripts/Classes/Components/ResourceSetter.gd @@ -134,13 +134,13 @@ static func get_pure_resource_path(resource_path := "") -> String: if Settings.file.visuals.resource_packs.is_empty() == false: for i in Settings.file.visuals.resource_packs: var new_path = get_override_resource_path(resource_path, i) - new_path = new_path.replace("user://custom_characters/", "user://resource_packs/" + new_path + "/Sprites/Players/CustomCharacters/") + new_path = new_path.replace(Global.config_path.path_join("custom_characters"), Global.config_path.path_join("resource_packs/" + new_path + "/Sprites/Players/CustomCharacters/")) if FileAccess.file_exists(new_path): return new_path return resource_path static func get_override_resource_path(resource_path := "", resource_pack := "") -> String: if resource_pack != "": - return resource_path.replace("res://Assets", "user://resource_packs/" + resource_pack) + return resource_path.replace("res://Assets", Global.config_path.path_join("resource_packs/" + resource_pack)) else: return resource_path diff --git a/Scripts/Classes/Components/ResourceSetterNew.gd b/Scripts/Classes/Components/ResourceSetterNew.gd index 31b5f78..90175c8 100644 --- a/Scripts/Classes/Components/ResourceSetterNew.gd +++ b/Scripts/Classes/Components/ResourceSetterNew.gd @@ -276,8 +276,8 @@ func get_config_file(resource_pack := "") -> void: print("resource pack to use: " + resource_pack) func get_resource_pack_path(res_path := "", resource_pack := "") -> String: - var user_path := res_path.replace("res://Assets", "user://resource_packs/" + resource_pack) - user_path = user_path.replace("user://custom_characters/", "user://resource_packs/" + resource_pack + "/Sprites/Players/CustomCharacters/") + var user_path := res_path.replace("res://Assets", Global.config_path.path_join("resource_packs/" + resource_pack)) + user_path = user_path.replace(Global.config_path.path_join("custom_characters"), Global.config_path.path_join("resource_packs/" + resource_pack + "/Sprites/Players/CustomCharacters/")) if FileAccess.file_exists(user_path): return user_path else: diff --git a/Scripts/Classes/Editor/LevelEditor.gd b/Scripts/Classes/Editor/LevelEditor.gd index e3efb80..cf08cf7 100644 --- a/Scripts/Classes/Editor/LevelEditor.gd +++ b/Scripts/Classes/Editor/LevelEditor.gd @@ -366,8 +366,6 @@ func close_save_menu() -> void: menu_open = false current_state = EditorState.TILE_MENU -const CUSTOM_LEVEL_DIR := "user://custom_levels/" - func handle_tile_cursor() -> void: Input.set_custom_mouse_cursor(null) var snapped_position = ((%TileCursor.get_global_mouse_position() - CURSOR_OFFSET).snapped(Vector2(16, 16))) + CURSOR_OFFSET diff --git a/Scripts/Classes/Entities/Objects/CheckpointFlag.gd b/Scripts/Classes/Entities/Objects/CheckpointFlag.gd index 5d428c7..f151663 100644 --- a/Scripts/Classes/Entities/Objects/CheckpointFlag.gd +++ b/Scripts/Classes/Entities/Objects/CheckpointFlag.gd @@ -18,7 +18,7 @@ func get_character_sprite_path(player_id := 0) -> String: var character = Player.CHARACTERS[int(Global.player_characters[player_id])] var path = "res://Assets/Sprites/Players/" + character + "/CheckpointFlag.json" if int(Global.player_characters[player_id]) > 3: - path = path.replace("res://Assets/Sprites/Players", "user://custom_characters") + path = path.replace("res://Assets/Sprites/Players", Global.config_path.path_join("custom_characters")) return path func activate(player: Player) -> void: diff --git a/Scripts/Classes/Entities/Player.gd b/Scripts/Classes/Entities/Player.gd index 47c246e..98fd404 100644 --- a/Scripts/Classes/Entities/Player.gd +++ b/Scripts/Classes/Entities/Player.gd @@ -194,7 +194,7 @@ func _ready() -> void: func apply_character_physics() -> void: var path = "res://Assets/Sprites/Players/" + character + "/CharacterInfo.json" if int(Global.player_characters[player_id]) > 3: - path = path.replace("res://Assets/Sprites/Players", "user://custom_characters") + path = path.replace("res://Assets/Sprites/Players", Global.config_path.path_join("custom_characters/")) path = ResourceSetter.get_pure_resource_path(path) var json = JSON.parse_string(FileAccess.open(path, FileAccess.READ).get_as_text()) for i in json.physics: @@ -323,7 +323,7 @@ func apply_character_sfx_map() -> void: var custom_character := false if int(Global.player_characters[player_id]) > 3: custom_character = true - path = path.replace("res://Assets/Sprites/Players", "user://custom_characters") + path = path.replace("res://Assets/Sprites/Players", Global.config_path.path_join("custom_characters/")) path = ResourceSetter.get_pure_resource_path(path) var json = JSON.parse_string(FileAccess.open(path, FileAccess.READ).get_as_text()) @@ -333,7 +333,7 @@ func apply_character_sfx_map() -> void: if FileAccess.file_exists(res_path) == false or custom_character: var directory = "res://Assets/Sprites/Players/" + character + "/" + json[i] if int(Global.player_characters[player_id]) > 3: - directory = directory.replace("res://Assets/Sprites/Players", "user://custom_characters") + directory = directory.replace("res://Assets/Sprites/Players", Global.config_path.path_join("custom_characters/")) directory = ResourceSetter.get_pure_resource_path(directory) if FileAccess.file_exists(directory): json[i] = directory @@ -699,7 +699,7 @@ func dispense_stored_item() -> void: func get_character_sprite_path(power_stateto_use := power_state.state_name) -> String: var path = "res://Assets/Sprites/Players/" + character + "/" + power_stateto_use + ".json" if int(Global.player_characters[player_id]) > 3: - path = path.replace("res://Assets/Sprites/Players", "user://custom_characters") + path = path.replace("res://Assets/Sprites/Players", Global.config_path.path_join("custom_characters/")) return path func enter_pipe(pipe: PipeArea, warp_to_level := true) -> void: diff --git a/Scripts/Classes/Singletons/AudioManager.gd b/Scripts/Classes/Singletons/AudioManager.gd index ce2ed7b..a220f2a 100644 --- a/Scripts/Classes/Singletons/AudioManager.gd +++ b/Scripts/Classes/Singletons/AudioManager.gd @@ -113,7 +113,7 @@ func play_sfx(stream_name = "", position := Vector2.ZERO, pitch := 1.0) -> void: var stream = stream_name var is_custom = false if stream_name is String: - is_custom = sfx_library[stream_name].contains("user://custom_characters") + is_custom = sfx_library[stream_name].contains(Global.config_path.path_join("custom_characters")) stream = import_stream(sfx_library[stream_name]) if is_custom == false: player.stream = ResourceSetter.get_resource(stream, player) @@ -236,7 +236,7 @@ func handle_music_override() -> void: func create_stream_from_json(json_path := "") -> AudioStream: if json_path.contains(".json") == false: var path = ResourceSetter.get_pure_resource_path(json_path) - if path.contains("user://"): + if path.contains(Global.config_path): match json_path.get_slice(".", 1): "wav": return AudioStreamWAV.load_from_file(ResourceSetter.get_pure_resource_path(json_path)) diff --git a/Scripts/Classes/Singletons/Global.gd b/Scripts/Classes/Singletons/Global.gd index dc946f5..daebf53 100644 --- a/Scripts/Classes/Singletons/Global.gd +++ b/Scripts/Classes/Singletons/Global.gd @@ -26,11 +26,13 @@ var second_quest := false var extra_worlds_win := false const lang_codes := ["en", "fr", "es", "de", "it", "pt", "pl", "tr", "ru", "jp", "fil", "id", "ga"] +var config_path : String = get_config_path() + var rom_path := "" var rom_assets_exist := false -const ROM_POINTER_PATH := "user://rom_pointer.smb" -const ROM_PATH := "user://baserom.nes" -const ROM_ASSETS_PATH := "user://resource_packs/BaseAssets" +var ROM_POINTER_PATH = config_path.path_join("rom_pointer.smb") +var ROM_PATH = config_path.path_join("baserom.nes") +var ROM_ASSETS_PATH = config_path.path_join("resource_packs/BaseAssets") const ROM_PACK_NAME := "BaseAssets" const ROM_ASSETS_VERSION := 0 @@ -169,8 +171,46 @@ func _ready() -> void: get_server_version() if OS.is_debug_build(): debug_mode = false + setup_config_dirs() check_for_rom() +func setup_config_dirs() -> void: + var dirs = [ + "custom_characters", + "custom_levels", + "logs", + "marathon_recordings", + "resource_packs", + "saves" + ] + + for d in dirs: + var full_path = Global.config_path.path_join(d) + if not DirAccess.dir_exists_absolute(full_path): + DirAccess.make_dir_recursive_absolute(full_path) + +func get_config_path() -> String: + var exe_path := OS.get_executable_path() + var exe_dir := exe_path.get_base_dir() + var portable_flag := exe_dir.path_join("portable.txt") + + # Test that exe dir is writeable, if not fallback to user:// + if FileAccess.file_exists(portable_flag): + var test_file = exe_dir.path_join("test.txt") + var f = FileAccess.open(test_file, FileAccess.WRITE) + if f: + f.close() + var dir = DirAccess.open(exe_dir) + if dir: + dir.remove(test_file.get_file()) + var local_dir = exe_dir.path_join("config") + if not DirAccess.dir_exists_absolute(local_dir): + DirAccess.make_dir_recursive_absolute(local_dir) + return local_dir + else: + push_warning("Portable flag found but exe directory is not writeable. Falling back to user://") + return "user://" + func check_for_rom() -> void: rom_path = "" rom_assets_exist = false @@ -330,7 +370,7 @@ func close_freeze() -> void: $Transition/Freeze.hide() $Transition.hide() -var recording_dir = "user://marathon_recordings/" +var recording_dir = config_path.path_join("marathon_recordings") func update_game_status() -> void: var lives_str := str(Global.lives) diff --git a/Scripts/Classes/Singletons/SaveManager.gd b/Scripts/Classes/Singletons/SaveManager.gd index b35f4ac..4f73681 100644 --- a/Scripts/Classes/Singletons/SaveManager.gd +++ b/Scripts/Classes/Singletons/SaveManager.gd @@ -1,6 +1,6 @@ extends Node -const SAVE_DIR := "user://saves/CAMPAIGN.sav" +var SAVE_DIR : String = Global.config_path.path_join("saves/CAMPAIGN.sav") var visited_levels := "1000000000000000000000000000000010000000000000000000" @@ -71,14 +71,10 @@ func write_save(campaign: String = Global.current_campaign, force := false) -> v if Global.debugged_in and not force: return var save = null - DirAccess.make_dir_recursive_absolute("user://saves") - DirAccess.make_dir_recursive_absolute("user://resource_packs") - DirAccess.make_dir_recursive_absolute("user://custom_characters") - DirAccess.make_dir_recursive_absolute("user://custom_levels") var save_json = {} - var path = "user://saves/" + campaign + ".sav" + var path = Global.config_path.path_join("saves/" + campaign + ".sav") if FileAccess.file_exists(path): - save = FileAccess.open("user://saves/" + campaign + ".sav", FileAccess.READ) + save = FileAccess.open(path, FileAccess.READ) save_json = JSON.parse_string(save.get_as_text()) save.close() else: @@ -150,7 +146,8 @@ func clear_save() -> void: visited_levels[0][0] = "1" var save = SAVE_TEMPLATE.duplicate(true) apply_save(save) - DirAccess.remove_absolute("user://saves/" + Global.current_campaign + ".sav") + var save_path = Global.config_path.path_join("saves" + Global.current_campaign + ".sav") + DirAccess.remove_absolute(save_path) write_save(Global.current_campaign) func clear_array(arr := []) -> void: @@ -172,9 +169,10 @@ func get_level_idx(world_num := 1, level_num := 1) -> int: return ((world_num - 1) * 4) + (level_num - 1) func load_achievements() -> void: - if FileAccess.file_exists("user://achievements.sav") == false: + var path = Global.config_path.path_join("achievements.sav") + if FileAccess.file_exists(path) == false: write_achievements() - var file = FileAccess.open("user://achievements.sav", FileAccess.READ) + var file = FileAccess.open(path, FileAccess.READ) var idx := 0 for i in file.get_as_text(): Global.achievements[idx] = i @@ -183,6 +181,7 @@ func load_achievements() -> void: file.close() func write_achievements() -> void: - var file = FileAccess.open("user://achievements.sav", FileAccess.WRITE) + var path = Global.config_path.path_join("achievements.sav") + var file = FileAccess.open(path, FileAccess.WRITE) file.store_string(Global.achievements) file.close() diff --git a/Scripts/Classes/Singletons/SettingsManager.gd b/Scripts/Classes/Singletons/SettingsManager.gd index b097e74..ba644c8 100644 --- a/Scripts/Classes/Singletons/SettingsManager.gd +++ b/Scripts/Classes/Singletons/SettingsManager.gd @@ -79,10 +79,10 @@ var file := { } } -const SETTINGS_DIR := "user://settings.cfg" +static var SETTINGS_DIR := Global.config_path.path_join("settings.cfg") func _enter_tree() -> void: - DirAccess.make_dir_absolute("user://resource_packs") + DirAccess.make_dir_absolute(Global.config_path.path_join("resource_packs")) load_settings() await get_tree().physics_frame apply_settings() diff --git a/Scripts/Classes/Singletons/SpeedrunHandler.gd b/Scripts/Classes/Singletons/SpeedrunHandler.gd index c2427ed..1ab51c3 100644 --- a/Scripts/Classes/Singletons/SpeedrunHandler.gd +++ b/Scripts/Classes/Singletons/SpeedrunHandler.gd @@ -201,7 +201,7 @@ func gen_time_string(timer_dict := {}) -> String: func save_recording() -> void: var recording := [timer, current_recording, levels, str(["Mario", "Luigi", "Toad", "Toadette"].find(get_tree().get_first_node_in_group("Players").character)), anim_list] - var recording_dir = "user://marathon_recordings/" + Global.current_campaign + var recording_dir = Global.config_path.path_join("marathon_recordings/" + Global.current_campaign) DirAccess.make_dir_recursive_absolute(recording_dir) var file = FileAccess.open(recording_dir + "/" + str(Global.world_num) + "-" + str(Global.level_num) + ("warp" if is_warp_run else "") + ".json", FileAccess.WRITE) file.store_string(compress_recording(JSON.stringify(recording, "", false, true))) @@ -240,7 +240,7 @@ func load_best_marathon() -> void: anim_list = recording[4].duplicate() func load_recording(world_num := 0, level_num := 0, is_warpless := true, campaign := "SMB1") -> Array: - var recording_dir = "user://marathon_recordings/" + campaign + var recording_dir = Global.config_path.path_join("marathon_recordings/" + campaign) var path = recording_dir + "/" + str(world_num) + "-" + str(level_num) + ("" if is_warpless else "warp") + ".json" print(path) if FileAccess.file_exists(path) == false: @@ -257,12 +257,12 @@ func load_best_times(campaign = Global.current_campaign) -> void: best_level_any_times.clear() for world_num in 8: for level_num in 4: - var path = "user://marathon_recordings/" + campaign + "/" + str(world_num + 1) + "-" + str(level_num + 1) + ".json" + var path = Global.config_path.path_join("marathon_recordings/" + campaign + "/" + str(world_num + 1) + "-" + str(level_num + 1) + ".json") if FileAccess.file_exists(path): best_level_warpless_times[world_num][level_num] = load_recording(world_num + 1, level_num + 1, true, campaign)[0] else: best_level_warpless_times[world_num][level_num] = -1 - path = "user://marathon_recordings/" + campaign + "/" + str(world_num + 1) + "-" + str(level_num + 1) +"warp" + ".json" + path = Global.config_path.path_join("marathon_recordings/" + campaign + "/" + str(world_num + 1) + "-" + str(level_num + 1) +"warp" + ".json") if FileAccess.file_exists(path): best_level_any_times[str(world_num + 1) + "-" + str(level_num + 1)] = load_recording(world_num + 1, level_num + 1, false, campaign)[0] check_for_medal_achievement() diff --git a/Scripts/Parts/LevelSaver.gd b/Scripts/Parts/LevelSaver.gd index 7a14ceb..dda2bf0 100644 --- a/Scripts/Parts/LevelSaver.gd +++ b/Scripts/Parts/LevelSaver.gd @@ -34,10 +34,9 @@ func save_level(level_name := "Unnamed Level", level_author := "You", level_desc return level_file func write_file(json := {}, lvl_file_name := "") -> void: - DirAccess.make_dir_absolute(LevelEditor.CUSTOM_LEVEL_DIR) for i in "<>:?!/": lvl_file_name = lvl_file_name.replace(i, "") - var file = FileAccess.open(LevelEditor.CUSTOM_LEVEL_DIR + lvl_file_name, FileAccess.WRITE) + var file = FileAccess.open(Global.config_path.path_join("custom_levels/" + lvl_file_name), FileAccess.WRITE) file.store_string(JSON.stringify(json, "", false)) file.close() print("saved") diff --git a/Scripts/Parts/PlayerSprite.gd b/Scripts/Parts/PlayerSprite.gd index 641045c..0d878a4 100644 --- a/Scripts/Parts/PlayerSprite.gd +++ b/Scripts/Parts/PlayerSprite.gd @@ -24,7 +24,7 @@ func update() -> void: if resource_setter != null: var path = "res://Assets/Sprites/Players/" + character + "/" + Player.POWER_STATES[int(power_state)] + ".json" if Player.CHARACTERS.find(character) > 3: - path = path.replace("res://Assets/Sprites/Players/", "user://custom_characters/") + path = path.replace("res://Assets/Sprites/Players/", Global.config_path.path_join("custom_characters/")) var json = resource_setter.get_resource(load(path)) sprite_frames = json if sprite_frames == null: diff --git a/Scripts/Parts/ResourcePackLoader.gd b/Scripts/Parts/ResourcePackLoader.gd index 916723b..ede06e0 100644 --- a/Scripts/Parts/ResourcePackLoader.gd +++ b/Scripts/Parts/ResourcePackLoader.gd @@ -9,7 +9,7 @@ func _ready() -> void: get_resource_packs() func open_folder() -> void: - OS.shell_show_in_file_manager(ProjectSettings.globalize_path("user://resource_packs"), true) + OS.shell_show_in_file_manager(ProjectSettings.globalize_path(Global.config_path.path_join("resource_packs")), true) func get_resource_packs() -> void: for i in containers: @@ -17,12 +17,12 @@ func get_resource_packs() -> void: i.queue_free() containers = [] resource_packs = [] - for i in DirAccess.get_directories_at("user://resource_packs"): + for i in DirAccess.get_directories_at(Global.config_path.path_join("resource_packs")): resource_packs.append(i) for i in resource_packs: - var pack_info_path = "user://resource_packs/" + i + "/" + "pack_info.json" + var pack_info_path = Global.config_path.path_join("resource_packs/" + i + "/pack_info.json") if FileAccess.file_exists(pack_info_path) and i != Global.ROM_PACK_NAME: - create_container("user://resource_packs/" + i) + create_container(Global.config_path.path_join("resource_packs/" + i)) func create_container(resource_pack := "") -> void: var container = RESOURCE_PACK_CONTAINER.instantiate() @@ -36,7 +36,7 @@ func create_container(resource_pack := "") -> void: container.icon = ImageTexture.create_from_image(image) elif FileAccess.file_exists(resource_pack + "/icon.gif"): container.icon = GifManager.animated_texture_from_file(resource_pack + "/icon.gif") - container.pack_name = resource_pack.replace("user://resource_packs/", "") + container.pack_name = resource_pack.replace(Global.config_path.path_join("resource_packs"), "") $"../ScrollContainer/VBoxContainer".add_child(container) containers.append(container) container.add_to_group("Options") diff --git a/Scripts/Parts/ResourcePackTemplateCreator.gd b/Scripts/Parts/ResourcePackTemplateCreator.gd index fd37530..722cd2e 100644 --- a/Scripts/Parts/ResourcePackTemplateCreator.gd +++ b/Scripts/Parts/ResourcePackTemplateCreator.gd @@ -12,15 +12,15 @@ const base_info_json := { func create_template() -> void: get_directories("res://Assets", files, directories) for i in directories: - DirAccess.make_dir_recursive_absolute(i.replace("res://Assets", "user://resource_packs/new_pack")) + DirAccess.make_dir_recursive_absolute(i.replace("res://Assets", Global.config_path.path_join("resource_packs/new_pack"))) for i in files: var destination = i if destination.contains("res://"): - destination = i.replace("res://Assets", "user://resource_packs/new_pack") + destination = i.replace("res://Assets", Global.config_path.path_join("resource_packs/new_pack")) else: - destination = i.replace("user://resource_packs/BaseAssets", "user://resource_packs/new_pack") + destination = i.replace(Global.config_path.path_join("resource_packs/BaseAssets"), Global.config_path.path_join("resource_packs/new_pack")) print("Copying '" + i + "' to: '" + destination) - if i.contains(".bgm") or i.contains(".json") or i.contains("user://"): + if i.contains(".bgm") or i.contains(".json") or i.contains(Global.config_path): DirAccess.copy_absolute(i, destination) else: var resource = load(i) @@ -33,7 +33,9 @@ func create_template() -> void: file.store_buffer(resource.data) file.close() - var file = FileAccess.open("user://resource_packs/new_pack/pack_info.json", FileAccess.WRITE) + var pack_info_path = Global.config_path.path_join("resource_packs/new_pack/pack_info.json") + DirAccess.make_dir_recursive_absolute(pack_info_path.get_base_dir()) + var file = FileAccess.open(pack_info_path, FileAccess.WRITE) file.store_string(JSON.stringify(base_info_json, "\t")) file.close() print("Done") @@ -51,7 +53,7 @@ func get_files(base_dir := "", files := []) -> void: i = i.replace(".import", "") print(i) var target_path = base_dir + "/" + i - var rom_assets_path = target_path.replace("res://Assets", "user://resource_packs/BaseAssets") + var rom_assets_path = target_path.replace("res://Assets", Global.config_path.path_join("resource_packs/BaseAssets")) if FileAccess.file_exists(rom_assets_path): files.append(rom_assets_path) else: diff --git a/Scripts/UI/CharacterSelect.gd b/Scripts/UI/CharacterSelect.gd index c9acbf2..64040a1 100644 --- a/Scripts/UI/CharacterSelect.gd +++ b/Scripts/UI/CharacterSelect.gd @@ -41,21 +41,24 @@ func get_custom_characters() -> void: idx += 1 print(Player.CHARACTER_NAMES) - DirAccess.make_dir_recursive_absolute("user://custom_characters") - for i in DirAccess.get_directories_at("user://custom_characters"): - if FileAccess.file_exists("user://custom_characters/" + i + "/CharacterInfo.json"): - var char_path = "user://custom_characters/" + i + "/" - var json = JSON.parse_string(FileAccess.open(char_path + "CharacterInfo.json", FileAccess.READ).get_as_text()) + var base_path = Global.config_path.rstrip("/") + var char_dir = base_path.path_join("custom_characters") + + for i in DirAccess.get_directories_at(char_dir): + var char_path = char_dir.path_join(i) + var char_info_path = char_path.path_join("CharacterInfo.json") + if FileAccess.file_exists(char_info_path): + var json = JSON.parse_string(FileAccess.open(char_path.path_join("CharacterInfo.json"), FileAccess.READ).get_as_text()) Player.CHARACTERS.append(i) Player.CHARACTER_NAMES.append(json.name) - if FileAccess.file_exists(char_path + "CharacterColour.json"): - Player.CHARACTER_COLOURS.append(load(char_path + "CharacterColour.json")) - if FileAccess.file_exists(char_path + "LifeIcon.json"): - GameHUD.character_icons.append(load(char_path + "LifeIcon.json")) - if FileAccess.file_exists(char_path + "ColourPalette.json"): - Player.CHARACTER_PALETTES.append(load(char_path + "ColourPalette.json")) - if FileAccess.file_exists(char_path + "SFX.json"): - AudioManager.character_sfx_map[i] = JSON.parse_string(FileAccess.open(char_path + "SFX.json", FileAccess.READ).get_as_text()) + if FileAccess.file_exists(char_path.path_join("CharacterColour.json")): + Player.CHARACTER_COLOURS.append(load(char_path.path_join("CharacterColour.json"))) + if FileAccess.file_exists(char_path.path_join("LifeIcon.json")): + GameHUD.character_icons.append(load(char_path.path_join("LifeIcon.json"))) + if FileAccess.file_exists(char_path.path_join("ColourPalette.json")): + Player.CHARACTER_PALETTES.append(load(char_path.path_join("ColourPalette.json"))) + if FileAccess.file_exists(char_path.path_join("SFX.json")): + AudioManager.character_sfx_map[i] = JSON.parse_string(FileAccess.open(char_path.path_join("SFX.json"), FileAccess.READ).get_as_text()) func open() -> void: get_custom_characters() diff --git a/Scripts/UI/CustomLevelList.gd b/Scripts/UI/CustomLevelList.gd index 77ba07d..6f04a77 100644 --- a/Scripts/UI/CustomLevelList.gd +++ b/Scripts/UI/CustomLevelList.gd @@ -4,7 +4,6 @@ signal level_selected(container: CustomLevelContainer) const CUSTOM_LEVEL_CONTAINER = preload("uid://dt20tjug8m6oh") -const CUSTOM_LEVEL_PATH := "user://custom_levels/" const base64_charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" signal closed @@ -25,7 +24,8 @@ func open(refresh_list := true) -> void: set_process(true) func open_folder() -> void: - OS.shell_show_in_file_manager(ProjectSettings.globalize_path(CUSTOM_LEVEL_PATH)) + var custom_level_path = Global.config_path.path_join("custom_levels") + OS.shell_show_in_file_manager(ProjectSettings.globalize_path(custom_level_path)) func _process(_delta: float) -> void: if Input.is_action_just_pressed("ui_back"): @@ -41,11 +41,12 @@ func refresh() -> void: if i is CustomLevelContainer: i.queue_free() containers.clear() - get_levels("user://custom_levels") - get_levels("user://custom_levels/downloaded") + get_levels(Global.config_path.path_join("custom_levels")) + get_levels(Global.config_path.path_join("custom_levels/downloaded")) -func get_levels(path := "user://custom_levels") -> void: - DirAccess.make_dir_recursive_absolute(path) +func get_levels(path : String = "") -> void: + if path == "": + path = Global.config_path.path_join("custom_levels") var idx := 0 for i in DirAccess.get_files_at(path): if i.contains(".lvl") == false: diff --git a/Scripts/UI/CustomLevelMenu.gd b/Scripts/UI/CustomLevelMenu.gd index 50d7545..eabbb0c 100644 --- a/Scripts/UI/CustomLevelMenu.gd +++ b/Scripts/UI/CustomLevelMenu.gd @@ -6,7 +6,6 @@ static var current_level_file := "" static var has_entered := false var selected_lvl_idx := 0 -const CUSTOM_LEVEL_PATH := "user://custom_levels/" const base64_charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" static var page_number_save := -1 static var last_played_container = null diff --git a/Scripts/UI/LssLevelInfo.gd b/Scripts/UI/LssLevelInfo.gd index f32a345..5e32ee2 100644 --- a/Scripts/UI/LssLevelInfo.gd +++ b/Scripts/UI/LssLevelInfo.gd @@ -21,7 +21,7 @@ func _ready() -> void: func open(container: OnlineLevelContainer) -> void: container_to_play = container.duplicate() - has_downloaded = FileAccess.file_exists("user://custom_levels/downloaded/" + container.level_id + ".lvl") or saved_stuff.is_empty() == false + has_downloaded = FileAccess.file_exists(Global.config_path.path_join("custom_levels/downloaded/" + container.level_id + ".lvl")) or saved_stuff.is_empty() == false show() level_thumbnail = container.level_thumbnail %Download.text = "DOWNLOAD" @@ -66,7 +66,7 @@ func close() -> void: set_process(false) func download_level() -> void: - DirAccess.make_dir_recursive_absolute("user://custom_levels/downloaded") + DirAccess.make_dir_recursive_absolute(Global.config_path.path_join("custom_levels/downloaded")) var url = "https://levelsharesquare.com/api/levels/" + level_id + "/code" print(url) $DownloadLevel.request(url, [], HTTPClient.METHOD_GET) @@ -85,7 +85,7 @@ func on_request_completed(result: int, response_code: int, headers: PackedString func level_downloaded(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void: var string = body.get_string_from_utf8() var json = JSON.parse_string(string) - var file = FileAccess.open("user://custom_levels/downloaded/" + level_id + ".lvl", FileAccess.WRITE) + var file = FileAccess.open(Global.config_path.path_join("custom_levels/downloaded/" + level_id + ".lvl"), FileAccess.WRITE) var data = null if json.levelData.data is Array: data = get_json_from_bytes(json.levelData.data) @@ -101,11 +101,11 @@ func level_downloaded(result: int, response_code: int, headers: PackedStringArra func save_thumbnail() -> void: if OnlineLevelContainer.cached_thumbnails.has(level_id): var thumbnail = OnlineLevelContainer.cached_thumbnails.get(level_id) - DirAccess.make_dir_recursive_absolute("user://custom_levels/downloaded/thumbnails") - thumbnail.get_image().save_png("user://custom_levels/downloaded/thumbnails/"+ level_id + ".png") + DirAccess.make_dir_recursive_absolute(Global.config_path.path_join("custom_levels/downloaded/thumbnails")) + thumbnail.get_image().save_png(Global.config_path.path_join("custom_levels/downloaded/thumbnails/" + level_id + ".png")) func play_level() -> void: - var file_path := "user://custom_levels/downloaded/" + level_id + ".lvl" + var file_path := Global.config_path.path_join("custom_levels/downloaded/" + level_id + ".lvl") var file = JSON.parse_string(FileAccess.open(file_path, FileAccess.READ).get_as_text()) LevelEditor.level_file = file set_process(false) diff --git a/addons/mod_loader/api/log.gd b/addons/mod_loader/api/log.gd index 6e4761c..4f3c59f 100644 --- a/addons/mod_loader/api/log.gd +++ b/addons/mod_loader/api/log.gd @@ -6,10 +6,20 @@ extends Object # Path to the latest log file. -const MOD_LOG_PATH := "user://logs/modloader.log" +static var MOD_LOG_PATH = get_log_path() const _LOG_NAME := "ModLoader:Log" +static func get_log_path() -> String: + var exe_dir = OS.get_executable_path().get_base_dir() + var portable_flag = exe_dir.path_join("portable.txt") + if FileAccess.file_exists(portable_flag): + var log_dir = exe_dir.path_join("config/logs") + DirAccess.make_dir_recursive_absolute(log_dir) + return log_dir.path_join("modloader.log") + else: + return "user://logs/modloader.log" + ## Denotes the severity of a log entry enum VERBOSITY_LEVEL { ERROR, ## For errors and fatal errors diff --git a/addons/mod_loader/api/profile.gd b/addons/mod_loader/api/profile.gd index e831354..c977977 100644 --- a/addons/mod_loader/api/profile.gd +++ b/addons/mod_loader/api/profile.gd @@ -7,8 +7,15 @@ extends Object const LOG_NAME := "ModLoader:UserProfile" # The path where the Mod User Profiles data is stored. -const FILE_PATH_USER_PROFILES := "user://mod_user_profiles.json" +static var FILE_PATH_USER_PROFILES := get_profiles_path() +static func get_profiles_path() -> String: + var exe_dir = OS.get_executable_path().get_base_dir() + var portable_flag = exe_dir.path_join("portable.txt") + if FileAccess.file_exists(portable_flag): + return exe_dir.path_join("config/mod_user_profiles.json") + else: + return "user://mod_user_profiles.json" # API profile functions # ============================================================================= diff --git a/addons/mod_loader/internal/cache.gd b/addons/mod_loader/internal/cache.gd index cc424e6..cae14f8 100644 --- a/addons/mod_loader/internal/cache.gd +++ b/addons/mod_loader/internal/cache.gd @@ -4,9 +4,16 @@ extends RefCounted # This Class provides methods for caching data. -const CACHE_FILE_PATH = "user://mod_loader_cache.json" +static var CACHE_FILE_PATH = get_cache_path() const LOG_NAME = "ModLoader:Cache" +static func get_cache_path() -> String: + var exe_dir = OS.get_executable_path().get_base_dir() + var portable_flag = exe_dir.path_join("portable.txt") + if FileAccess.file_exists(portable_flag): + return exe_dir.path_join("config/mod_loader_cache.json") + else: + return "user://mod_loader_cache.json" # ModLoaderStore is passed as parameter so the cache data can be loaded on ModLoaderStore._init() static func init_cache(_ModLoaderStore) -> void: diff --git a/addons/mod_loader/internal/path.gd b/addons/mod_loader/internal/path.gd index 9c67928..b692d8f 100644 --- a/addons/mod_loader/internal/path.gd +++ b/addons/mod_loader/internal/path.gd @@ -6,9 +6,24 @@ extends RefCounted # Currently all of the included functions are internal and should only be used by the mod loader itself. const LOG_NAME := "ModLoader:Path" -const MOD_CONFIG_DIR_PATH := "user://mod_configs" -const MOD_CONFIG_DIR_PATH_OLD := "user://configs" +static var MOD_CONFIG_DIR_PATH : String = get_modconfigs_path() +static var MOD_CONFIG_DIR_PATH_OLD : String = get_modconfigs_path_old() +static func get_modconfigs_path() -> String: + var exe_dir = OS.get_executable_path().get_base_dir() + var portable_flag = exe_dir.path_join("portable.txt") + if FileAccess.file_exists(portable_flag): + return exe_dir.path_join("config/mod_configs") + else: + return "user://configs" + +static func get_modconfigs_path_old() -> String: + var exe_dir = OS.get_executable_path().get_base_dir() + var portable_flag = exe_dir.path_join("portable.txt") + if FileAccess.file_exists(portable_flag): + return exe_dir.path_join("config/configs") + else: + return "user://configs" # Get the path to a local folder. Primarily used to get the (packed) mods # folder, ie "res://mods" or the OS's equivalent, as well as the configs path diff --git a/addons/mod_loader/setup/setup_log.gd b/addons/mod_loader/setup/setup_log.gd index 600a8bf..83744f9 100644 --- a/addons/mod_loader/setup/setup_log.gd +++ b/addons/mod_loader/setup/setup_log.gd @@ -3,7 +3,7 @@ class_name ModLoaderSetupLog # Slimed down version of ModLoaderLog for the ModLoader Self Setup -const MOD_LOG_PATH := "user://logs/modloader.log" +static var MOD_LOG_PATH := Global.config_path.path_join("logs/modloader.log") enum VERBOSITY_LEVEL { ERROR, diff --git a/addons/mod_tool/global/store.gd b/addons/mod_tool/global/store.gd index 8541b3d..33fcc81 100644 --- a/addons/mod_tool/global/store.gd +++ b/addons/mod_tool/global/store.gd @@ -5,8 +5,7 @@ extends Node # Global store for all Data the ModTool requires. - -const PATH_SAVE_FILE := "user://mod-tool-plugin-save.json" +static var PATH_SAVE_FILE := Global.config_path.path_join("mod-tool-plugin-save.json") const PATH_TEMPLATES_DIR := "res://addons/mod_tool/templates/" var editor_plugin: EditorPlugin @@ -96,7 +95,7 @@ func init(store: Dictionary) -> void: func update_paths(new_name_mod_dir: String) -> void: path_mod_dir = "res://mods-unpacked/" + new_name_mod_dir - path_temp_dir = "user://temp/" + new_name_mod_dir + path_temp_dir = Global.config_path.path_join("temp/" + new_name_mod_dir) path_global_temp_dir = ProjectSettings.globalize_path(path_temp_dir) path_manifest = path_mod_dir + "/manifest.json" path_global_final_zip = "%s/%s.zip" % [path_global_export_dir, name_mod_dir]