diff options
Diffstat (limited to 'client/system')
-rw-r--r-- | client/system/cli.gd | 112 | ||||
-rw-r--r-- | client/system/cli.gd.uid | 1 | ||||
-rw-r--r-- | client/system/disable_wrong_joypads.gd | 61 | ||||
-rw-r--r-- | client/system/disable_wrong_joypads.gd.uid | 1 | ||||
-rw-r--r-- | client/system/profile.gd | 91 | ||||
-rw-r--r-- | client/system/profile.gd.uid | 1 | ||||
-rw-r--r-- | client/system/server_list.gd | 113 | ||||
-rw-r--r-- | client/system/server_list.gd.uid | 1 | ||||
-rw-r--r-- | client/system/settings.gd | 200 | ||||
-rw-r--r-- | client/system/settings.gd.uid | 1 | ||||
-rw-r--r-- | client/system/translation_manager.gd | 57 | ||||
-rw-r--r-- | client/system/translation_manager.gd.uid | 1 |
12 files changed, 640 insertions, 0 deletions
diff --git a/client/system/cli.gd b/client/system/cli.gd new file mode 100644 index 00000000..b6ba8eef --- /dev/null +++ b/client/system/cli.gd @@ -0,0 +1,112 @@ +# Hurry Curry! - a game about cooking +# Copyright (C) 2025 Hurry Curry! contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +extends Node +class_name Cli + +enum Mode { FLAG, OPTION, MULTI_OPTION, POSITIONAL } +class Option: + var short #: String? + var long: String + var mode: Mode + var help: String + func _init(s, l: String, m: Mode, h: String): + short = s; long = l; mode = m; help = h + +static var OPTIONS := [ + Option.new("h", "help", Mode.FLAG, "Show help"), + Option.new("s", "setting", Mode.MULTI_OPTION, "Per-launch setting override"), + Option.new(null, "connect_address", Mode.POSITIONAL, "Connect to a server directly without menu interaction") +] + +static var opts = {} #: Dictionary[String, Variant] + +static func init() -> bool: + if not parse(): return false + if opts.has("help"): + print_help() + return false + return true + +static func print_help(): + print("OPTIONS:\n") + for opt in OPTIONS: + var line = "" + if opt.mode == Mode.POSITIONAL: + line += "<" + opt.long.to_upper() + ">" + else: + if opt.short: line += "-" + opt.short + ", " + line += "--" + opt.long + if opt.mode == Mode.OPTION or opt.mode == Mode.MULTI_OPTION: + line += " <VALUE>" + while line.length() < 25: line += " " + line += " " + opt.help + print(line) + +static func parse() -> bool: + var args := OS.get_cmdline_user_args() + while not args.is_empty(): + var arg := args[0] + args.remove_at(0) + if arg.begins_with("--"): + var long = arg.trim_prefix("--") + var opt_index = OPTIONS.find_custom(func(x): return x.long == long) + if opt_index == -1: + push_error("unknown long option \"%s\"" % long) + return false + if not _parse_opt(args, OPTIONS[opt_index]): return false + elif arg.begins_with("-"): + for short in arg.trim_prefix("-"): + var opt_index = OPTIONS.find_custom(func(x): return x.short == short) + if opt_index == -1: + push_error("unknown short option \"%s\"" % short) + return false + if not _parse_opt(args, OPTIONS[opt_index]): return false + else: + var opt_index = OPTIONS.find_custom(func(x): return x.mode == Mode.POSITIONAL) + if opt_index == -1: + push_error("no positional arguments") + return false + var opt = OPTIONS[opt_index] + opts[opt.long] = arg + + print("Parsed options: ", opts) + return true + +static func _parse_opt(args: Array[String], opt: Option) -> bool: + match opt.mode: + Mode.FLAG: + opts[opt.long] = true + return true + Mode.OPTION: + if args.is_empty(): + push_error("missing option value") + return false + opts[opt.long] = args[0] + args.remove_at(0) + return true + Mode.MULTI_OPTION: + if args.is_empty(): + push_error("missing option value") + return false + if not opts.has(opt.long): opts[opt.long] = [] + opts[opt.long].push_back(args[0]) + args.remove_at(0) + return true + Mode.POSITIONAL: + push_error("positional arg doesnt need flag") + return false + push_error("unreachable") + return false diff --git a/client/system/cli.gd.uid b/client/system/cli.gd.uid new file mode 100644 index 00000000..031e72be --- /dev/null +++ b/client/system/cli.gd.uid @@ -0,0 +1 @@ +uid://b26r6mw82umnc diff --git a/client/system/disable_wrong_joypads.gd b/client/system/disable_wrong_joypads.gd new file mode 100644 index 00000000..029768b3 --- /dev/null +++ b/client/system/disable_wrong_joypads.gd @@ -0,0 +1,61 @@ +# Hurry Curry! - a game about cooking +# Copyright (C) 2025 Hurry Curry! contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +extends Node + +func _ready() -> void: + Input.joy_connection_changed.connect(joy_check) + +static var banned_words: PackedStringArray = [ + "touchpad", "trackpad", "clickpad", "mouse", "pen", "finger", "led", + "Synaptics", +] + +static func joy_check(device: int, connected: bool) -> void: + if not connected: + return + var device_name: String = Input.get_joy_name(device) + if not is_banned(device_name): + return + var guid: String = Input.get_joy_guid(device) + var mapping: String = guid + ',' + device_name.replace(',', '') + Input.add_joy_mapping(mapping, true) + for axis: JoyAxis in range(JOY_AXIS_SDL_MAX) as Array[JoyAxis]: + var event: InputEventJoypadMotion = InputEventJoypadMotion.new() + event.device = device + event.axis = axis + Input.parse_input_event(event) + for button_index: JoyButton in range(JOY_BUTTON_SDL_MAX) as Array[JoyButton]: + var event: InputEventJoypadButton = InputEventJoypadButton.new() + event.device = device + event.button_index = button_index + Input.parse_input_event(event) + prints('Ignoring joypad device:', mapping) + +static func is_banned(device_name: String) -> bool: + for word: String in banned_words: + var i: int = device_name.findn(word) + if i < 0: + continue + if i > 0 and not is_ascii_non_letter(device_name[i - 1]): + continue + var j: int = i + word.length() + if j < device_name.length() and not is_ascii_non_letter(device_name[j]): + continue + return true + return false + +static func is_ascii_non_letter(c: String) -> bool: + return c.unicode_at(0) < 128 and not (c >= 'a' and c <= 'z' or c >= 'A' and c <= 'Z') diff --git a/client/system/disable_wrong_joypads.gd.uid b/client/system/disable_wrong_joypads.gd.uid new file mode 100644 index 00000000..dbf5f234 --- /dev/null +++ b/client/system/disable_wrong_joypads.gd.uid @@ -0,0 +1 @@ +uid://chhri171ljnss diff --git a/client/system/profile.gd b/client/system/profile.gd new file mode 100644 index 00000000..438e8e66 --- /dev/null +++ b/client/system/profile.gd @@ -0,0 +1,91 @@ +# Hurry Curry! - a game about cooking +# Copyright (C) 2025 Hurry Curry! contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +extends Node +class_name Profile + +static var default_profile := { + "username": "", + "character_style": { + "color": 0, + "headwear": 0, + "hairstyle": 0 + }, + "last_server_url": "", + "tutorial_ingredients_played": [], + "registry_asked": false, + "hints": { + "has_moved": false, + "has_boosted": false, + "has_interacted": false, + "has_rotated": false, + "has_reset": false, + "has_zoomed": false, + "has_seen_performance": false, + "has_seen_join_while_running": false + } +} + +# profile is stored in a Dictionary[String, Any] +static var values: Dictionary +static var loaded_path: String + +static func load(path: String): + # TOCTOU here. Godot docs says its fine. + if not FileAccess.file_exists(path): + print("Skip profile load") + return default_profile + var f = FileAccess.open(path, FileAccess.READ) + + values = f.get_var(true) + if values != null and values is Dictionary: + G.add_missing_keys(values, default_profile) + loaded_path = path + +static func save(): + var f = FileAccess.open(loaded_path, FileAccess.WRITE) + var to_save = values.duplicate(true) + f.store_var(to_save, true) + +static func read(key: String): + if values.has(key): + return values[key] + else: + push_error("Tried to access profile setting \"%s\", which does not exist (missing key)" % key) + return null + +static func write(key: String, value): + if !values.has(key): + push_error("Tried to set profile setting \"%s\", which does not yet exist (missing key)" % key) + return + if values[key] != value: + values[key] = value + +static func set_hint(key: String, value: bool): + if !values["hints"].has(key): + push_error("Tried to set hint \"%s\", which does not yet exist (missing key)" % key) + if values["hints"][key] != value: + if value: + Settings.write("gameplay.hints_started", true) + Settings.save() + values["hints"][key] = value + save() # TODO avoid this call when bulk-unsetting hints + +static func get_hint(key: String): + if values["hints"].has(key): + return values["hints"][key] + else: + push_error("Tried to access hint \"%s\", which does not exist (missing key)" % key) + return null diff --git a/client/system/profile.gd.uid b/client/system/profile.gd.uid new file mode 100644 index 00000000..5cd63b28 --- /dev/null +++ b/client/system/profile.gd.uid @@ -0,0 +1 @@ +uid://1hlvx8wuxl2d diff --git a/client/system/server_list.gd b/client/system/server_list.gd new file mode 100644 index 00000000..77b5ee0c --- /dev/null +++ b/client/system/server_list.gd @@ -0,0 +1,113 @@ +# Hurry Curry! - a game about cooking +# Copyright (C) 2025 Hurry Curry! contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +extends Node + +signal update_loading(status: bool) +signal update_server_list(list: Array) + +enum Registry { + MDNS = 0, + GLOBAL = 1, +} + +const MDNS_URL: String = "http://127.0.0.1:27033/v1/list" +const HEADERS: Array[String] = [ + "Accept: application/json", + "User-Agent: Hurry Curry! %s" % Global.VERSION + ] + +var current_list: Array[Array] = [[], []] +var loading := false +var mdns := HTTPRequest.new() +var reg := HTTPRequest.new() + +# Fallback to http, since there seems to be a problem related to mbed tls in Godot. +# See: https://github.com/godotengine/godot/issues/96103 +var using_http_fallback := false + +var mdns_timer := Timer.new() +var reg_timer := Timer.new() +# after 30 minutes we stop fetching results to reduce server load +var timeout := Timer.new() + +func _ready() -> void: + add_child(mdns) + add_child(reg) + mdns_timer.wait_time = 5. + mdns_timer.one_shot = false + mdns_timer.timeout.connect(fetch_server_list.bind(Registry.MDNS)) + add_child(mdns_timer) + reg_timer.wait_time = 60. + reg_timer.one_shot = false + reg_timer.timeout.connect(fetch_server_list.bind(Registry.GLOBAL)) + add_child(reg_timer) + timeout.wait_time = 60. * 30. + timeout.timeout.connect(func(): + stop() + ) + add_child(timeout) + mdns.request_completed.connect(_on_request_completed.bind(Registry.MDNS)) + reg.request_completed.connect(_on_request_completed.bind(Registry.GLOBAL)) + +func fetch_server_list(registry: Registry) -> void: + match registry: + Registry.MDNS: + if Settings.read("online.use_discover"): + match Discover.state: + Service.State.STOPPED: Discover.start() + Service.State.RUNNING: mdns.request(MDNS_URL, HEADERS) + Registry.GLOBAL: + if Settings.read("online.use_registry"): + var url: String = Settings.read("online.registry_url") + url = url.replace("https:", "http:") if using_http_fallback else url + reg.request(url + "/v1/list", HEADERS) + + loading = true + update_loading.emit(true) + +func _on_request_completed(result: int, _response_code: int, _headers: PackedStringArray, body: PackedByteArray, registry: Registry): + loading = false + update_loading.emit(false) + if result != 0: + push_warning("Fetching server list failed with code %d." % result) + if !using_http_fallback: + print("Retrying with http...") + using_http_fallback = true + fetch_server_list(Registry.GLOBAL) + return + var json = JSON.parse_string(body.get_string_from_utf8()) + if json == null: + push_error("Server list response invalid") + return + current_list[registry] = json + update_server_list.emit(current_list) + +func start() -> void: + timeout.stop() + timeout.start() + mdns_timer.start() + fetch_server_list(Registry.MDNS) + reg_timer.start() + fetch_server_list(Registry.GLOBAL) + +func stop() -> void: + timeout.stop() + mdns_timer.stop() + reg_timer.stop() + +func one_shot() -> void: + start() + stop() diff --git a/client/system/server_list.gd.uid b/client/system/server_list.gd.uid new file mode 100644 index 00000000..e5479756 --- /dev/null +++ b/client/system/server_list.gd.uid @@ -0,0 +1 @@ +uid://b5rgw37pfh22b diff --git a/client/system/settings.gd b/client/system/settings.gd new file mode 100644 index 00000000..1cacfce9 --- /dev/null +++ b/client/system/settings.gd @@ -0,0 +1,200 @@ +# Hurry Curry! - a game about cooking +# Copyright (C) 2025 Hurry Curry! contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +extends Node +class_name Settings + +static func get_root(): + return SettingsRoot.new([ + SettingsCategory.new("gameplay", [ + ToggleSetting.new("usernames", true), + ToggleSetting.new("latch_boost", true), + ToggleSetting.new("vibration", true), + ToggleSetting.new("invert_camera", false), + ToggleSetting.new("interpolate_camera_rotation", false), + ButtonSetting.new("setup_completed", false, launch_setup), + ToggleSetting.new("tutorial_disabled", false), + ToggleSetting.new("hints_started", false), + ToggleSetting.new("accessible_movement", false), + ToggleSetting.new("first_person", false), + DropdownSetting.new("interact_target", "dirsnap", ["dir", "dirsnap"]), + ]), + SettingsCategory.new("graphics", [ + PresetRow.new("preset", { + "low": {"ui_blur": true, "aa": "disabled", "ssao": false, "taa": false, "shadows": false, "glow": false, "grass_amount": 0, "lq_trees": true}, + "medium": {"ui_blur": true, "aa": "fx", "ssao": false, "taa": false, "shadows": true, "glow": false, "grass_amount": 16, "lq_trees": false}, + "high": {"ui_blur": true, "aa": "ms2x", "ssao": true, "taa": false, "shadows": true, "glow": true, "grass_amount": 24, "lq_trees": false} + }), + DropdownSetting.new("fullscreen", "keep", ["keep", "always", "never"]), + DropdownSetting.new("aa", "ms2x" if Global.on_high_end() else "disabled", ["disabled", "fx", "ms2x", "ms4x"]), + ToggleSetting.new("ssao", true if Global.on_high_end() else false), + ToggleSetting.new("taa", false), + DropdownSetting.new("gi", "disabled", ["disabled", "sdfgi", "voxelgi"]), + ToggleSetting.new("shadows", true if Global.on_high_end() else false), + ToggleSetting.new("glow", true if Global.on_high_end() else false), + RangeSetting.new("grass_amount", 24 if Global.on_high_end() else 0, 0, 32, false), + ToggleSetting.new("lq_trees", false if Global.on_high_end() else true), + ToggleSetting.new("debug_info", false), + ToggleSetting.new("ui_blur", true) + ]), + SettingsCategory.new("audio", [ + RangeSetting.new("master_volume", 0, -30, 0), + RangeSetting.new("music_volume", 0, -30, 0), + RangeSetting.new("sfx_volume", 0, -30, 0), + ]), + SettingsCategory.new("ui", [ + DropdownSetting.new("touch_controls", "automatic", ["automatic", "enabled", "disabled"]), + DropdownSetting.new("language", "system", Global.language_list()), + ToggleSetting.new("hide_overlays", false), + DropdownSetting.new("scale_mode", "resize", ["resize", "disabled"]), + RangeSetting.new("scale_factor", 1. if not Global.on_mobile() else 1.5, 0.5, 1.5, 3), + ]), + SettingsCategory.new("input", InputManager.settings()), + SettingsCategory.new("server", [ + PathSetting.new("binary_path", "", FileDialog.FileMode.FILE_MODE_OPEN_FILE), + PathSetting.new("editor_binary_path", "", FileDialog.FileMode.FILE_MODE_OPEN_DIR), + PathSetting.new("data_path", "", FileDialog.FileMode.FILE_MODE_OPEN_DIR), + TextSetting.new("name", "A Hurry Curry! Server"), + ToggleSetting.new("allow_external_connections", true), + ToggleSetting.new("enable_ipv6", true), + NumberSetting.new("bind_port", 27032, 1, 65535), + ToggleSetting.new("upnp", false), + ToggleSetting.new("mdns", true), + ToggleSetting.new("register", false), + ]), + SettingsCategory.new("online", [ + ToggleSetting.new("use_registry", false), + TextSetting.new("registry_url", "https://hurrycurry-registry.metamuffin.org/"), + ToggleSetting.new("use_discover", true), + PathSetting.new("discover_binary", "", FileDialog.FileMode.FILE_MODE_OPEN_FILE), + ]) + ]) + +static var tree: GameSetting +static var values: Dictionary = {} +static var loaded_path: String + +static func read(key: String): + if !values.has(key): + push_error("Tried to access setting \"%s\", which does not exist (missing key)" % key) + return null + return values[key] + +static func write_unchecked(key: String, value): + value = value.duplicate(true) if value is Array else value + if key in values and typeof(values[key]) == typeof(value) and not value is Array and values[key] == value: return + values[key] = value + trigger_hook(key, value) + +static func write(key: String, value): + if !values.has(key): + push_error("Tried to set setting \"%s\", which does not yet exist (missing key)" % key) + return + else: write_unchecked(key, value) + +static func load(path: String): + tree = get_root() + loaded_path = path + var changed = {} + if FileAccess.file_exists(path): + var f = FileAccess.open(path, FileAccess.READ) + changed = JSON.parse_string(f.get_as_text()) + tree.load(changed) + +static func save(): + var changed = {} + tree.save(changed) + var f = FileAccess.open(loaded_path, FileAccess.WRITE) + f.store_string(JSON.stringify(changed)) + +static func trigger_hook(key: String, value): + if change_hooks_display.get(key) != null: change_hooks_display.get(key).callv([value] if value != null else []) + if change_hooks_apply.get(key) != null: change_hooks_apply.get(key).callv([value] if value != null else []) + if key.find(".") != -1: trigger_hook(key.rsplit(".", false, 1)[0], null) + +static func hook_changed(key: String, display: bool, callable: Callable): + if display: change_hooks_display[key] = callable + else: change_hooks_apply[key] = callable + +static func hook_changed_init(key: String, display: bool, callable: Callable): + hook_changed(key, display, callable) + callable.call(read(key)) + +static func get_category_dict(prefix: String): + var map = {} + for k in values.keys(): + var kn = k.trim_prefix(prefix + ".") + if kn == k: continue + map[kn] = read(k) + return map + +static func launch_setup(): + Global.focused_menu.submenu("res://gui/menus/setup/setup.tscn") + +static var change_hooks_display = {} +static var change_hooks_apply = { + "input": h_input, + "gameplay.hints_started": h_hints_started, + "graphics.aa": h_aa, + "graphics.taa": h_taa, + "graphics.fullscreen": h_fullscreen, + "ui.scale_mode": h_scale_mode, + "ui.scale_factor": h_scale_factor, + "ui.language": h_language, + "audio.master_volume": h_volume_master, + "audio.music_volume": h_volume_music, + "audio.sfx_volume": h_volume_sfx, +} + +static func h_aa(_mode): + Global.configure_viewport_aa(Global.get_viewport()) + +static func h_taa(enabled): + Global.get_viewport().use_taa = enabled + +static func h_scale_mode(mode: String): + var root = Global.get_tree().root + if OS.has_feature("movie"): + root.content_scale_mode = Window.CONTENT_SCALE_MODE_VIEWPORT + root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_KEEP + else: match mode: + "resize": root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS + "disabled": root.content_scale_mode = Window.CONTENT_SCALE_MODE_DISABLED + +static func h_scale_factor(value: float): + Global.get_tree().root.content_scale_factor = value + +static func h_volume_master(value: float): Sound.set_volume(0, value) +static func h_volume_music(value: float): Sound.set_volume(1, value) +static func h_volume_sfx(value: float): Sound.set_volume(2, value) + +static func h_language(language: String): + if language == "system": language = OS.get_locale_language() + TranslationServer.set_locale(language) + +static func h_fullscreen(mode: String): + match mode: + "keep": pass + "always": DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN) + "never": if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN: + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + +static func h_input(): + InputManager.apply_input_map(Settings.get_category_dict("input")) + +static func h_hints_started(started: bool): + if not started: + for k in Profile.values["hints"].keys(): + Profile.set_hint(k, false) diff --git a/client/system/settings.gd.uid b/client/system/settings.gd.uid new file mode 100644 index 00000000..ca85e233 --- /dev/null +++ b/client/system/settings.gd.uid @@ -0,0 +1 @@ +uid://dkingwif2bsek diff --git a/client/system/translation_manager.gd b/client/system/translation_manager.gd new file mode 100644 index 00000000..9aa374b2 --- /dev/null +++ b/client/system/translation_manager.gd @@ -0,0 +1,57 @@ +# Hurry Curry! - a game about cooking +# Copyright (C) 2025 Hurry Curry! contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License only. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +extends Node + +const LOCALE_PATH := "res://locale/" +const LOCALE_BOOK_PATH := "res://locale_book/" +const NATIVE_LANGUAGE_NAMES_FILE_NAME := "native_language_names.ini1" + +func _init() -> void: + # Use english as fallback + var native_language_names := get_ini_dict(NATIVE_LANGUAGE_NAMES_FILE_NAME, LOCALE_PATH) + var fallback_strings := get_ini_dict("en.ini", LOCALE_PATH) + var fallback_strings_book := get_ini_dict("en.ini", LOCALE_BOOK_PATH) + + for file_name in DirAccess.get_files_at(LOCALE_PATH): + if !file_name.ends_with(".ini"): + continue + + var translation := Translation.new() + translation.locale = file_name.trim_suffix(".ini") + var trans_strings := get_ini_dict(file_name, LOCALE_PATH) + var trans_book_strings := get_ini_dict(file_name, LOCALE_BOOK_PATH) + + for k in fallback_strings.keys(): + translation.add_message(k, trans_strings[k] if trans_strings.has(k) else fallback_strings[k]) + for k in fallback_strings_book.keys(): + translation.add_message(k, trans_book_strings[k] if trans_book_strings.has(k) else fallback_strings_book[k]) + for k in native_language_names.keys(): + translation.add_message("c.settings.ui.language.%s" % k, native_language_names[k]) + + TranslationServer.add_translation(translation) + + +func get_ini_dict(file_name: String, locale_path: String) -> Dictionary: # Dictionary[String, String] + var dict := {} + var lines := FileAccess.get_file_as_string(locale_path + file_name).split("\n", false) + if lines.size() > 0: + lines.remove_at(0) + + for line in lines: + var halves := line.split("=", true, 1) + dict[halves[0].strip_edges()] = halves[1].strip_edges().replace("%n", "\n") + + return dict diff --git a/client/system/translation_manager.gd.uid b/client/system/translation_manager.gd.uid new file mode 100644 index 00000000..ea5fd7da --- /dev/null +++ b/client/system/translation_manager.gd.uid @@ -0,0 +1 @@ +uid://cn5c1hvcxe736 |