Improve collection graph controls, version rules and help

This commit is contained in:
cthangover committed 2026-10-26 20:22:58 +03:00
1 parent b6c61ea3df
commit 7ae31f180c
24 files changed
+2201 -311

No files matched your search

+83 -10
View File
@@ -219,16 +219,63 @@ mod_manager/collection_delete_confirm = The collection «{0}» will be deleted.
mod_manager/collection_delete_done = Collection deleted. Its mods stay in the system. mod_manager/collection_delete_done = Collection deleted. Its mods stay in the system.
mod_manager/collection_nested_readonly = This block is nested deeper — open it on its own to change it. mod_manager/collection_nested_readonly = This block is nested deeper — open it on its own to change it.
mod_manager/collection_forked = Block “{0}” copied for this game — other games stay unchanged. mod_manager/collection_forked = Block “{0}” copied for this game — other games stay unchanged.
mod_manager/help_open = What do the controls on this screen mean? mod_manager/help_open = Contextual hints (F1)
mod_manager/help_close = Close help mod_manager/help_active = Hover over a control · Left click — pin the hint · Esc — exit
mod_manager/help_mods_title = Mods mod_manager/collection_order_legend = Dependency → the mod that needs it. Card positions and group boundaries do not define load order.
mod_manager/help_mods_body = This is the list of mods installed on this device. Use the search box in the header to find a mod by its name, and the refresh button to re-read the catalog — freshly installed mods appear at once. Click a mod card to open its page with the description, the version and the actions. The switch on each card turns the mod on or off for the games that use it; a disabled mod keeps its files but is not loaded. Scenes and content of a disabled mod disappear from running games until you enable it again, while your saves are kept either way. mod_manager/enable_tooltip = Load this mod in games that use it.
mod_manager/help_mod_page_title = Mod page mod_manager/disable_tooltip = Do not load this mod. Its files and saves are kept.
mod_manager/help_mod_page_body = This page shows one mod in detail: the description, the author, the version and the list of its dependencies. From here you can enable or disable the mod for the current games, and open its scenes if the mod provides any. The links under the description jump to the collections this mod belongs to. The back button and Escape return you to the mods list; the on/off state of the mod is saved with your game and survives restarts, so there is no need to set it every time. If two mods ship the same file, the page tells you which one wins. mod_manager/hint_mods_tab_title = Mods
mod_manager/help_collections_title = Collections mod_manager/hint_mods_tab_body = The catalog and installed mods. Manage mod files, versions and whether games load them here.
mod_manager/help_collections_body = The collections tab lists game collections: ready-made bundles of mods that play like separate games. Every card shows the cover, the title and the mods the collection includes. Click a card to open the collection, and use Configure to open the scheme editor for it. Installing a collection fetches the mods it needs; removing it never deletes your saves — the game's data sleeps and returns together with the collection. mod_manager/hint_collections_tab_title = Collections
mod_manager/help_scheme_title = Scheme editor mod_manager/hint_collections_tab_body = Ready-made mod sets that run as separate games. Their contents can be changed in the scheme editor.
mod_manager/help_scheme_body = The scheme editor changes what a collection includes: which mods take part in the game and in which order they load. Turn the checkboxes on and off to add or remove mods, and drag rows to change the load order — when two mods ship the same file, the one loaded later wins. The editor validates your scheme before saving and warns about missing dependencies. Changes apply on the next launch of the game; your saves are kept, and the data of absent mods sleeps until the mod returns. mod_manager/hint_collection_search_title = Search collections
mod_manager/hint_collection_search_body = Filters cards by title, description, identifier and tags.
mod_manager/hint_refresh_title = Refresh
mod_manager/hint_refresh_body = Reloads the catalog and compares it with installed mods. Your current settings are preserved.
mod_manager/hint_mod_search_title = Search mods
mod_manager/hint_mod_search_body = Filters the list by mod title, author or identifier.
mod_manager/hint_mod_sort_title = Sorting
mod_manager/hint_mod_sort_body = Changes only the card order: by name, author or installation state.
mod_manager/hint_filters_reset_title = Reset filters
mod_manager/hint_filters_reset_body = Restores the default search, states, categories and grouping.
mod_manager/hint_group_categories_title = Group by category
mod_manager/hint_group_categories_body = Groups cards into sections without changing the mods themselves.
mod_manager/hint_mod_card_title = Mod card
mod_manager/hint_mod_card_body = Click an empty part of the card to open its description, dependencies and every available action.
mod_manager/hint_mod_version_title = Mod version
mod_manager/hint_mod_version_body = Chooses the version to install or update. Labels mark the latest and currently installed versions.
mod_manager/hint_mod_action_title = Mod action
mod_manager/hint_mod_action_body = The action follows the mod state: install, update, change version, reinstall or delete.
mod_manager/hint_mod_enabled_title = Mod loading
mod_manager/hint_mod_enabled_body = A disabled mod stays on disk but is not loaded by games. Saves and mod data are preserved.
mod_manager/hint_mod_delete_title = Delete mod
mod_manager/hint_mod_delete_body = Removes the mod files from this device. Collections and game saves are not deleted.
mod_manager/hint_mod_collections_title = Used in collections
mod_manager/hint_mod_collections_body = Opens the collections that include this mod.
mod_manager/hint_collection_new_title = New collection
mod_manager/hint_collection_new_body = Creates an empty game scheme. Add mods and mechanics in the editor.
mod_manager/hint_collection_apply_title = Apply contents
mod_manager/hint_collection_apply_body = Installs missing mods and enables required ones. You can review the plan first.
mod_manager/hint_collection_edit_title = Configure collection
mod_manager/hint_collection_edit_body = Opens the game contents: mods, dependencies, load order and the change plan.
mod_manager/hint_collection_delete_title = Delete collection
mod_manager/hint_collection_delete_body = Deletes only the collection. Installed mods and game saves are kept.
mod_manager/hint_collection_add_mechanic_title = Add mechanic
mod_manager/hint_collection_add_mechanic_body = Adds a group for related mods and their settings to the scheme.
mod_manager/hint_collection_game_title = Edited collection
mod_manager/hint_collection_game_body = Switches the editor to another game without closing this screen.
mod_manager/hint_collection_problems_title = Scheme problems
mod_manager/hint_collection_problems_body = Lists missing mods, incompatible versions and conflicts that prevent the game from running.
mod_manager/hint_collection_plan_title = Change plan
mod_manager/hint_collection_plan_body = Shows which mods applying the scheme will install and enable.
mod_manager/hint_collection_json_title = Scheme as JSON
mod_manager/hint_collection_json_body = Opens the scheme as text for inspection and precise editing.
mod_manager/hint_collection_reset_title = Discard changes
mod_manager/hint_collection_reset_body = Restores the last saved scheme. Mods that are already installed are not removed.
mod_manager/hint_collection_links_title = Scheme links
mod_manager/hint_collection_links_body = Shows or hides dependency lines between groups and mods.
mod_manager/hint_load_order_title = Load order
mod_manager/hint_load_order_body = Mods load from top to bottom. When two mods replace the same resource, the lower mod wins.
mod_manager/sort_tooltip = Sort the list: by name, author or install state. mod_manager/sort_tooltip = Sort the list: by name, author or install state.
mod_manager/filters_reset_tooltip = Return every filter and the search box to their defaults. mod_manager/filters_reset_tooltip = Return every filter and the search box to their defaults.
mod_manager/collection_json_tooltip = Open the scheme as raw JSON: view, edit and apply it back. mod_manager/collection_json_tooltip = Open the scheme as raw JSON: view, edit and apply it back.
@@ -236,3 +283,29 @@ mod_manager/collection_reset_tooltip = Discard all unsaved scheme changes.
mod_manager/collection_game_tooltip = Switch the scheme being edited to another collection. mod_manager/collection_game_tooltip = Switch the scheme being edited to another collection.
mod_manager/collection_plan_tooltip = Open the plan: what apply will install and enable. mod_manager/collection_plan_tooltip = Open the plan: what apply will install and enable.
mod_manager/collection_problems_tooltip = Open the problem list: missing mods, version gaps and conflicts. mod_manager/collection_problems_tooltip = Open the problem list: missing mods, version gaps and conflicts.
# Graphite collection editor
mod_manager/collection_links_selected = Selected links
mod_manager/collection_links_all = All links
mod_manager/collection_links_none = No links
mod_manager/collection_canvas_search = Find in scheme…
mod_manager/collection_canvas_fit = Fit width
mod_manager/collection_canvas_arrange = Arrange
mod_manager/collection_canvas_focus = Show selection
mod_manager/collection_version_caption = Version for this collection
mod_manager/collection_version_hint = This is the game requirement, not the installed mod version.
mod_manager/collection_version_edit = Change requirement
mod_manager/collection_version_any = Any
mod_manager/collection_version_minimum = At least
mod_manager/collection_version_exact = Exactly
mod_manager/collection_version_branch = Branch
mod_manager/collection_version_maximum = At most
mod_manager/collection_version_greater = Above
mod_manager/collection_version_less = Below
mod_manager/collection_version_branch_hint = Branch: ^1 accepts 1.x; ^1.2 accepts 1.2.x.
mod_manager/collection_version_invalid = Invalid version. Enter a value such as 2.0.0.
mod_manager/collection_role_hint = A group describes the mod’s purpose in this game; another game may assign it a different role.
mod_manager/collection_used_by = Required by: {0}
mod_manager/collection_other_role = {0} → {1}
mod_manager/hint_collection_dependencies_title = Dependencies and group purpose
mod_manager/hint_collection_dependencies_body = Dependency → the mod that needs it. Card positions and group boundaries do not define load order. A group describes the mod’s purpose in this game; another game may assign it a different role.
+83 -10
View File
@@ -219,16 +219,63 @@ mod_manager/collection_delete_confirm = Se eliminará la colección «{0}». Sus
mod_manager/collection_delete_done = Colección eliminada. Sus mods siguen en el sistema. mod_manager/collection_delete_done = Colección eliminada. Sus mods siguen en el sistema.
mod_manager/collection_nested_readonly = Este bloque está anidado más adentro: ábrelo por separado para cambiarlo. mod_manager/collection_nested_readonly = Este bloque está anidado más adentro: ábrelo por separado para cambiarlo.
mod_manager/collection_forked = Bloque «{0}» copiado para este juego: los demás juegos no cambian. mod_manager/collection_forked = Bloque «{0}» copiado para este juego: los demás juegos no cambian.
mod_manager/help_open = Ayuda: ¿qué hace cada control de esta pantalla? mod_manager/help_open = Sugerencias contextuales (F1)
mod_manager/help_close = Cerrar la ayuda mod_manager/help_active = Pasa el cursor por un control · Clic izquierdo: fijar · Esc: salir
mod_manager/help_mods_title = Mods mod_manager/collection_order_legend = Dependencia → mod que la necesita. La posición y los grupos no definen el orden de carga.
mod_manager/help_mods_body = Esta es la lista de mods instalados en este dispositivo. Usa el cuadro de búsqueda de la cabecera para encontrar un mod por su nombre y el botón de actualizar para releer el catálogo: los mods recién instalados aparecen enseguida. Haz clic en la tarjeta de un mod para abrir su página con la descripción, la versión y las acciones. El interruptor de cada tarjeta activa o desactiva el mod en los juegos que lo usan; un mod desactivado conserva sus archivos pero no se carga. Las escenas y el contenido de un mod desactivado desaparecen de los juegos en marcha hasta que lo vuelvas a activar; tus partidas guardadas se conservan igualmente. mod_manager/enable_tooltip = Cargar este mod en los juegos que lo utilizan.
mod_manager/help_mod_page_title = Página del mod mod_manager/disable_tooltip = No cargar este mod. Sus archivos y partidas se conservan.
mod_manager/help_mod_page_body = Esta página muestra un mod en detalle: la descripción, el autor, la versión y la lista de sus dependencias. Desde aquí puedes activar o desactivar el mod en los juegos actuales y abrir sus escenas si el mod las ofrece. Los enlaces bajo la descripción llevan a las colecciones a las que pertenece el mod. El botón de atrás y Escape te devuelven a la lista de mods; el estado del mod se guarda con tu partida y sobrevive a los reinicios, así que no hace falta configurarlo cada vez. Si dos mods traen el mismo archivo, la página te dice cuál gana. mod_manager/hint_mods_tab_title = Mods
mod_manager/help_collections_title = Colecciones mod_manager/hint_mods_tab_body = El catálogo y los mods instalados. Gestiona sus archivos, versiones y carga en los juegos.
mod_manager/help_collections_body = La pestaña de colecciones muestra las colecciones de juegos: paquetes listos de mods que se juegan como juegos aparte. Cada tarjeta muestra la portada, el título y los mods que incluye la colección. Haz clic en una tarjeta para abrir la colección y usa Configurar para abrir el editor de esquema. Instalar una colección descarga los mods que necesita; quitarla nunca borra tus partidas guardadas: los datos del juego descansan y vuelven cuando la colección regresa. mod_manager/hint_collections_tab_title = Colecciones
mod_manager/help_scheme_title = Editor de esquema mod_manager/hint_collections_tab_body = Conjuntos de mods que funcionan como juegos independientes. Su contenido se edita en el editor de esquema.
mod_manager/help_scheme_body = El editor de esquema cambia lo que incluye una colección: qué mods participan en el juego y en qué orden se cargan. Marca y desmarca las casillas para añadir o quitar mods y arrastra las filas para cambiar el orden de carga: cuando dos mods traen el mismo archivo, gana el que se carga después. Antes de guardar, el editor valida tu esquema y avisa de las dependencias que faltan. Los cambios se aplican en el próximo inicio del juego; tus partidas se conservan y los datos de los mods ausentes descansan hasta que el mod vuelva. mod_manager/hint_collection_search_title = Buscar colecciones
mod_manager/hint_collection_search_body = Filtra las tarjetas por título, descripción, identificador y etiquetas.
mod_manager/hint_refresh_title = Actualizar
mod_manager/hint_refresh_body = Recarga el catálogo y lo compara con los mods instalados. Conserva la configuración actual.
mod_manager/hint_mod_search_title = Buscar mods
mod_manager/hint_mod_search_body = Filtra la lista por título, autor o identificador del mod.
mod_manager/hint_mod_sort_title = Ordenación
mod_manager/hint_mod_sort_body = Solo cambia el orden de las tarjetas: por nombre, autor o estado de instalación.
mod_manager/hint_filters_reset_title = Restablecer filtros
mod_manager/hint_filters_reset_body = Restaura la búsqueda, los estados, las categorías y la agrupación predeterminados.
mod_manager/hint_group_categories_title = Agrupar por categorías
mod_manager/hint_group_categories_body = Agrupa las tarjetas en secciones sin cambiar los mods.
mod_manager/hint_mod_card_title = Tarjeta del mod
mod_manager/hint_mod_card_body = Haz clic en una zona libre para abrir la descripción, las dependencias y todas las acciones.
mod_manager/hint_mod_version_title = Versión del mod
mod_manager/hint_mod_version_body = Elige la versión que instalar o actualizar. Las marcas indican la más reciente y la instalada.
mod_manager/hint_mod_action_title = Acción del mod
mod_manager/hint_mod_action_body = La acción depende del estado: instalar, actualizar, cambiar de versión, reinstalar o eliminar.
mod_manager/hint_mod_enabled_title = Carga del mod
mod_manager/hint_mod_enabled_body = Un mod desactivado sigue en el disco, pero los juegos no lo cargan. Las partidas y los datos se conservan.
mod_manager/hint_mod_delete_title = Eliminar mod
mod_manager/hint_mod_delete_body = Elimina los archivos del mod del dispositivo. Las colecciones y partidas se conservan.
mod_manager/hint_mod_collections_title = Uso en colecciones
mod_manager/hint_mod_collections_body = Abre las colecciones que incluyen este mod.
mod_manager/hint_collection_new_title = Nueva colección
mod_manager/hint_collection_new_body = Crea un esquema de juego vacío al que añadir mods y mecánicas.
mod_manager/hint_collection_apply_title = Aplicar contenido
mod_manager/hint_collection_apply_body = Instala los mods que faltan y activa los necesarios. Antes puedes revisar el plan.
mod_manager/hint_collection_edit_title = Configurar colección
mod_manager/hint_collection_edit_body = Abre el contenido del juego: mods, dependencias, orden de carga y plan de cambios.
mod_manager/hint_collection_delete_title = Eliminar colección
mod_manager/hint_collection_delete_body = Elimina solo la colección. Los mods instalados y las partidas se conservan.
mod_manager/hint_collection_add_mechanic_title = Añadir mecánica
mod_manager/hint_collection_add_mechanic_body = Añade al esquema un grupo para mods relacionados y sus ajustes.
mod_manager/hint_collection_game_title = Colección en edición
mod_manager/hint_collection_game_body = Cambia el editor a otro juego sin cerrar esta pantalla.
mod_manager/hint_collection_problems_title = Problemas del esquema
mod_manager/hint_collection_problems_body = Muestra mods ausentes, versiones incompatibles y conflictos que impiden iniciar el juego.
mod_manager/hint_collection_plan_title = Plan de cambios
mod_manager/hint_collection_plan_body = Muestra qué mods se instalarán y activarán al aplicar el esquema.
mod_manager/hint_collection_json_title = Esquema en JSON
mod_manager/hint_collection_json_body = Abre el esquema como texto para revisarlo y editarlo con precisión.
mod_manager/hint_collection_reset_title = Descartar cambios
mod_manager/hint_collection_reset_body = Restaura el último esquema guardado. No desinstala los mods existentes.
mod_manager/hint_collection_links_title = Enlaces del esquema
mod_manager/hint_collection_links_body = Muestra u oculta las líneas de dependencia entre grupos y mods.
mod_manager/hint_load_order_title = Orden de carga
mod_manager/hint_load_order_body = Los mods se cargan de arriba abajo. Si dos sustituyen el mismo recurso, prevalece el inferior.
mod_manager/sort_tooltip = Ordenar la lista: por nombre, autor o estado de instalación. mod_manager/sort_tooltip = Ordenar la lista: por nombre, autor o estado de instalación.
mod_manager/filters_reset_tooltip = Devolver todos los filtros y el cuadro de búsqueda a sus valores predeterminados. mod_manager/filters_reset_tooltip = Devolver todos los filtros y el cuadro de búsqueda a sus valores predeterminados.
mod_manager/collection_json_tooltip = Abrir el esquema como JSON sin formato: verlo, editarlo y aplicarlo de vuelta. mod_manager/collection_json_tooltip = Abrir el esquema como JSON sin formato: verlo, editarlo y aplicarlo de vuelta.
@@ -236,3 +283,29 @@ mod_manager/collection_reset_tooltip = Descartar todos los cambios sin guardar d
mod_manager/collection_game_tooltip = Cambiar el esquema que se está editando a otra colección. mod_manager/collection_game_tooltip = Cambiar el esquema que se está editando a otra colección.
mod_manager/collection_plan_tooltip = Abrir el plan: qué instalará y activará la aplicación. mod_manager/collection_plan_tooltip = Abrir el plan: qué instalará y activará la aplicación.
mod_manager/collection_problems_tooltip = Abrir la lista de problemas: mods que faltan, versiones y conflictos. mod_manager/collection_problems_tooltip = Abrir la lista de problemas: mods que faltan, versiones y conflictos.
# Graphite collection editor
mod_manager/collection_links_selected = Enlaces seleccionados
mod_manager/collection_links_all = Todos los enlaces
mod_manager/collection_links_none = Sin enlaces
mod_manager/collection_canvas_search = Buscar en el esquema…
mod_manager/collection_canvas_fit = Ajustar ancho
mod_manager/collection_canvas_arrange = Organizar
mod_manager/collection_canvas_focus = Ver selección
mod_manager/collection_version_caption = Versión para esta colección
mod_manager/collection_version_hint = Es el requisito del juego, no la versión instalada del mod.
mod_manager/collection_version_edit = Cambiar requisito
mod_manager/collection_version_any = Cualquiera
mod_manager/collection_version_minimum = Como mínimo
mod_manager/collection_version_exact = Exacta
mod_manager/collection_version_branch = Rama
mod_manager/collection_version_maximum = Como máximo
mod_manager/collection_version_greater = Mayor que
mod_manager/collection_version_less = Menor que
mod_manager/collection_version_branch_hint = Rama: ^1 acepta 1.x; ^1.2 acepta 1.2.x.
mod_manager/collection_version_invalid = Versión no válida. Introduce, por ejemplo, 2.0.0.
mod_manager/collection_role_hint = El grupo describe la función del mod en este juego; otro juego puede asignarle otra función.
mod_manager/collection_used_by = Requerido por: {0}
mod_manager/collection_other_role = {0} → {1}
mod_manager/hint_collection_dependencies_title = Dependencias y función de los grupos
mod_manager/hint_collection_dependencies_body = Dependencia → mod que la necesita. La posición y los grupos no definen el orden de carga. El grupo describe la función del mod en este juego; otro juego puede asignarle otra función.
+83 -10
View File
@@ -219,16 +219,63 @@ mod_manager/collection_delete_confirm = コレクション「{0}」を削除し
mod_manager/collection_delete_done = コレクションを削除しました。MOD はシステムに残っています。 mod_manager/collection_delete_done = コレクションを削除しました。MOD はシステムに残っています。
mod_manager/collection_nested_readonly = このブロックはさらに深く入れ子になっています——変更するには単独で開いてください。 mod_manager/collection_nested_readonly = このブロックはさらに深く入れ子になっています——変更するには単独で開いてください。
mod_manager/collection_forked = ブロック「{0}」をこのゲーム用にコピーしました——他のゲームは変わりません。 mod_manager/collection_forked = ブロック「{0}」をこのゲーム用にコピーしました——他のゲームは変わりません。
mod_manager/help_open = この画面の操作の説明を表示 mod_manager/help_open = コンテキストヒント(F1)
mod_manager/help_close = ヘルプを閉じる mod_manager/help_active = コントロールにポインターを合わせる · 左クリックで固定 · Escで終了
mod_manager/help_mods_title = MOD mod_manager/collection_order_legend = 依存先 → それを必要とするMOD。カードの位置やグループの境界は読み込み順を決めません。
mod_manager/help_mods_body = これは端末にインストールされたModの一覧です。ヘッダーの検索ボックスで名前からModを探し、更新ボタンでカタログを読み込み直します。新しくインストールしたModは、更新するとすぐ一覧に現れます。Modのカードをクリックすると、説明・バージョン・実行できる操作を示すページが開きます。各カードのスイッチで、そのModを使うゲームに対して有効と無効を切り替えられます。無効にしたModはファイルをそのまま残しますが、ゲームには読み込まれません。無効化されたModのシーンや内容は、もう一度有効にするまで実行中のゲームから消えます。セーブデータはどちらの場合も保たれます。 mod_manager/enable_tooltip = このMODを使用するゲームで読み込みます。
mod_manager/help_mod_page_title = Modのページ mod_manager/disable_tooltip = このMODを読み込みません。ファイルとセーブは残ります。
mod_manager/help_mod_page_body = このページでは一つのModを詳しく表示します。説明、作者、バージョン、そして依存しているModの一覧です。ここから現在のゲームに対してModを有効または無効にでき、Modがシーンを提供していれば、そのシーンを直接開くこともできます。説明の下にあるリンクは、このModが含まれるコレクションへ移動します。戻るボタンとEscapeキーでModの一覧に戻ります。Modの有効・無効の状態はゲームと一緒に保存され、再起動の後も変わらないので、毎回設定し直す必要はありません。同じファイルを含むModが二つあれば、どちらが優先されるかもページに表示されます。 mod_manager/hint_mods_tab_title = MOD
mod_manager/help_collections_title = コレクション mod_manager/hint_mods_tab_body = カタログとインストール済みMODです。ファイル、バージョン、ゲームでの読み込みを管理します。
mod_manager/help_collections_body = コレクションタブには、ゲームのコレクションが並びます。コレクションとは、ひとつのゲームとして遊べるModのまとまりです。各カードにはカバー画像、タイトル、そしてコレクションに含まれるModが表示されます。カードをクリックしてコレクションを開き、「構成」ボタンで構成エディターを開けます。コレクションをインストールすると、必要なModがまとめて取り込まれます。コレクションを削除してもセーブデータは決して消えません。ゲームのデータは休止状態で待機し、コレクションを戻せば一緒に戻ります。 mod_manager/hint_collections_tab_title = コレクション
mod_manager/help_scheme_title = 構成エディター mod_manager/hint_collections_tab_body = 独立したゲームとして実行できるMODのセットです。内容は構成エディターで変更できます。
mod_manager/help_scheme_body = 構成エディターは、コレクションの中身を変更します。どのModをゲームに参加させ、どの順序で読み込むかをここで決めます。チェックボックスの切り替えでModを追加・削除し、行をドラッグして読み込み順を変更します。同じファイルを含むMod同士は、後から読み込まれる方が優先されます。保存の前に構成が検証され、不足している依存関係があれば警告が出ます。変更は次にゲームを起動したときに適用されます。セーブデータはそのまま残り、存在しないModのデータは、そのModが戻ってくるまで眠ります。 mod_manager/hint_collection_search_title = コレクションを検索
mod_manager/hint_collection_search_body = タイトル、説明、ID、タグでカードを絞り込みます。
mod_manager/hint_refresh_title = 更新
mod_manager/hint_refresh_body = カタログを再読み込みし、インストール済みMODと照合します。現在の設定は保持されます。
mod_manager/hint_mod_search_title = MODを検索
mod_manager/hint_mod_search_body = MODのタイトル、作者、IDで一覧を絞り込みます。
mod_manager/hint_mod_sort_title = 並べ替え
mod_manager/hint_mod_sort_body = カードの順序だけを名前、作者、インストール状態で並べ替えます。
mod_manager/hint_filters_reset_title = フィルターをリセット
mod_manager/hint_filters_reset_body = 検索、状態、カテゴリー、グループ化を初期設定に戻します。
mod_manager/hint_group_categories_title = カテゴリーでグループ化
mod_manager/hint_group_categories_body = MODを変更せず、カードをカテゴリー別にまとめます。
mod_manager/hint_mod_card_title = MODカード
mod_manager/hint_mod_card_body = カードの空いている部分をクリックすると、説明、依存関係、全操作を開きます。
mod_manager/hint_mod_version_title = MODのバージョン
mod_manager/hint_mod_version_body = インストールや更新に使うバージョンを選びます。最新版とインストール済み版が印で分かります。
mod_manager/hint_mod_action_title = MODの操作
mod_manager/hint_mod_action_body = 状態に応じて、インストール、更新、バージョン変更、再インストール、削除を実行します。
mod_manager/hint_mod_enabled_title = MODの読み込み
mod_manager/hint_mod_enabled_body = 無効なMODはディスクに残りますが、ゲームでは読み込まれません。セーブとMODデータは保持されます。
mod_manager/hint_mod_delete_title = MODを削除
mod_manager/hint_mod_delete_body = MODファイルを端末から削除します。コレクションとセーブデータは削除しません。
mod_manager/hint_mod_collections_title = 使用しているコレクション
mod_manager/hint_mod_collections_body = このMODを含むコレクションを開きます。
mod_manager/hint_collection_new_title = 新しいコレクション
mod_manager/hint_collection_new_body = 空のゲーム構成を作成します。MODとメカニクスはエディターで追加できます。
mod_manager/hint_collection_apply_title = 内容を適用
mod_manager/hint_collection_apply_body = 不足しているMODをインストールし、必要なMODを有効にします。事前にプランを確認できます。
mod_manager/hint_collection_edit_title = コレクションを設定
mod_manager/hint_collection_edit_body = MOD、依存関係、読み込み順、変更プランを開きます。
mod_manager/hint_collection_delete_title = コレクションを削除
mod_manager/hint_collection_delete_body = コレクションだけを削除し、インストール済みMODとセーブデータは保持します。
mod_manager/hint_collection_add_mechanic_title = メカニクスを追加
mod_manager/hint_collection_add_mechanic_body = 関連するMODと設定をまとめるグループを構成に追加します。
mod_manager/hint_collection_game_title = 編集中のコレクション
mod_manager/hint_collection_game_body = この画面を閉じずに、エディターを別のゲームへ切り替えます。
mod_manager/hint_collection_problems_title = 構成の問題
mod_manager/hint_collection_problems_body = 起動を妨げる不足MOD、互換性のないバージョン、競合を表示します。
mod_manager/hint_collection_plan_title = 変更プラン
mod_manager/hint_collection_plan_body = 構成の適用時にインストールおよび有効化されるMODを表示します。
mod_manager/hint_collection_json_title = JSON形式の構成
mod_manager/hint_collection_json_body = 構成をテキストで開き、確認と精密な編集を行えます。
mod_manager/hint_collection_reset_title = 変更を破棄
mod_manager/hint_collection_reset_body = 最後に保存した構成に戻します。インストール済みMODは削除しません。
mod_manager/hint_collection_links_title = 構成のリンク
mod_manager/hint_collection_links_body = グループとMODの依存関係を示す線の表示を切り替えます。
mod_manager/hint_load_order_title = 読み込み順
mod_manager/hint_load_order_body = MODは上から下へ読み込まれます。同じリソースを置き換える場合は下のMODが優先されます。
mod_manager/sort_tooltip = リストの並べ替え:名前・作者・インストール状態。 mod_manager/sort_tooltip = リストの並べ替え:名前・作者・インストール状態。
mod_manager/filters_reset_tooltip = すべてのフィルターと検索ボックスを既定の状態に戻します。 mod_manager/filters_reset_tooltip = すべてのフィルターと検索ボックスを既定の状態に戻します。
mod_manager/collection_json_tooltip = 構成を生のJSONとして開きます。表示・編集して元に適用できます。 mod_manager/collection_json_tooltip = 構成を生のJSONとして開きます。表示・編集して元に適用できます。
@@ -236,3 +283,29 @@ mod_manager/collection_reset_tooltip = 保存していない構成の変更を
mod_manager/collection_game_tooltip = 編集中の構成を別のコレクションに切り替えます。 mod_manager/collection_game_tooltip = 編集中の構成を別のコレクションに切り替えます。
mod_manager/collection_plan_tooltip = プランを開きます:適用で何をインストールし、何を有効にするか。 mod_manager/collection_plan_tooltip = プランを開きます:適用で何をインストールし、何を有効にするか。
mod_manager/collection_problems_tooltip = 問題の一覧を開きます:不足しているMod、バージョン、競合。 mod_manager/collection_problems_tooltip = 問題の一覧を開きます:不足しているMod、バージョン、競合。
# Graphite collection editor
mod_manager/collection_links_selected = 選択中の接続
mod_manager/collection_links_all = すべての接続
mod_manager/collection_links_none = 接続を非表示
mod_manager/collection_canvas_search = 図内を検索…
mod_manager/collection_canvas_fit = 幅に合わせる
mod_manager/collection_canvas_arrange = 自動配置
mod_manager/collection_canvas_focus = 選択を表示
mod_manager/collection_version_caption = このコレクションのバージョン
mod_manager/collection_version_hint = ゲームの要件です。インストール済みのMODバージョンは変わりません。
mod_manager/collection_version_edit = 要件を変更
mod_manager/collection_version_any = 任意
mod_manager/collection_version_minimum = 以上
mod_manager/collection_version_exact = 固定
mod_manager/collection_version_branch = ブランチ
mod_manager/collection_version_maximum = 以下
mod_manager/collection_version_greater = より新しい
mod_manager/collection_version_less = より古い
mod_manager/collection_version_branch_hint = ブランチ:^1 は 1.x、^1.2 は 1.2.x を許可します。
mod_manager/collection_version_invalid = 無効なバージョンです。例:2.0.0。
mod_manager/collection_role_hint = グループはこのゲームでのMODの用途です。他のゲームでは異なる役割になり得ます。
mod_manager/collection_used_by = 必要とするMOD:{0}
mod_manager/collection_other_role = {0} → {1}
mod_manager/hint_collection_dependencies_title = 依存関係とグループの用途
mod_manager/hint_collection_dependencies_body = 依存先 → それを必要とするMOD。カードの位置やグループの境界は読み込み順を決めません。 グループはこのゲームでのMODの用途です。他のゲームでは異なる役割になり得ます。
+83 -1
View File
@@ -219,7 +219,63 @@ mod_manager/collection_delete_confirm = 컬렉션 "{0}"이(가) 삭제됩니다.
mod_manager/collection_delete_done = 컬렉션을 삭제했습니다. 모드는 시스템에 남아 있습니다. mod_manager/collection_delete_done = 컬렉션을 삭제했습니다. 모드는 시스템에 남아 있습니다.
mod_manager/collection_nested_readonly = 이 블록은 더 깊이 중첩되어 있습니다 — 바꾸려면 따로 여세요. mod_manager/collection_nested_readonly = 이 블록은 더 깊이 중첩되어 있습니다 — 바꾸려면 따로 여세요.
mod_manager/collection_forked = 블록 "{0}"을(를) 이 게임용으로 복사했습니다 — 다른 게임은 그대로입니다. mod_manager/collection_forked = 블록 "{0}"을(를) 이 게임용으로 복사했습니다 — 다른 게임은 그대로입니다.
mod_manager/help_open = 이 화면의 조작 방법 보기 mod_manager/help_open = 상황별 힌트 (F1)
mod_manager/help_active = 컨트롤에 포인터를 올리세요 · 왼쪽 클릭으로 고정 · Esc로 종료
mod_manager/collection_order_legend = 의존 모드 → 이를 필요로 하는 모드. 카드 위치와 그룹 경계는 로드 순서를 정하지 않습니다.
mod_manager/enable_tooltip = 이 모드를 사용하는 게임에서 불러옵니다.
mod_manager/disable_tooltip = 이 모드를 불러오지 않습니다. 파일과 저장 데이터는 유지됩니다.
mod_manager/hint_mods_tab_title = 모드
mod_manager/hint_mods_tab_body = 카탈로그와 설치된 모드입니다. 파일, 버전, 게임에서의 불러오기를 관리합니다.
mod_manager/hint_collections_tab_title = 컬렉션
mod_manager/hint_collections_tab_body = 별도의 게임처럼 실행되는 모드 묶음입니다. 구성 편집기에서 내용을 바꿀 수 있습니다.
mod_manager/hint_collection_search_title = 컬렉션 검색
mod_manager/hint_collection_search_body = 제목, 설명, 식별자, 태그로 카드를 필터링합니다.
mod_manager/hint_refresh_title = 새로 고침
mod_manager/hint_refresh_body = 카탈로그를 다시 불러와 설치된 모드와 비교합니다. 현재 설정은 유지됩니다.
mod_manager/hint_mod_search_title = 모드 검색
mod_manager/hint_mod_search_body = 모드 제목, 제작자 또는 식별자로 목록을 필터링합니다.
mod_manager/hint_mod_sort_title = 정렬
mod_manager/hint_mod_sort_body = 카드 순서만 이름, 제작자 또는 설치 상태 기준으로 바꿉니다.
mod_manager/hint_filters_reset_title = 필터 초기화
mod_manager/hint_filters_reset_body = 검색, 상태, 카테고리, 그룹화를 기본값으로 되돌립니다.
mod_manager/hint_group_categories_title = 카테고리별 그룹화
mod_manager/hint_group_categories_body = 모드는 바꾸지 않고 카드만 카테고리별로 묶습니다.
mod_manager/hint_mod_card_title = 모드 카드
mod_manager/hint_mod_card_body = 카드의 빈 곳을 클릭하면 설명, 의존성과 모든 작업이 열립니다.
mod_manager/hint_mod_version_title = 모드 버전
mod_manager/hint_mod_version_body = 설치하거나 업데이트할 버전을 선택합니다. 최신 버전과 설치된 버전이 표시됩니다.
mod_manager/hint_mod_action_title = 모드 작업
mod_manager/hint_mod_action_body = 상태에 따라 설치, 업데이트, 버전 변경, 재설치 또는 삭제를 실행합니다.
mod_manager/hint_mod_enabled_title = 모드 불러오기
mod_manager/hint_mod_enabled_body = 꺼진 모드는 디스크에 남지만 게임에서 불러오지 않습니다. 저장 파일과 모드 데이터는 유지됩니다.
mod_manager/hint_mod_delete_title = 모드 삭제
mod_manager/hint_mod_delete_body = 장치에서 모드 파일을 삭제합니다. 컬렉션과 게임 저장 파일은 삭제하지 않습니다.
mod_manager/hint_mod_collections_title = 사용 중인 컬렉션
mod_manager/hint_mod_collections_body = 이 모드가 포함된 컬렉션을 엽니다.
mod_manager/hint_collection_new_title = 새 컬렉션
mod_manager/hint_collection_new_body = 빈 게임 구성을 만듭니다. 편집기에서 모드와 메커닉을 추가하세요.
mod_manager/hint_collection_apply_title = 구성 적용
mod_manager/hint_collection_apply_body = 없는 모드를 설치하고 필요한 모드를 켭니다. 먼저 플랜을 확인할 수 있습니다.
mod_manager/hint_collection_edit_title = 컬렉션 구성
mod_manager/hint_collection_edit_body = 게임의 모드, 의존성, 불러오기 순서와 변경 플랜을 엽니다.
mod_manager/hint_collection_delete_title = 컬렉션 삭제
mod_manager/hint_collection_delete_body = 컬렉션만 삭제합니다. 설치된 모드와 게임 저장 파일은 유지됩니다.
mod_manager/hint_collection_add_mechanic_title = 메커닉 추가
mod_manager/hint_collection_add_mechanic_body = 관련 모드와 설정을 묶는 그룹을 구성에 추가합니다.
mod_manager/hint_collection_game_title = 편집 중인 컬렉션
mod_manager/hint_collection_game_body = 현재 화면을 닫지 않고 편집기를 다른 게임으로 전환합니다.
mod_manager/hint_collection_problems_title = 구성 문제
mod_manager/hint_collection_problems_body = 실행을 막는 누락 모드, 호환되지 않는 버전과 충돌을 보여 줍니다.
mod_manager/hint_collection_plan_title = 변경 플랜
mod_manager/hint_collection_plan_body = 구성을 적용할 때 설치하고 켤 모드를 보여 줍니다.
mod_manager/hint_collection_json_title = JSON 구성
mod_manager/hint_collection_json_body = 구성을 텍스트로 열어 검토하고 정밀하게 편집합니다.
mod_manager/hint_collection_reset_title = 변경 취소
mod_manager/hint_collection_reset_body = 마지막으로 저장한 구성을 복원합니다. 설치된 모드는 제거하지 않습니다.
mod_manager/hint_collection_links_title = 구성 연결
mod_manager/hint_collection_links_body = 그룹과 모드 사이의 의존성 선을 표시하거나 숨깁니다.
mod_manager/hint_load_order_title = 불러오기 순서
mod_manager/hint_load_order_body = 모드는 위에서 아래로 불러와집니다. 같은 리소스를 바꾸면 아래 모드가 적용됩니다.
mod_manager/help_close = 도움말 닫기 mod_manager/help_close = 도움말 닫기
mod_manager/help_mods_title = 모드 mod_manager/help_mods_title = 모드
mod_manager/help_mods_body = 이 장치에 설치된 모드 목록입니다. 헤더의 검색 상자로 이름을 찾고, 새로 고침 버튼으로 카탈로그를 다시 읽습니다. 방금 설치한 모드도 새로 고치면 바로 목록에 나타납니다. 모드 카드를 클릭하면 설명, 버전, 동작이 있는 페이지가 열립니다. 각 카드의 스위치는 그 모드를 사용하는 게임에서 모드를 켜거나 끕니다. 꺼진 모드는 파일을 유지하지만 불러오지 않습니다. 꺼진 모드의 장면과 콘텐츠는 다시 켤 때까지 실행 중인 게임에서 사라지지만, 저장 파일은 언제나 유지됩니다. mod_manager/help_mods_body = 이 장치에 설치된 모드 목록입니다. 헤더의 검색 상자로 이름을 찾고, 새로 고침 버튼으로 카탈로그를 다시 읽습니다. 방금 설치한 모드도 새로 고치면 바로 목록에 나타납니다. 모드 카드를 클릭하면 설명, 버전, 동작이 있는 페이지가 열립니다. 각 카드의 스위치는 그 모드를 사용하는 게임에서 모드를 켜거나 끕니다. 꺼진 모드는 파일을 유지하지만 불러오지 않습니다. 꺼진 모드의 장면과 콘텐츠는 다시 켤 때까지 실행 중인 게임에서 사라지지만, 저장 파일은 언제나 유지됩니다.
@@ -236,3 +292,29 @@ mod_manager/collection_reset_tooltip = 저장하지 않은 구성 변경을 모
mod_manager/collection_game_tooltip = 편집 중인 구성을 다른 컬렉션으로 바꿉니다. mod_manager/collection_game_tooltip = 편집 중인 구성을 다른 컬렉션으로 바꿉니다.
mod_manager/collection_plan_tooltip = 플랜을 엽니다: 적용 시 무엇을 설치하고 켜는지. mod_manager/collection_plan_tooltip = 플랜을 엽니다: 적용 시 무엇을 설치하고 켜는지.
mod_manager/collection_problems_tooltip = 문제 목록을 엽니다: 빠진 모드, 버전 차이, 충돌. mod_manager/collection_problems_tooltip = 문제 목록을 엽니다: 빠진 모드, 버전 차이, 충돌.
# Graphite collection editor
mod_manager/collection_links_selected = 선택한 연결
mod_manager/collection_links_all = 모든 연결
mod_manager/collection_links_none = 연결 숨기기
mod_manager/collection_canvas_search = 도식에서 찾기…
mod_manager/collection_canvas_fit = 너비에 맞춤
mod_manager/collection_canvas_arrange = 자동 배치
mod_manager/collection_canvas_focus = 선택 항목 보기
mod_manager/collection_version_caption = 이 컬렉션의 버전 요구 사항
mod_manager/collection_version_hint = 게임의 버전 요구 사항이며 설치된 모드 버전은 바뀌지 않습니다.
mod_manager/collection_version_edit = 요구 사항 변경
mod_manager/collection_version_any = 모든 버전
mod_manager/collection_version_minimum = 최소 버전
mod_manager/collection_version_exact = 정확한 버전
mod_manager/collection_version_branch = 버전 분기
mod_manager/collection_version_maximum = 최대 버전
mod_manager/collection_version_greater = 초과
mod_manager/collection_version_less = 미만
mod_manager/collection_version_branch_hint = 분기: ^1은 1.x를, ^1.2는 1.2.x를 허용합니다.
mod_manager/collection_version_invalid = 잘못된 버전입니다. 예: 2.0.0.
mod_manager/collection_role_hint = 그룹은 이 게임에서 모드의 용도를 설명합니다. 다른 게임에서는 역할이 다를 수 있습니다.
mod_manager/collection_used_by = 필요로 하는 모드: {0}
mod_manager/collection_other_role = {0} → {1}
mod_manager/hint_collection_dependencies_title = 의존성과 그룹 용도
mod_manager/hint_collection_dependencies_body = 의존 모드 → 이를 필요로 하는 모드. 카드 위치와 그룹 경계는 로드 순서를 정하지 않습니다. 그룹은 이 게임에서 모드의 용도를 설명합니다. 다른 게임에서는 역할이 다를 수 있습니다.
+83 -10
View File
@@ -219,16 +219,63 @@ mod_manager/collection_delete_confirm = Коллекция «{0}» будет у
mod_manager/collection_delete_done = Коллекция удалена. Моды остались в системе. mod_manager/collection_delete_done = Коллекция удалена. Моды остались в системе.
mod_manager/collection_nested_readonly = Этот блок вложен глубже — откройте его отдельно, чтобы изменить. mod_manager/collection_nested_readonly = Этот блок вложен глубже — откройте его отдельно, чтобы изменить.
mod_manager/collection_forked = Блок «{0}» скопирован для этой игры — другие игры не изменятся. mod_manager/collection_forked = Блок «{0}» скопирован для этой игры — другие игры не изменятся.
mod_manager/help_open = Подсказка: что делают элементы этого экрана mod_manager/help_open = Контекстные подсказки (F1)
mod_manager/help_close = Закрыть справку mod_manager/help_active = Наведите на элемент · ЛКМ — закрепить подсказку · Esc — выйти
mod_manager/help_mods_title = Моды mod_manager/collection_order_legend = Зависимость → мод, которому она нужна. Положение карточки и границы групп не задают порядок загрузки.
mod_manager/help_mods_body = Список модов, установленных на этом устройстве. Поле поиска в заголовке находит мод по названию, кнопка обновления перечитывает каталог — только что установленные моды появляются сразу. Щёлкните карточку мода, чтобы открыть его страницу с описанием, версией и действиями. Переключатель на карточке включает или выключает мод для игр, которые его используют; выключенный мод хранит файлы, но не загружается. Сцены и содержимое выключенного мода исчезают из запущенных игр, пока вы снова его не включите; сохранения остаются в любом случае. mod_manager/enable_tooltip = Загружать мод в играх, которые его используют.
mod_manager/help_mod_page_title = Страница мода mod_manager/disable_tooltip = Не загружать мод. Файлы и сохранения останутся.
mod_manager/help_mod_page_body = Здесь показан один мод подробно: описание, автор, версия и список его зависимостей. Отсюда можно включить или выключить мод для текущих игр, а также открыть его сцены, если мод их предоставляет. Ссылки под описанием ведут к коллекциям, куда мод входит. Кнопка «Назад» и Escape возвращают к списку модов; состояние мода сохраняется вместе с игрой и переживает перезапуск, задавать его заново не нужно. Если два мода содержат один и тот же файл, страница подскажет, какой из них победит. mod_manager/hint_mods_tab_title = Моды
mod_manager/help_collections_title = Коллекции mod_manager/hint_mods_tab_body = Каталог и установленные моды. Здесь вы управляете файлами, версиями и загрузкой модов в играх.
mod_manager/help_collections_body = Вкладка коллекций показывает игровые коллекции: готовые наборы модов, которые играются как отдельные игры. На каждой карточке — обложка, название и список модов коллекции. Щёлкните карточку, чтобы открыть коллекцию, и нажмите «Настроить», чтобы открыть редактор схемы. Установка коллекции загружает нужные ей моды; удаление коллекции никогда не трогает ваши сохранения — данные игры спят и возвращаются вместе с коллекцией. mod_manager/hint_collections_tab_title = Коллекции
mod_manager/help_scheme_title = Редактор схемы mod_manager/hint_collections_tab_body = Готовые наборы модов, которые запускаются как отдельные игры. Их состав можно менять в редакторе схемы.
mod_manager/help_scheme_body = Редактор схемы меняет состав коллекции: какие моды участвуют в игре и в каком порядке они загружаются. Включайте и выключайте флажки, чтобы добавить или убрать мод, перетаскивайте строки, чтобы изменить порядок загрузки, — поздние моды перекрывают ранние, когда оба содержат один и тот же файл. Перед сохранением редактор проверяет схему и предупреждает о недостающих зависимостях. Изменения вступают в силу при следующем запуске игры; сохранения остаются, а данные отсутствующих модов спят до их возвращения. mod_manager/hint_collection_search_title = Поиск коллекций
mod_manager/hint_collection_search_body = Фильтрует карточки по названию, описанию, идентификатору и тегам.
mod_manager/hint_refresh_title = Обновить
mod_manager/hint_refresh_body = Заново загружает каталог и сверяет его с установленными модами. Текущие настройки не сбрасываются.
mod_manager/hint_mod_search_title = Поиск модов
mod_manager/hint_mod_search_body = Фильтрует список по названию, автору или идентификатору мода.
mod_manager/hint_mod_sort_title = Сортировка
mod_manager/hint_mod_sort_body = Меняет только порядок карточек: по имени, автору или состоянию установки.
mod_manager/hint_filters_reset_title = Сброс фильтров
mod_manager/hint_filters_reset_body = Возвращает поиск, состояния, категории и группировку к значениям по умолчанию.
mod_manager/hint_group_categories_title = Группировка по категориям
mod_manager/hint_group_categories_body = Объединяет карточки в разделы. Состояние модов при этом не меняется.
mod_manager/hint_mod_card_title = Карточка мода
mod_manager/hint_mod_card_body = Нажмите на свободное место карточки, чтобы открыть описание, зависимости и все действия с модом.
mod_manager/hint_mod_version_title = Версия мода
mod_manager/hint_mod_version_body = Выбирает версию для установки или обновления. Пометки показывают последнюю и уже установленную версии.
mod_manager/hint_mod_action_title = Действие с модом
mod_manager/hint_mod_action_body = Действие зависит от состояния: установка, обновление, смена версии, переустановка или удаление.
mod_manager/hint_mod_enabled_title = Загрузка мода
mod_manager/hint_mod_enabled_body = Выключенный мод остаётся на диске, но не загружается в играх. Сохранения и данные модов сохраняются.
mod_manager/hint_mod_delete_title = Удаление мода
mod_manager/hint_mod_delete_body = Удаляет файлы мода с устройства. Коллекции и сохранения игр не удаляются.
mod_manager/hint_mod_collections_title = Использование в коллекциях
mod_manager/hint_mod_collections_body = Открывает коллекции, в состав которых входит этот мод.
mod_manager/hint_collection_new_title = Новая коллекция
mod_manager/hint_collection_new_body = Создаёт пустую схему игры. Моды и правила можно добавить в редакторе.
mod_manager/hint_collection_apply_title = Применить состав
mod_manager/hint_collection_apply_body = Устанавливает недостающие моды и включает нужные. Перед выполнением можно посмотреть план.
mod_manager/hint_collection_edit_title = Настроить коллекцию
mod_manager/hint_collection_edit_body = Открывает состав игры: моды, зависимости, порядок загрузки и план изменений.
mod_manager/hint_collection_delete_title = Удаление коллекции
mod_manager/hint_collection_delete_body = Удаляет только коллекцию. Установленные моды и сохранения игр остаются.
mod_manager/hint_collection_add_mechanic_title = Добавить механику
mod_manager/hint_collection_add_mechanic_body = Добавляет в схему группу для связанных модов и их настроек.
mod_manager/hint_collection_game_title = Редактируемая коллекция
mod_manager/hint_collection_game_body = Переключает редактор на другую игру, не закрывая текущий экран.
mod_manager/hint_collection_problems_title = Проблемы схемы
mod_manager/hint_collection_problems_body = Показывает отсутствующие моды, неподходящие версии и конфликты, которые мешают запуску.
mod_manager/hint_collection_plan_title = План изменений
mod_manager/hint_collection_plan_body = Показывает, какие моды будут установлены и включены при применении схемы.
mod_manager/hint_collection_json_title = Схема в JSON
mod_manager/hint_collection_json_body = Открывает текстовое представление схемы для просмотра и точного редактирования.
mod_manager/hint_collection_reset_title = Отменить изменения
mod_manager/hint_collection_reset_body = Возвращает последнюю сохранённую схему. Уже установленные моды не удаляются.
mod_manager/hint_collection_links_title = Связи на схеме
mod_manager/hint_collection_links_body = Показывает или скрывает линии зависимостей между группами и модами.
mod_manager/hint_load_order_title = Порядок загрузки
mod_manager/hint_load_order_body = Моды загружаются сверху вниз. Если два мода заменяют один ресурс, используется вариант нижнего мода.
mod_manager/sort_tooltip = Сортировка списка: по имени, автору или состоянию установки. mod_manager/sort_tooltip = Сортировка списка: по имени, автору или состоянию установки.
mod_manager/filters_reset_tooltip = Вернуть все фильтры и поле поиска к значениям по умолчанию. mod_manager/filters_reset_tooltip = Вернуть все фильтры и поле поиска к значениям по умолчанию.
mod_manager/collection_json_tooltip = Открыть схему как чистый JSON: посмотреть, отредактировать и применить обратно. mod_manager/collection_json_tooltip = Открыть схему как чистый JSON: посмотреть, отредактировать и применить обратно.
@@ -236,3 +283,29 @@ mod_manager/collection_reset_tooltip = Отменить все несохран
mod_manager/collection_game_tooltip = Переключить редактируемую схему на другую коллекцию. mod_manager/collection_game_tooltip = Переключить редактируемую схему на другую коллекцию.
mod_manager/collection_plan_tooltip = Открыть план: что установит и включит применение схемы. mod_manager/collection_plan_tooltip = Открыть план: что установит и включит применение схемы.
mod_manager/collection_problems_tooltip = Открыть список проблем: отсутствующие моды, версии и конфликты. mod_manager/collection_problems_tooltip = Открыть список проблем: отсутствующие моды, версии и конфликты.
# Graphite collection editor
mod_manager/collection_links_selected = Связи выбранного
mod_manager/collection_links_all = Все связи
mod_manager/collection_links_none = Без связей
mod_manager/collection_canvas_search = Найти на схеме…
mod_manager/collection_canvas_fit = Вписать
mod_manager/collection_canvas_arrange = Расположить
mod_manager/collection_canvas_focus = К выбранному
mod_manager/collection_version_caption = Версия для этой коллекции
mod_manager/collection_version_hint = Это требование игры, а не установленная версия мода.
mod_manager/collection_version_edit = Изменить требование
mod_manager/collection_version_any = Любая
mod_manager/collection_version_minimum = Не ниже
mod_manager/collection_version_exact = Точно
mod_manager/collection_version_branch = Ветка
mod_manager/collection_version_maximum = Не выше
mod_manager/collection_version_greater = Выше
mod_manager/collection_version_less = Ниже
mod_manager/collection_version_branch_hint = Ветка: ^1 принимает 1.x; ^1.2 принимает 1.2.x.
mod_manager/collection_version_invalid = Некорректная версия. Укажите, например, 2.0.0.
mod_manager/collection_role_hint = Группа задаёт назначение мода в этой игре; в другой игре его роль может отличаться.
mod_manager/collection_used_by = Нужен для: {0}
mod_manager/collection_other_role = {0} → {1}
mod_manager/hint_collection_dependencies_title = Зависимости и назначение групп
mod_manager/hint_collection_dependencies_body = Зависимость → мод, которому она нужна. Положение карточки и границы групп не задают порядок загрузки. Группа задаёт назначение мода в этой игре; в другой игре его роль может отличаться.
+83 -10
View File
@@ -219,16 +219,63 @@ mod_manager/collection_delete_confirm = Bộ sưu tập «{0}» sẽ bị xóa.
mod_manager/collection_delete_done = Đã xóa bộ sưu tập. Các mod của nó vẫn ở trong hệ thống. mod_manager/collection_delete_done = Đã xóa bộ sưu tập. Các mod của nó vẫn ở trong hệ thống.
mod_manager/collection_nested_readonly = Khối này nằm lồng sâu hơn — hãy mở riêng nó để thay đổi. mod_manager/collection_nested_readonly = Khối này nằm lồng sâu hơn — hãy mở riêng nó để thay đổi.
mod_manager/collection_forked = Đã sao chép khối "{0}" cho game này — các game khác giữ nguyên. mod_manager/collection_forked = Đã sao chép khối "{0}" cho game này — các game khác giữ nguyên.
mod_manager/help_open = Xem chú thích các điều khiển trên màn hình này mod_manager/help_open = Gợi ý theo ngữ cảnh (F1)
mod_manager/help_close = Đóng trợ giúp mod_manager/help_active = Di chuột lên điều khiển · Nhấp trái để ghim · Esc để thoát
mod_manager/help_mods_title = Mod mod_manager/collection_order_legend = Phụ thuộc → mod cần nó. Vị trí thẻ và ranh giới nhóm không quyết định thứ tự tải.
mod_manager/help_mods_body = Đây là danh sách mod đã cài trên thiết bị này. Dùng ô tìm kiếm ở đầu trang để tìm mod theo tên, và nút làm mới để đọc lại danh mục — mod mới cài sẽ xuất hiện ngay. Nhấp vào thẻ mod để mở trang của nó với mô tả, phiên bản và các thao tác. Công tắc trên mỗi thẻ bật hoặc tắt mod cho những trò chơi dùng nó; mod bị tắt vẫn giữ tệp nhưng không được nạp. Cảnh và nội dung của mod bị tắt biến mất khỏi trò chơi đang chạy cho tới khi bạn bật lại, còn bản lưu của bạn vẫn được giữ. mod_manager/enable_tooltip = Nạp mod này trong các game có sử dụng nó.
mod_manager/help_mod_page_title = Trang mod mod_manager/disable_tooltip = Không nạp mod này. Tệp và bản lưu vẫn được giữ.
mod_manager/help_mod_page_body = Trang này hiển thị chi tiết một mod: mô tả, tác giả, phiên bản và danh sách phụ thuộc. Từ đây bạn có thể bật hoặc tắt mod cho các trò chơi hiện tại, và mở các cảnh của mod nếu nó có cung cấp. Các liên kết dưới phần mô tả dẫn tới những bộ sưu tập có chứa mod này. Nút quay lại và Escape đưa bạn về danh sách mod; trạng thái bật/tắt của mod được lưu cùng trò chơi và giữ nguyên sau khi khởi động lại, nên không cần đặt lại mỗi lần. Nếu hai mod đều có cùng một tệp, trang sẽ cho biết tệp của mod nào thắng. mod_manager/hint_mods_tab_title = Mod
mod_manager/help_collections_title = Bộ sưu tập mod_manager/hint_mods_tab_body = Danh mục và mod đã cài. Quản lý tệp, phiên bản và việc game nạp mod tại đây.
mod_manager/help_collections_body = Thẻ bộ sưu tập liệt kê các bộ sưu tập trò chơi: những gói mod sẵn sàng chơi như những trò chơi riêng biệt. Mỗi thẻ hiển thị ảnh bìa, tên và các mod mà bộ sưu tập gồm có. Nhấp vào thẻ để mở bộ sưu tập, dùng nút Cấu hình để mở trình sửa sơ đồ của nó. Cài đặt bộ sưu tập sẽ tải các mod nó cần; gỡ bỏ không bao giờ xoá bản lưu của bạn — dữ liệu trò chơi ngủ đông và quay lại cùng với bộ sưu tập. mod_manager/hint_collections_tab_title = Bộ sưu tập
mod_manager/help_scheme_title = Trình sửa sơ đồ mod_manager/hint_collections_tab_body = Các gói mod chạy như game độc lập. Có thể đổi nội dung trong trình sửa sơ đồ.
mod_manager/help_scheme_body = Trình sửa sơ đồ thay đổi nội dung của bộ sưu tập: mod nào tham gia trò chơi và theo thứ tự nạp nào. Bật tắt các ô chọn để thêm hoặc bỏ mod, kéo các hàng để đổi thứ tự nạp — khi hai mod đều có cùng một tệp, mod nạp sau sẽ thắng. Trước khi lưu, trình sửa sơ đồ kiểm tra sơ đồ và cảnh báo về phụ thuộc còn thiếu. Thay đổi có hiệu lực ở lần khởi động trò chơi kế tiếp; bản lưu của bạn được giữ lại, dữ liệu của các mod vắng mặt ngủ cho tới khi mod trở lại. mod_manager/hint_collection_search_title = Tìm bộ sưu tập
mod_manager/hint_collection_search_body = Lọc thẻ theo tên, mô tả, định danh và thẻ phân loại.
mod_manager/hint_refresh_title = Làm mới
mod_manager/hint_refresh_body = Nạp lại danh mục và đối chiếu với mod đã cài. Thiết lập hiện tại được giữ nguyên.
mod_manager/hint_mod_search_title = Tìm mod
mod_manager/hint_mod_search_body = Lọc danh sách theo tên, tác giả hoặc định danh mod.
mod_manager/hint_mod_sort_title = Sắp xếp
mod_manager/hint_mod_sort_body = Chỉ đổi thứ tự thẻ theo tên, tác giả hoặc trạng thái cài đặt.
mod_manager/hint_filters_reset_title = Đặt lại bộ lọc
mod_manager/hint_filters_reset_body = Khôi phục tìm kiếm, trạng thái, danh mục và cách nhóm mặc định.
mod_manager/hint_group_categories_title = Nhóm theo danh mục
mod_manager/hint_group_categories_body = Nhóm các thẻ vào từng mục mà không thay đổi mod.
mod_manager/hint_mod_card_title = Thẻ mod
mod_manager/hint_mod_card_body = Nhấp vào chỗ trống trên thẻ để mở mô tả, phụ thuộc và mọi thao tác khả dụng.
mod_manager/hint_mod_version_title = Phiên bản mod
mod_manager/hint_mod_version_body = Chọn phiên bản để cài hoặc cập nhật. Nhãn chỉ ra bản mới nhất và bản đang cài.
mod_manager/hint_mod_action_title = Thao tác với mod
mod_manager/hint_mod_action_body = Thao tác tùy trạng thái: cài, cập nhật, đổi phiên bản, cài lại hoặc xóa.
mod_manager/hint_mod_enabled_title = Nạp mod
mod_manager/hint_mod_enabled_body = Mod bị tắt vẫn trên đĩa nhưng game không nạp. Bản lưu và dữ liệu mod được giữ.
mod_manager/hint_mod_delete_title = Xóa mod
mod_manager/hint_mod_delete_body = Xóa tệp mod khỏi thiết bị. Không xóa bộ sưu tập và bản lưu game.
mod_manager/hint_mod_collections_title = Dùng trong bộ sưu tập
mod_manager/hint_mod_collections_body = Mở các bộ sưu tập có chứa mod này.
mod_manager/hint_collection_new_title = Bộ sưu tập mới
mod_manager/hint_collection_new_body = Tạo sơ đồ game trống. Thêm mod và cơ chế trong trình sửa.
mod_manager/hint_collection_apply_title = Áp dụng nội dung
mod_manager/hint_collection_apply_body = Cài mod còn thiếu và bật mod cần thiết. Có thể xem kế hoạch trước.
mod_manager/hint_collection_edit_title = Cấu hình bộ sưu tập
mod_manager/hint_collection_edit_body = Mở nội dung game: mod, phụ thuộc, thứ tự nạp và kế hoạch thay đổi.
mod_manager/hint_collection_delete_title = Xóa bộ sưu tập
mod_manager/hint_collection_delete_body = Chỉ xóa bộ sưu tập. Mod đã cài và bản lưu game được giữ.
mod_manager/hint_collection_add_mechanic_title = Thêm cơ chế
mod_manager/hint_collection_add_mechanic_body = Thêm nhóm cho các mod liên quan và thiết lập của chúng vào sơ đồ.
mod_manager/hint_collection_game_title = Bộ sưu tập đang sửa
mod_manager/hint_collection_game_body = Chuyển trình sửa sang game khác mà không đóng màn hình này.
mod_manager/hint_collection_problems_title = Vấn đề của sơ đồ
mod_manager/hint_collection_problems_body = Liệt kê mod thiếu, phiên bản không tương thích và xung đột ngăn game chạy.
mod_manager/hint_collection_plan_title = Kế hoạch thay đổi
mod_manager/hint_collection_plan_body = Cho biết mod nào sẽ được cài và bật khi áp dụng sơ đồ.
mod_manager/hint_collection_json_title = Sơ đồ dạng JSON
mod_manager/hint_collection_json_body = Mở sơ đồ dưới dạng văn bản để kiểm tra và sửa chính xác.
mod_manager/hint_collection_reset_title = Hủy thay đổi
mod_manager/hint_collection_reset_body = Khôi phục sơ đồ đã lưu gần nhất. Không gỡ các mod đã cài.
mod_manager/hint_collection_links_title = Liên kết sơ đồ
mod_manager/hint_collection_links_body = Hiện hoặc ẩn đường phụ thuộc giữa các nhóm và mod.
mod_manager/hint_load_order_title = Thứ tự nạp
mod_manager/hint_load_order_body = Mod nạp từ trên xuống dưới. Khi hai mod thay cùng một tài nguyên, mod phía dưới được dùng.
mod_manager/sort_tooltip = Sắp xếp danh sách: theo tên, tác giả hoặc trạng thái cài đặt. mod_manager/sort_tooltip = Sắp xếp danh sách: theo tên, tác giả hoặc trạng thái cài đặt.
mod_manager/filters_reset_tooltip = Đưa mọi bộ lọc và ô tìm kiếm về mặc định. mod_manager/filters_reset_tooltip = Đưa mọi bộ lọc và ô tìm kiếm về mặc định.
mod_manager/collection_json_tooltip = Mở sơ đồ dưới dạng JSON thô: xem, sửa rồi áp dụng lại. mod_manager/collection_json_tooltip = Mở sơ đồ dưới dạng JSON thô: xem, sửa rồi áp dụng lại.
@@ -236,3 +283,29 @@ mod_manager/collection_reset_tooltip = Bỏ mọi thay đổi chưa lưu của s
mod_manager/collection_game_tooltip = Chuyển sơ đồ đang sửa sang bộ sưu tập khác. mod_manager/collection_game_tooltip = Chuyển sơ đồ đang sửa sang bộ sưu tập khác.
mod_manager/collection_plan_tooltip = Mở kế hoạch: việc áp dụng sẽ cài và bật những gì. mod_manager/collection_plan_tooltip = Mở kế hoạch: việc áp dụng sẽ cài và bật những gì.
mod_manager/collection_problems_tooltip = Mở danh sách vấn đề: mod còn thiếu, khác phiên bản và xung đột. mod_manager/collection_problems_tooltip = Mở danh sách vấn đề: mod còn thiếu, khác phiên bản và xung đột.
# Graphite collection editor
mod_manager/collection_links_selected = Liên kết đã chọn
mod_manager/collection_links_all = Mọi liên kết
mod_manager/collection_links_none = Ẩn liên kết
mod_manager/collection_canvas_search = Tìm trong sơ đồ…
mod_manager/collection_canvas_fit = Vừa chiều rộng
mod_manager/collection_canvas_arrange = Sắp xếp
mod_manager/collection_canvas_focus = Hiện mục đã chọn
mod_manager/collection_version_caption = Phiên bản cho bộ sưu tập này
mod_manager/collection_version_hint = Đây là yêu cầu của game, không phải phiên bản mod đã cài.
mod_manager/collection_version_edit = Đổi yêu cầu
mod_manager/collection_version_any = Bất kỳ
mod_manager/collection_version_minimum = Tối thiểu
mod_manager/collection_version_exact = Chính xác
mod_manager/collection_version_branch = Nhánh
mod_manager/collection_version_maximum = Tối đa
mod_manager/collection_version_greater = Lớn hơn
mod_manager/collection_version_less = Nhỏ hơn
mod_manager/collection_version_branch_hint = Nhánh: ^1 chấp nhận 1.x; ^1.2 chấp nhận 1.2.x.
mod_manager/collection_version_invalid = Phiên bản không hợp lệ. Ví dụ: 2.0.0.
mod_manager/collection_role_hint = Nhóm mô tả vai trò của mod trong game này; game khác có thể gán vai trò khác.
mod_manager/collection_used_by = Cần cho: {0}
mod_manager/collection_other_role = {0} → {1}
mod_manager/hint_collection_dependencies_title = Phụ thuộc và vai trò nhóm
mod_manager/hint_collection_dependencies_body = Phụ thuộc → mod cần nó. Vị trí thẻ và ranh giới nhóm không quyết định thứ tự tải. Nhóm mô tả vai trò của mod trong game này; game khác có thể gán vai trò khác.
+83 -10
View File
@@ -219,16 +219,63 @@ mod_manager/collection_delete_confirm = 合集"{0}"将被删除。它的模组
mod_manager/collection_delete_done = 合集已删除。它的模组仍在系统中。 mod_manager/collection_delete_done = 合集已删除。它的模组仍在系统中。
mod_manager/collection_nested_readonly = 这个模块嵌套得更深——请单独打开它来修改。 mod_manager/collection_nested_readonly = 这个模块嵌套得更深——请单独打开它来修改。
mod_manager/collection_forked = 模块"{0}"已为本游戏复制——其他游戏保持不变。 mod_manager/collection_forked = 模块"{0}"已为本游戏复制——其他游戏保持不变。
mod_manager/help_open = 查看本界面各控件的说明 mod_manager/help_open = 上下文提示(F1)
mod_manager/help_close = 关闭帮助 mod_manager/help_active = 将指针移到控件上 · 左键单击固定提示 · Esc 退出
mod_manager/help_mods_title = 模组 mod_manager/collection_order_legend = 依赖项 → 使用它的模组。卡片位置和分组边界不决定加载顺序。
mod_manager/help_mods_body = 这里是已安装到这台设备上的全部模组的列表。顶部的搜索框可以按名称快速查找模组,旁边的刷新按钮会重新读取目录,刚安装的模组会立刻出现。点击任意模组卡片即可打开它的页面,查看描述、版本和可以执行的操作。每张卡片上的开关决定使用该模组的游戏是否加载它;停用的模组会保留自己的文件,但不会进入游戏。被停用模组的场景和内容会从正在运行的游戏中消失,直到你重新启用它为止,你的存档数据始终保留。列表为空时先刷新目录;如果模组只属于某个游戏,切换游戏后它会出现在对应的列表里。 mod_manager/enable_tooltip = 在使用此模组的游戏中加载它。
mod_manager/help_mod_page_title = 模组页面 mod_manager/disable_tooltip = 不加载此模组。文件和存档会保留。
mod_manager/help_mod_page_body = 这个页面详细展示一个模组:描述、作者、版本,以及它依赖的其他模组。在这里可以把模组对当前的游戏启用或停用,如果模组提供了场景,也可以直接打开这些场景。描述下方的链接会跳转到包含这个模组的合集。返回按钮和 Esc 键带你回到模组列表;模组的开关状态会和游戏一起保存,重启之后依然保持不变,不需要每次重新设置。如果两个模组带有同一个文件,页面会告诉你哪一方生效。如果你只是想试用一个模组,可以在这里看完它的依赖,再去合集中决定是否安装,避免装上互相冲突的内容。 mod_manager/hint_mods_tab_title = 模组
mod_manager/help_collections_title = 合集 mod_manager/hint_mods_tab_body = 模组目录和已安装的模组。在这里管理文件、版本以及游戏是否加载它们。
mod_manager/help_collections_body = 合集标签页列出游戏的合集:合集是一组打包好的模组,玩起来就像一款独立的游戏。每张卡片显示封面、标题,以及这个合集包含的模组。点击卡片即可打开合集,使用“配置”按钮可以打开它的结构编辑器。安装合集会把它需要的模组一并取回来;移除合集绝不会删除你的存档——游戏的数据只是安静等待,合集回来时会随之恢复,你不会丢失任何东西。合集的封面上还标有它的类别和来源,安装之前可以先把几个合集对比一下,再决定要玩哪一个;找不到想要的合集时,先用刷新按钮更新目录再来查看。 mod_manager/hint_collections_tab_title = 合集
mod_manager/help_scheme_title = 结构编辑器 mod_manager/hint_collections_tab_body = 可作为独立游戏运行的模组组合。可以在结构编辑器中更改其内容。
mod_manager/help_scheme_body = 结构编辑器修改合集的内容:哪些模组参与这个游戏,以及它们按什么顺序加载。勾选或取消勾选可以添加或移除模组,拖动行可以改变加载顺序——当两个模组带有同一个文件时,后加载的会覆盖先加载的,页面上会用提示标出这样的覆盖关系。保存之前编辑器会检查结构,并对缺失的依赖提出警告,照着提示补齐即可。更改在下次启动游戏时生效;你的存档保留不动,缺失模组的数据会休眠,等模组回来时一并恢复。对结构没有把握时,先不要改动游戏自带的合集,复制一份再做试验更安全。 mod_manager/hint_collection_search_title = 搜索合集
mod_manager/hint_collection_search_body = 按标题、描述、标识符和标签筛选卡片。
mod_manager/hint_refresh_title = 刷新
mod_manager/hint_refresh_body = 重新加载目录并与已安装的模组核对,保留当前设置。
mod_manager/hint_mod_search_title = 搜索模组
mod_manager/hint_mod_search_body = 按模组标题、作者或标识符筛选列表。
mod_manager/hint_mod_sort_title = 排序
mod_manager/hint_mod_sort_body = 只改变卡片顺序:按名称、作者或安装状态。
mod_manager/hint_filters_reset_title = 重置筛选
mod_manager/hint_filters_reset_body = 恢复默认的搜索、状态、类别和分组方式。
mod_manager/hint_group_categories_title = 按类别分组
mod_manager/hint_group_categories_body = 将卡片按类别分区,不改变模组本身。
mod_manager/hint_mod_card_title = 模组卡片
mod_manager/hint_mod_card_body = 点击卡片空白处可打开描述、依赖项和全部可用操作。
mod_manager/hint_mod_version_title = 模组版本
mod_manager/hint_mod_version_body = 选择要安装或更新的版本。标记会指出最新版和当前安装版。
mod_manager/hint_mod_action_title = 模组操作
mod_manager/hint_mod_action_body = 操作随模组状态变化:安装、更新、切换版本、重新安装或删除。
mod_manager/hint_mod_enabled_title = 加载模组
mod_manager/hint_mod_enabled_body = 停用的模组仍保留在磁盘上,但游戏不会加载。存档和模组数据都会保留。
mod_manager/hint_mod_delete_title = 删除模组
mod_manager/hint_mod_delete_body = 从设备上删除模组文件,不删除合集和游戏存档。
mod_manager/hint_mod_collections_title = 用于合集
mod_manager/hint_mod_collections_body = 打开包含此模组的合集。
mod_manager/hint_collection_new_title = 新合集
mod_manager/hint_collection_new_body = 创建空的游戏结构,然后在编辑器中添加模组和机制。
mod_manager/hint_collection_apply_title = 应用内容
mod_manager/hint_collection_apply_body = 安装缺失的模组并启用所需模组。执行前可以先查看计划。
mod_manager/hint_collection_edit_title = 配置合集
mod_manager/hint_collection_edit_body = 打开游戏内容:模组、依赖项、加载顺序和更改计划。
mod_manager/hint_collection_delete_title = 删除合集
mod_manager/hint_collection_delete_body = 只删除合集,保留已安装的模组和游戏存档。
mod_manager/hint_collection_add_mechanic_title = 添加机制
mod_manager/hint_collection_add_mechanic_body = 在结构中添加一组相关模组及其设置。
mod_manager/hint_collection_game_title = 正在编辑的合集
mod_manager/hint_collection_game_body = 不关闭当前界面,直接切换到另一个游戏。
mod_manager/hint_collection_problems_title = 结构问题
mod_manager/hint_collection_problems_body = 列出阻碍游戏运行的缺失模组、不兼容版本和冲突。
mod_manager/hint_collection_plan_title = 更改计划
mod_manager/hint_collection_plan_body = 显示应用结构时将安装和启用哪些模组。
mod_manager/hint_collection_json_title = JSON 结构
mod_manager/hint_collection_json_body = 以文本形式打开结构,便于检查和精确编辑。
mod_manager/hint_collection_reset_title = 放弃更改
mod_manager/hint_collection_reset_body = 恢复上次保存的结构,不移除已经安装的模组。
mod_manager/hint_collection_links_title = 结构连线
mod_manager/hint_collection_links_body = 显示或隐藏组与模组之间的依赖连线。
mod_manager/hint_load_order_title = 加载顺序
mod_manager/hint_load_order_body = 模组从上到下加载。两个模组替换同一资源时,下方的模组生效。
mod_manager/sort_tooltip = 对列表排序:按名称、作者或安装状态。 mod_manager/sort_tooltip = 对列表排序:按名称、作者或安装状态。
mod_manager/filters_reset_tooltip = 把所有筛选条件和搜索框恢复为默认值。 mod_manager/filters_reset_tooltip = 把所有筛选条件和搜索框恢复为默认值。
mod_manager/collection_json_tooltip = 以原始 JSON 打开结构:查看、编辑后再应用回去。 mod_manager/collection_json_tooltip = 以原始 JSON 打开结构:查看、编辑后再应用回去。
@@ -236,3 +283,29 @@ mod_manager/collection_reset_tooltip = 放弃所有未保存的结构修改。
mod_manager/collection_game_tooltip = 把正在编辑的结构切换到另一个合集。 mod_manager/collection_game_tooltip = 把正在编辑的结构切换到另一个合集。
mod_manager/collection_plan_tooltip = 打开计划:应用时会安装和启用哪些模组。 mod_manager/collection_plan_tooltip = 打开计划:应用时会安装和启用哪些模组。
mod_manager/collection_problems_tooltip = 打开问题列表:缺失的模组、版本差距和冲突。 mod_manager/collection_problems_tooltip = 打开问题列表:缺失的模组、版本差距和冲突。
# Graphite collection editor
mod_manager/collection_links_selected = 所选模组的连线
mod_manager/collection_links_all = 所有连线
mod_manager/collection_links_none = 隐藏连线
mod_manager/collection_canvas_search = 在图中查找…
mod_manager/collection_canvas_fit = 适应宽度
mod_manager/collection_canvas_arrange = 自动排列
mod_manager/collection_canvas_focus = 显示所选项
mod_manager/collection_version_caption = 此合集的版本要求
mod_manager/collection_version_hint = 这是游戏的版本要求,不是已安装的模组版本。
mod_manager/collection_version_edit = 修改要求
mod_manager/collection_version_any = 任意版本
mod_manager/collection_version_minimum = 不低于
mod_manager/collection_version_exact = 精确版本
mod_manager/collection_version_branch = 版本分支
mod_manager/collection_version_maximum = 不高于
mod_manager/collection_version_greater = 高于
mod_manager/collection_version_less = 低于
mod_manager/collection_version_branch_hint = 分支:^1 接受 1.x;^1.2 接受 1.2.x。
mod_manager/collection_version_invalid = 版本无效。请输入例如 2.0.0。
mod_manager/collection_role_hint = 分组说明模组在本游戏中的用途;在其他游戏中,它可以有不同的作用。
mod_manager/collection_used_by = 被以下模组依赖:{0}
mod_manager/collection_other_role = {0} → {1}
mod_manager/hint_collection_dependencies_title = 依赖关系与分组用途
mod_manager/hint_collection_dependencies_body = 依赖项 → 使用它的模组。卡片位置和分组边界不决定加载顺序。 分组说明模组在本游戏中的用途;在其他游戏中,它可以有不同的作用。
+50 -37
View File
@@ -9,55 +9,68 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_help") script = ExtResource("1_help")
[node name="Dim" type="ColorRect" parent="."] [node name="Highlight" type="Panel" parent="."]
layout_mode = 1 visible = false
anchors_preset = 15 layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.6)
[node name="Center" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2 mouse_filter = 2
[node name="Frame" type="PanelContainer" parent="Center"] [node name="Card" type="PanelContainer" parent="."]
custom_minimum_size = Vector2(820, 600) visible = false
layout_mode = 2 custom_minimum_size = Vector2(320, 0)
layout_mode = 0
mouse_filter = 0
[node name="Margin" type="MarginContainer" parent="Center/Frame"] [node name="Margin" type="MarginContainer" parent="Card"]
layout_mode = 2 layout_mode = 2
theme_override_constants/margin_left = 24 theme_override_constants/margin_left = 12
theme_override_constants/margin_top = 18 theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 24 theme_override_constants/margin_right = 12
theme_override_constants/margin_bottom = 18 theme_override_constants/margin_bottom = 10
[node name="Column" type="VBoxContainer" parent="Center/Frame/Margin"] [node name="Column" type="VBoxContainer" parent="Card/Margin"]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 theme_override_constants/separation = 4
[node name="Header" type="HBoxContainer" parent="Center/Frame/Margin/Column"] [node name="Title" type="Label" parent="Card/Margin/Column"]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 theme_override_font_sizes/font_size = 13
autowrap_mode = 3
[node name="Title" type="Label" parent="Center/Frame/Margin/Column/Header"] [node name="Body" type="RichTextLabel" parent="Card/Margin/Column"]
custom_minimum_size = Vector2(296, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_font_sizes/font_size = 20 theme_override_font_sizes/normal_font_size = 12
[node name="Close" type="Button" parent="Center/Frame/Margin/Column/Header"]
layout_mode = 2
[node name="Body" type="RichTextLabel" parent="Center/Frame/Margin/Column"]
layout_mode = 2
size_flags_vertical = 3
bbcode_enabled = true bbcode_enabled = true
selection_enabled = true selection_enabled = true
scroll_active = false
fit_content = true
autowrap_mode = 3
[node name="Footer" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -34.0
grow_horizontal = 2
grow_vertical = 0
mouse_filter = 2
[node name="Margin" type="MarginContainer" parent="Footer"]
layout_mode = 2
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 7
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 7
mouse_filter = 2
[node name="FooterText" type="Label" parent="Footer/Margin"]
layout_mode = 2
theme_override_font_sizes/font_size = 12
horizontal_alignment = 1
mouse_filter = 2
+121 -5
View File
@@ -2,6 +2,7 @@
[ext_resource type="Script" path="src/ModCollectionsScreen.cs" id="1_screen"] [ext_resource type="Script" path="src/ModCollectionsScreen.cs" id="1_screen"]
[ext_resource type="Script" path="src/UI/CollectionSchemeCanvas.cs" id="2_canvas"] [ext_resource type="Script" path="src/UI/CollectionSchemeCanvas.cs" id="2_canvas"]
[ext_resource type="Script" path="src/UI/CollectionMiniMap.cs" id="3_map"]
[node name="ModCollectionsScreen" type="Control"] [node name="ModCollectionsScreen" type="Control"]
layout_mode = 3 layout_mode = 3
@@ -70,10 +71,9 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
mouse_filter = 2 mouse_filter = 2
[node name="EdgesToggle" type="CheckBox" parent="Margin/Root/Header"] [node name="EdgesToggle" type="OptionButton" parent="Margin/Root/Header"]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 4 size_flags_vertical = 4
button_pressed = true
[node name="Problems" type="Button" parent="Margin/Root/Header"] [node name="Problems" type="Button" parent="Margin/Root/Header"]
layout_mode = 2 layout_mode = 2
@@ -112,25 +112,101 @@ size_flags_vertical = 4
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 8 theme_override_constants/separation = 8
[node name="Tools" type="HBoxContainer" parent="Margin/Root"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="AddMod" type="Button" parent="Margin/Root/Tools"]
layout_mode = 2
text = "mod_manager/collection_add_mod"
[node name="Search" type="LineEdit" parent="Margin/Root/Tools"]
custom_minimum_size = Vector2(170, 0)
layout_mode = 2
placeholder_text = "mod_manager/collection_canvas_search"
[node name="Spacer" type="Control" parent="Margin/Root/Tools"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="ZoomOut" type="Button" parent="Margin/Root/Tools"]
layout_mode = 2
text = "−"
[node name="Zoom" type="Label" parent="Margin/Root/Tools"]
custom_minimum_size = Vector2(42, 0)
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 1
text = "100%"
[node name="ZoomIn" type="Button" parent="Margin/Root/Tools"]
layout_mode = 2
text = "+"
[node name="Fit" type="Button" parent="Margin/Root/Tools"]
layout_mode = 2
text = "mod_manager/collection_canvas_fit"
[node name="Arrange" type="Button" parent="Margin/Root/Tools"]
layout_mode = 2
text = "mod_manager/collection_canvas_arrange"
[node name="Focus" type="Button" parent="Margin/Root/Tools"]
layout_mode = 2
text = "mod_manager/collection_canvas_focus"
[node name="OrderLegend" type="Label" parent="Margin/Root"]
layout_mode = 2
theme_override_font_sizes/font_size = 12
mouse_filter = 0
[node name="Body" type="HSplitContainer" parent="Margin/Root"] [node name="Body" type="HSplitContainer" parent="Margin/Root"]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
[node name="CanvasScroll" type="ScrollContainer" parent="Margin/Root/Body"] [node name="CanvasHost" type="Control" parent="Margin/Root/Body"]
custom_minimum_size = Vector2(440, 240)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
mouse_filter = 2
[node name="CanvasScroll" type="ScrollContainer" parent="Margin/Root/Body/CanvasHost"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
horizontal_scroll_mode = 1 horizontal_scroll_mode = 1
vertical_scroll_mode = 1 vertical_scroll_mode = 1
[node name="SchemeCanvas" type="Control" parent="Margin/Root/Body/CanvasScroll"] [node name="SchemeCanvas" type="Control" parent="Margin/Root/Body/CanvasHost/CanvasScroll"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
script = ExtResource("2_canvas") script = ExtResource("2_canvas")
[node name="MiniMap" type="Control" parent="Margin/Root/Body/CanvasHost"]
layout_mode = 1
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -170.0
offset_top = -124.0
offset_right = -18.0
offset_bottom = -24.0
grow_horizontal = 0
grow_vertical = 0
mouse_filter = 0
script = ExtResource("3_map")
[node name="Details" type="PanelContainer" parent="Margin/Root/Body"] [node name="Details" type="PanelContainer" parent="Margin/Root/Body"]
custom_minimum_size = Vector2(380, 0) custom_minimum_size = Vector2(330, 0)
layout_mode = 2 layout_mode = 2
[node name="DetailScroll" type="ScrollContainer" parent="Margin/Root/Body/Details"] [node name="DetailScroll" type="ScrollContainer" parent="Margin/Root/Body/Details"]
@@ -174,6 +250,46 @@ autowrap_mode = 3
layout_mode = 2 layout_mode = 2
autowrap_mode = 3 autowrap_mode = 3
[node name="VersionEditor" type="VBoxContainer" parent="Margin/Root/Body/Details/DetailScroll/Detail"]
visible = false
layout_mode = 2
theme_override_constants/separation = 7
[node name="Caption" type="Label" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor"]
layout_mode = 2
text = "mod_manager/collection_version_caption"
autowrap_mode = 3
[node name="Fields" type="HBoxContainer" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Policy" type="OptionButton" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor/Fields"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Value" type="LineEdit" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor/Fields"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
[node name="Rule" type="Label" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor"]
layout_mode = 2
autowrap_mode = 3
[node name="Hint" type="Label" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor"]
layout_mode = 2
text = "mod_manager/collection_version_hint"
autowrap_mode = 3
[node name="Feedback" type="Label" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor"]
layout_mode = 2
autowrap_mode = 3
[node name="SaveRule" type="Button" parent="Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor"]
layout_mode = 2
text = "mod_manager/collection_version_edit"
[node name="DetailExtra" type="VBoxContainer" parent="Margin/Root/Body/Details/DetailScroll/Detail"] [node name="DetailExtra" type="VBoxContainer" parent="Margin/Root/Body/Details/DetailScroll/Detail"]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 6 theme_override_constants/separation = 6
+375 -42
View File
@@ -11,6 +11,7 @@ using Godot;
using Cthangover.Mods.Interface.Base.Lists; using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming; using Cthangover.Mods.Interface.Theming;
using Cthangover.Mods.Interface.Tool;
namespace Mods.ModManager namespace Mods.ModManager
{ {
/// <summary> /// <summary>
@@ -39,7 +40,7 @@ namespace Mods.ModManager
private Button _newGame; private Button _newGame;
private Button _apply; private Button _apply;
private OptionButton _gameSelect; private OptionButton _gameSelect;
private CheckBox _edgesToggle; private OptionButton _edgesToggle;
private Button _pillProblems; private Button _pillProblems;
private Button _pillPlan; private Button _pillPlan;
private Label _modsCount; private Label _modsCount;
@@ -86,6 +87,17 @@ namespace Mods.ModManager
private Button _revert; private Button _revert;
private Button _delete; private Button _delete;
private Label _savesInfo; private Label _savesInfo;
private Button _addMod;
private LineEdit _canvasSearch;
private Label _zoomLabel;
private VBoxContainer _versionEditor;
private OptionButton _versionPolicy;
private LineEdit _versionValue;
private Label _versionRule;
private Label _versionFeedback;
private ModCollectionModRef _versionReference;
private bool _fitAfterBuild = true;
private bool _fitQueued;
/// <summary>Drafts created by "Duplicate", mapped to the game they were copied from (offer to copy its saves).</summary> /// <summary>Drafts created by "Duplicate", mapped to the game they were copied from (offer to copy its saves).</summary>
private readonly Dictionary<string, string> _duplicatedFrom = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, string> _duplicatedFrom = new(StringComparer.OrdinalIgnoreCase);
@@ -134,6 +146,7 @@ namespace Mods.ModManager
public void OpenCollection(string collectionId) public void OpenCollection(string collectionId)
{ {
_currentId = collectionId; _currentId = collectionId;
_fitAfterBuild = true;
_selectedGroupKey = null; _selectedGroupKey = null;
_selectedModId = null; _selectedModId = null;
// The gallery re-reads the store on every refresh; re-read it here // The gallery re-reads the store on every refresh; re-read it here
@@ -163,6 +176,254 @@ namespace Mods.ModManager
UnsubscribeEvents(); UnsubscribeEvents();
} }
private void BindGraphiteControls()
{
var tools = "Margin/Root/Tools/";
_addMod = GetNode<Button>(tools + "AddMod");
_addMod.Text = TranslationServer.Translate("mod_manager/collection_add_mod");
_addMod.Pressed += OnAddModPressed;
_canvasSearch = GetNode<LineEdit>(tools + "Search");
_canvasSearch.PlaceholderText = TranslationServer.Translate("mod_manager/collection_canvas_search");
_canvasSearch.TextChanged += _canvas.SetSearch;
_zoomLabel = GetNode<Label>(tools + "Zoom");
GetNode<Button>(tools + "ZoomOut").Pressed += _canvas.ZoomOut;
GetNode<Button>(tools + "ZoomIn").Pressed += _canvas.ZoomIn;
var fit = GetNode<Button>(tools + "Fit");
fit.Text = TranslationServer.Translate("mod_manager/collection_canvas_fit");
fit.Pressed += _canvas.FitWidth;
var arrange = GetNode<Button>(tools + "Arrange");
arrange.Text = TranslationServer.Translate("mod_manager/collection_canvas_arrange");
arrange.Pressed += _canvas.ArrangeGroups;
var focus = GetNode<Button>(tools + "Focus");
focus.Text = TranslationServer.Translate("mod_manager/collection_canvas_focus");
focus.Pressed += _canvas.ShowSelection;
_canvas.SetMiniMap(GetNode<CollectionMiniMap>("Margin/Root/Body/CanvasHost/MiniMap"));
var path = "Margin/Root/Body/Details/DetailScroll/Detail/VersionEditor";
_versionEditor = GetNode<VBoxContainer>(path);
_versionPolicy = GetNode<OptionButton>(path + "/Fields/Policy");
_versionValue = GetNode<LineEdit>(path + "/Fields/Value");
_versionRule = GetNode<Label>(path + "/Rule");
_versionFeedback = GetNode<Label>(path + "/Feedback");
GetNode<Label>(path + "/Caption").Text = TranslationServer.Translate("mod_manager/collection_version_caption");
GetNode<Label>(path + "/Hint").Text = TranslationServer.Translate("mod_manager/collection_version_hint");
var save = GetNode<Button>(path + "/SaveRule");
save.Text = TranslationServer.Translate("mod_manager/collection_version_edit");
save.Pressed += OnSaveVersionPressed;
_versionPolicy.ItemSelected += OnVersionPolicySelected;
_versionValue.TextChanged += OnVersionValueChanged;
_versionRule.AddThemeColorOverride("font_color", UiTokens.Accent);
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
var policies = new[]
{
"mod_manager/collection_version_any", "mod_manager/collection_version_minimum",
"mod_manager/collection_version_exact", "mod_manager/collection_version_branch",
"mod_manager/collection_version_maximum", "mod_manager/collection_version_greater",
"mod_manager/collection_version_less",
};
var operators = new[] { "", ">=", "=", "^", "<=", ">", "<" };
for (var i = 0; i < policies.Length; i++)
{
_versionPolicy.AddItem(TranslationServer.Translate(policies[i]));
_versionPolicy.SetItemMetadata(i, operators[i]);
}
}
private void OnGalleryPressed() => GalleryRequested?.Invoke();
private void OnBuildManagerPressed() => SelectTab("plan");
private void OnAddModPressed()
{
var group = _selectedGroupKey == null ? null : FindGroup(_selectedGroupKey);
if (group != null)
OpenModPicker(group);
}
private void OnCanvasViewChanged()
{
if (_zoomLabel != null)
_zoomLabel.Text = Mathf.RoundToInt(_canvas.Zoom * 100f) + "%";
}
private void OnCanvasFirstFrame()
{
GetTree().ProcessFrame -= OnCanvasFirstFrame;
_fitQueued = false;
_fitAfterBuild = false;
_canvas.FitWidth();
while (_canvas.Zoom < 0.75f)
_canvas.ZoomIn();
_canvas.ShowSelection();
}
private void BindVersionRule(ModCollectionGroup group, string modId)
{
foreach (var reference in group.Mods)
{
if (string.Equals(reference.ModId, modId, StringComparison.OrdinalIgnoreCase))
{
_versionReference = reference;
break;
}
}
if (_versionReference == null)
return;
var owner = EffectiveCollection(_versionReference.CollectionId);
foreach (var mechanic in owner?.Mechanics ?? new List<ModCollectionMechanic>())
{
if (!string.Equals(mechanic.Id, _versionReference.MechanicId, StringComparison.OrdinalIgnoreCase))
continue;
foreach (var raw in mechanic.Mods ?? new List<string>())
{
var requirement = ModDependency.Parse(raw);
if (!requirement.IsValid || !string.Equals(requirement.ModId, modId, StringComparison.OrdinalIgnoreCase))
continue;
for (var i = 0; i < _versionPolicy.ItemCount; i++)
{
if (_versionPolicy.GetItemMetadata(i).AsString() == requirement.Operator)
_versionPolicy.Selected = i;
}
_versionValue.Text = requirement.VersionRaw ?? string.Empty;
if (requirement.VersionRaw == null
&& ModRegistry.Instance.AllDiscoveredMods.TryGetValue(modId, out var installed))
_versionValue.Text = installed.Version ?? string.Empty;
_versionEditor.Visible = true;
_versionEditor.GetNode<Label>("Caption").Text = TranslationServer.Translate("mod_manager/collection_version_caption")
+ " · " + group.Name;
OnVersionPolicySelected(_versionPolicy.Selected);
return;
}
}
}
private void OnVersionPolicySelected(long index)
{
var op = _versionPolicy.GetItemMetadata((int)index).AsString();
_versionValue.Editable = op.Length > 0;
OnVersionValueChanged(_versionValue.Text);
}
private void OnVersionValueChanged(string value)
{
if (_versionReference == null || _versionPolicy.Selected < 0)
return;
var op = _versionPolicy.GetItemMetadata(_versionPolicy.Selected).AsString();
_versionRule.Text = _versionReference.ModId + op + (op.Length > 0 ? value.Trim() : string.Empty);
_versionFeedback.Text = op == "^"
? TranslationServer.Translate("mod_manager/collection_version_branch_hint") : string.Empty;
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.Muted);
foreach (var node in _graph?.Nodes ?? new List<ModCollectionNode>())
{
if (node.ModId != _versionReference.ModId || node.State != ModCollectionNodeState.VersionMismatch)
continue;
var requirements = new List<string>();
foreach (var requirement in node.Requirements)
{
if (!string.IsNullOrEmpty(requirement.Operator))
requirements.Add(requirement.Operator + requirement.VersionRaw);
}
_versionFeedback.Text = string.Format(TranslationServer.Translate("mod_manager/installed_version"), node.InstalledVersion)
+ "\n" + string.Format(TranslationServer.Translate("mod_manager/collection_requires"), string.Join(", ", requirements));
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
break;
}
}
private void OnSaveVersionPressed()
{
if (_versionReference == null || _versionPolicy.Selected < 0)
return;
var group = FindGroup(_selectedGroupKey);
if (group == null)
return;
var reference = _versionReference;
var op = _versionPolicy.GetItemMetadata(_versionPolicy.Selected).AsString();
var owner = EffectiveCollection(reference.CollectionId);
var valid = false;
var changed = false;
foreach (var mechanic in owner?.Mechanics ?? new List<ModCollectionMechanic>())
{
if (!string.Equals(mechanic.Id, reference.MechanicId, StringComparison.OrdinalIgnoreCase))
continue;
var preview = new ModCollectionMechanic { Mods = new List<string>(mechanic.Mods ?? new List<string>()) };
valid = preview.TrySetModVersion(reference.ModId, op, _versionValue.Text);
for (var i = 0; i < preview.Mods.Count; i++)
{
if (preview.Mods[i] != mechanic.Mods[i])
changed = true;
}
break;
}
if (!valid)
{
_versionFeedback.Text = TranslationServer.Translate("mod_manager/collection_version_invalid");
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
return;
}
if (!changed)
return;
var collection = DraftForEdit(group, reference.CollectionId);
if (collection == null)
return;
foreach (var mechanic in collection.Mechanics ?? new List<ModCollectionMechanic>())
{
if (!string.Equals(mechanic.Id, reference.MechanicId, StringComparison.OrdinalIgnoreCase))
continue;
if (!mechanic.TrySetModVersion(reference.ModId, op, _versionValue.Text))
{
_versionFeedback.Text = TranslationServer.Translate("mod_manager/collection_version_invalid");
_versionFeedback.AddThemeColorOverride("font_color", UiTokens.DangerText);
return;
}
Rebuild();
return;
}
}
private void AddModRelationships(string modId)
{
AddInfoLine(TranslationServer.Translate("mod_manager/collection_role_hint"));
foreach (var edge in _graph.Edges)
{
if (string.Equals(edge.FromModId, modId, StringComparison.OrdinalIgnoreCase))
{
var consumer = LookupModTitle(edge.ToModId);
AddInfoLine(string.Format(TranslationServer.Translate("mod_manager/collection_used_by"), consumer));
}
}
foreach (var collection in _collections)
{
if (string.Equals(collection.Id, _currentId, StringComparison.OrdinalIgnoreCase)
|| collection.Kind != ModCollectionKind.Game)
continue;
var effective = EffectiveCollection(collection.Id);
foreach (var mechanic in effective?.Mechanics ?? new List<ModCollectionMechanic>())
{
foreach (var raw in mechanic.Mods ?? new List<string>())
{
if (!string.Equals(ParseModId(raw), modId, StringComparison.OrdinalIgnoreCase))
continue;
AddInfoLine(string.Format(TranslationServer.Translate("mod_manager/collection_other_role"),
CollectionText.Resolve(effective.GetLocalizedName(TranslationServer.GetLocale())),
CollectionText.Resolve(mechanic.GetLocalizedName(TranslationServer.GetLocale()))));
break;
}
}
}
}
private string LookupModTitle(string modId)
{
foreach (var node in _graph.Nodes)
{
if (string.Equals(node.ModId, modId, StringComparison.OrdinalIgnoreCase))
return node.Title;
}
return modId;
}
private void BindSceneNodes() private void BindSceneNodes()
{ {
_close = GetNode<Button>("Margin/Root/Header/Close"); _close = GetNode<Button>("Margin/Root/Header/Close");
@@ -170,7 +431,7 @@ namespace Mods.ModManager
_addMechanic = GetNode<Button>("Margin/Root/Header/AddMechanic"); _addMechanic = GetNode<Button>("Margin/Root/Header/AddMechanic");
_newGame = GetNode<Button>("Margin/Root/Header/NewGame"); _newGame = GetNode<Button>("Margin/Root/Header/NewGame");
_gameSelect = GetNode<OptionButton>("Margin/Root/Header/GameSelect"); _gameSelect = GetNode<OptionButton>("Margin/Root/Header/GameSelect");
_edgesToggle = GetNode<CheckBox>("Margin/Root/Header/EdgesToggle"); _edgesToggle = GetNode<OptionButton>("Margin/Root/Header/EdgesToggle");
_pillProblems = GetNode<Button>("Margin/Root/Header/Problems"); _pillProblems = GetNode<Button>("Margin/Root/Header/Problems");
_pillPlan = GetNode<Button>("Margin/Root/Header/Plan"); _pillPlan = GetNode<Button>("Margin/Root/Header/Plan");
_modsCount = GetNode<Label>("Margin/Root/Header/ModsCount"); _modsCount = GetNode<Label>("Margin/Root/Header/ModsCount");
@@ -180,7 +441,7 @@ namespace Mods.ModManager
_save = GetNode<Button>("Margin/Root/Header/Save"); _save = GetNode<Button>("Margin/Root/Header/Save");
_bannerPanel = GetNode<PanelContainer>("Margin/Root/Banner"); _bannerPanel = GetNode<PanelContainer>("Margin/Root/Banner");
_bannerLabel = GetNode<Label>("Margin/Root/Banner/BannerLabel"); _bannerLabel = GetNode<Label>("Margin/Root/Banner/BannerLabel");
_canvas = GetNode<CollectionSchemeCanvas>("Margin/Root/Body/CanvasScroll/SchemeCanvas"); _canvas = GetNode<CollectionSchemeCanvas>("Margin/Root/Body/CanvasHost/CanvasScroll/SchemeCanvas");
var detail = "Margin/Root/Body/Details/DetailScroll/Detail/"; var detail = "Margin/Root/Body/Details/DetailScroll/Detail/";
_detailTitle = GetNode<Label>(detail + "DetailTitle"); _detailTitle = GetNode<Label>(detail + "DetailTitle");
@@ -199,10 +460,6 @@ namespace Mods.ModManager
_newGame.Text = TranslationServer.Translate("mod_manager/collection_new_game"); _newGame.Text = TranslationServer.Translate("mod_manager/collection_new_game");
_pillProblems.Text = TranslationServer.Translate("mod_manager/collection_problems"); _pillProblems.Text = TranslationServer.Translate("mod_manager/collection_problems");
_pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan"); _pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan");
_close.TooltipText = _close.Text;
_refresh.TooltipText = _refresh.Text;
_addMechanic.TooltipText = _addMechanic.Text;
_newGame.TooltipText = _newGame.Text;
_gameSelect.TooltipText = TranslationServer.Translate("mod_manager/collection_game_tooltip"); _gameSelect.TooltipText = TranslationServer.Translate("mod_manager/collection_game_tooltip");
_pillProblems.TooltipText = TranslationServer.Translate("mod_manager/collection_problems_tooltip"); _pillProblems.TooltipText = TranslationServer.Translate("mod_manager/collection_problems_tooltip");
_pillPlan.TooltipText = TranslationServer.Translate("mod_manager/collection_plan_tooltip"); _pillPlan.TooltipText = TranslationServer.Translate("mod_manager/collection_plan_tooltip");
@@ -215,21 +472,34 @@ namespace Mods.ModManager
_tabPlan.Text = TranslationServer.Translate("mod_manager/collection_tab_plan"); _tabPlan.Text = TranslationServer.Translate("mod_manager/collection_tab_plan");
_json.TooltipText = TranslationServer.Translate("mod_manager/collection_json_tooltip"); _json.TooltipText = TranslationServer.Translate("mod_manager/collection_json_tooltip");
_reset.TooltipText = TranslationServer.Translate("mod_manager/collection_reset_tooltip"); _reset.TooltipText = TranslationServer.Translate("mod_manager/collection_reset_tooltip");
_save.TooltipText = _save.Text;
_tabInspector.TooltipText = _tabInspector.Text;
_tabProblems.TooltipText = _tabProblems.Text;
_tabPlan.TooltipText = _tabPlan.Text;
_apply.Text = TranslationServer.Translate("mod_manager/collection_apply"); _apply.Text = TranslationServer.Translate("mod_manager/collection_apply");
_edgesToggle.Text = TranslationServer.Translate("mod_manager/collection_links"); _edgesToggle.AddItem(TranslationServer.Translate("mod_manager/collection_links_selected"));
_apply.TooltipText = _apply.Text; _edgesToggle.AddItem(TranslationServer.Translate("mod_manager/collection_links_all"));
_edgesToggle.TooltipText = _edgesToggle.Text; _edgesToggle.AddItem(TranslationServer.Translate("mod_manager/collection_links_none"));
var orderLegend = GetNode<Label>("Margin/Root/OrderLegend");
orderLegend.Text = TranslationServer.Translate("mod_manager/collection_order_legend");
orderLegend.AddThemeColorOverride("font_color", UiTokens.Faint);
orderLegend.SetMeta(ModHelpPanel.HintTranslationMetadata, "collection_dependencies");
ModHelpPanel.Attach(_refresh, "refresh");
ModHelpPanel.Attach(_addMechanic, "collection_add_mechanic");
ModHelpPanel.Attach(_newGame, "collection_new");
ModHelpPanel.Attach(_gameSelect, "collection_game");
ModHelpPanel.Attach(_pillProblems, "collection_problems");
ModHelpPanel.Attach(_pillPlan, "collection_plan");
ModHelpPanel.Attach(_json, "collection_json");
ModHelpPanel.Attach(_reset, "collection_reset");
ModHelpPanel.Attach(_apply, "collection_apply");
ModHelpPanel.Attach(_edgesToggle, "collection_links");
ModHelpPanel.Attach(GetNode<Label>("Margin/Root/OrderLegend"), "load_order");
var header = _json.GetParent(); var header = _json.GetParent();
_savesInfo = new Label { VerticalAlignment = VerticalAlignment.Center }; _savesInfo = new Label { VerticalAlignment = VerticalAlignment.Center };
_savesInfo.AddThemeColorOverride("font_color", UiTokens.Muted); _savesInfo.AddThemeColorOverride("font_color", UiTokens.Muted);
_duplicate = new Button { Text = TranslationServer.Translate("mod_manager/collection_duplicate"), TooltipText = TranslationServer.Translate("mod_manager/collection_duplicate") }; _duplicate = new Button { Text = TranslationServer.Translate("mod_manager/collection_duplicate") };
_revert = new Button { Text = TranslationServer.Translate("mod_manager/collection_revert"), TooltipText = TranslationServer.Translate("mod_manager/collection_revert") }; _revert = new Button { Text = TranslationServer.Translate("mod_manager/collection_revert") };
_delete = new Button { Text = TranslationServer.Translate("mod_manager/collection_delete"), TooltipText = TranslationServer.Translate("mod_manager/collection_delete") }; _delete = new Button { Text = TranslationServer.Translate("mod_manager/collection_delete") };
ModHelpPanel.Attach(_delete, "collection_delete");
foreach (var control in new Control[] { _savesInfo, _duplicate, _revert, _delete }) foreach (var control in new Control[] { _savesInfo, _duplicate, _revert, _delete })
{ {
header.AddChild(control); header.AddChild(control);
@@ -252,6 +522,7 @@ namespace Mods.ModManager
_bannerLabel.AddThemeColorOverride("font_color", UiTokens.Warning); _bannerLabel.AddThemeColorOverride("font_color", UiTokens.Warning);
_bannerPanel.Visible = false; _bannerPanel.Visible = false;
BindGraphiteControls();
BuildCrumbs(); BuildCrumbs();
BuildBuildBar(); BuildBuildBar();
if (Embedded) if (Embedded)
@@ -271,7 +542,7 @@ namespace Mods.ModManager
_crumbCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections")); _crumbCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"));
_crumbCollections.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall); _crumbCollections.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
_crumbCollections.Pressed += () => GalleryRequested?.Invoke(); _crumbCollections.Pressed += OnGalleryPressed;
_crumbCollections.TooltipText = TranslationServer.Translate("mod_manager/gallery_crumb"); _crumbCollections.TooltipText = TranslationServer.Translate("mod_manager/gallery_crumb");
_crumbs.AddChild(_crumbCollections); _crumbs.AddChild(_crumbCollections);
_crumbs.AddChild(UiKit.Label("›", UiTokens.FontBodySmall, UiTokens.Faint)); _crumbs.AddChild(UiKit.Label("›", UiTokens.FontBodySmall, UiTokens.Faint));
@@ -281,10 +552,19 @@ namespace Mods.ModManager
_crumbs.AddChild(UiKit.Label(TranslationServer.Translate("mod_manager/collection_crumb_scheme"), UiTokens.FontBodySmall, UiTokens.Body)); _crumbs.AddChild(UiKit.Label(TranslationServer.Translate("mod_manager/collection_crumb_scheme"), UiTokens.FontBodySmall, UiTokens.Body));
_crumbs.AddChild(UiKit.Spacer()); _crumbs.AddChild(UiKit.Spacer());
GetNode("Margin/Root/Header/AddMechanic").Reparent(_crumbs);
GetNode("Margin/Root/Header/EdgesToggle").Reparent(_crumbs);
GetNode("Margin/Root/Header/Problems").Reparent(_crumbs);
GetNode("Margin/Root/Header/ModsCount").Reparent(_crumbs); GetNode("Margin/Root/Header/ModsCount").Reparent(_crumbs);
var header = GetNode<HBoxContainer>("Margin/Root/Header");
_crumbs.Reparent(header);
header.MoveChild(_crumbs, 0);
_crumbs.SizeFlagsHorizontal = SizeFlags.ExpandFill;
GetNode<Control>("Margin/Root/Header/Spacer").Visible = false;
var tools = GetNode<HBoxContainer>("Margin/Root/Tools");
_addMechanic.Reparent(tools);
tools.MoveChild(_addMechanic, 0);
_edgesToggle.Reparent(tools);
tools.MoveChild(_edgesToggle, 2);
_pillProblems.Reparent(tools);
tools.MoveChild(_pillProblems, 3);
} }
/// <summary> /// <summary>
@@ -317,13 +597,13 @@ namespace Mods.ModManager
row.AddChild(_buildHint); row.AddChild(_buildHint);
row.AddChild(UiKit.Spacer()); row.AddChild(UiKit.Spacer());
_buildManager = UiKit.Button(TranslationServer.Translate("mod_manager/build_open_manager")); _buildManager = UiKit.Button(TranslationServer.Translate("mod_manager/collection_tab_problems"));
_buildManager.TooltipText = _buildManager.Text; ModHelpPanel.Attach(_buildManager, "collection_plan");
_buildManager.Pressed += () => OpenManagerRequested?.Invoke(null); _buildManager.Pressed += OnBuildManagerPressed;
row.AddChild(_buildManager); row.AddChild(_buildManager);
_buildApply = UiKit.Button(TranslationServer.Translate("mod_manager/collection_apply"), ButtonKind.Primary); _buildApply = UiKit.Button(TranslationServer.Translate("mod_manager/collection_apply"), ButtonKind.Primary);
_buildApply.TooltipText = _buildApply.Text; ModHelpPanel.Attach(_buildApply, "collection_apply");
_buildApply.Pressed += OnApplyPressed; _buildApply.Pressed += OnApplyPressed;
row.AddChild(_buildApply); row.AddChild(_buildApply);
} }
@@ -345,6 +625,7 @@ namespace Mods.ModManager
_close.Visible = false; _close.Visible = false;
_refresh.Visible = false; _refresh.Visible = false;
_newGame.Visible = false; _newGame.Visible = false;
_pillPlan.Visible = false;
} }
private void SubscribeEvents() private void SubscribeEvents()
@@ -353,7 +634,7 @@ namespace Mods.ModManager
_refresh.Pressed += OnRefreshPressed; _refresh.Pressed += OnRefreshPressed;
_addMechanic.Pressed += OnAddMechanicPressed; _addMechanic.Pressed += OnAddMechanicPressed;
_newGame.Pressed += OnNewGamePressed; _newGame.Pressed += OnNewGamePressed;
_edgesToggle.Toggled += _canvas.SetEdgesVisible; _edgesToggle.ItemSelected += _canvas.SetEdgeMode;
_pillProblems.Pressed += () => SelectTab("problems"); _pillProblems.Pressed += () => SelectTab("problems");
_pillPlan.Pressed += () => SelectTab("plan"); _pillPlan.Pressed += () => SelectTab("plan");
_json.Pressed += OpenJsonDialog; _json.Pressed += OpenJsonDialog;
@@ -369,6 +650,7 @@ namespace Mods.ModManager
_canvas.ModSelected += OnModSelected; _canvas.ModSelected += OnModSelected;
_canvas.GroupCommand += OnGroupCommand; _canvas.GroupCommand += OnGroupCommand;
_canvas.LayoutChanged += OnLayoutChanged; _canvas.LayoutChanged += OnLayoutChanged;
_canvas.ViewChanged += OnCanvasViewChanged;
ModDownloader.OnProgress += OnInstallProgress; ModDownloader.OnProgress += OnInstallProgress;
CatalogService.OnRefreshed += OnCatalogRefreshed; CatalogService.OnRefreshed += OnCatalogRefreshed;
@@ -378,6 +660,8 @@ namespace Mods.ModManager
{ {
ModDownloader.OnProgress -= OnInstallProgress; ModDownloader.OnProgress -= OnInstallProgress;
CatalogService.OnRefreshed -= OnCatalogRefreshed; CatalogService.OnRefreshed -= OnCatalogRefreshed;
if (_fitQueued)
GetTree().ProcessFrame -= OnCanvasFirstFrame;
} }
/* ═══════════════ Data flow ═══════════════ */ /* ═══════════════ Data flow ═══════════════ */
@@ -396,12 +680,37 @@ namespace Mods.ModManager
private void Rebuild() private void Rebuild()
{ {
_graph = Resolve(); _graph = Resolve();
foreach (var node in _graph.Nodes)
{
if (ModRegistry.Instance.AllDiscoveredMods.TryGetValue(node.ModId, out var info))
node.Title = info.DisplayTitle;
}
_layout = _currentId == null ? new ModCollectionLayout() : CollectionLayoutStore.Load(_currentId); _layout = _currentId == null ? new ModCollectionLayout() : CollectionLayoutStore.Load(_currentId);
_plan = ModCollectionPlan.Build(_graph, CatalogService.CachedEntries ?? new List<CatalogExportEntry>()); _plan = ModCollectionPlan.Build(_graph, CatalogService.CachedEntries ?? new List<CatalogExportEntry>());
if (_selectedGroupKey == null && _graph.Groups.Count > 0)
{
var initial = _graph.Groups[0];
foreach (var group in _graph.Groups)
{
if (group.Role == "battle" && group.Selected && group.Mods.Count > 0)
{
initial = group;
break;
}
}
_selectedGroupKey = initial.Key;
_selectedModId = initial.Mods.Count > 0 ? initial.Mods[0].ModId : null;
}
_canvas.Build(_graph, _layout); _canvas.Build(_graph, _layout);
_canvas.SetSelection(_selectedGroupKey, _selectedModId); _canvas.SetSelection(_selectedGroupKey, _selectedModId);
_canvas.SetEdgesVisible(_edgesToggle.ButtonPressed); _canvas.SetEdgeMode(_edgesToggle.Selected);
if (_fitAfterBuild && !_fitQueued)
{
_fitQueued = true;
GetTree().ProcessFrame += OnCanvasFirstFrame;
}
UpdateHeader(); UpdateHeader();
UpdateInspector(); UpdateInspector();
@@ -530,7 +839,7 @@ namespace Mods.ModManager
var current = EffectiveCollection(_currentId); var current = EffectiveCollection(_currentId);
var isGame = current?.Kind == ModCollectionKind.Game; var isGame = current?.Kind == ModCollectionKind.Game;
_savesInfo.Visible = isGame; _savesInfo.Visible = isGame && !Embedded;
if (isGame) if (isGame)
_savesInfo.Text = string.Format(TranslationServer.Translate("mod_manager/collection_saves"), SaveService.ListSaves(_currentId).Count); _savesInfo.Text = string.Format(TranslationServer.Translate("mod_manager/collection_saves"), SaveService.ListSaves(_currentId).Count);
_duplicate.Disabled = current == null; _duplicate.Disabled = current == null;
@@ -559,6 +868,11 @@ namespace Mods.ModManager
_pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan"); _pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan");
return; return;
} }
if (errors > 0)
{
_pillPlan.Text = $"{TranslationServer.Translate("mod_manager/collection_problems")}: {errors}";
return;
}
var parts = new List<string>(); var parts = new List<string>();
if (_plan.Install.Count > 0) if (_plan.Install.Count > 0)
@@ -571,7 +885,7 @@ namespace Mods.ModManager
} }
private int CountErrors() private int CountErrors()
=> _graph?.Problems?.Count ?? 0; => (_graph?.Problems?.Count ?? 0) + (_plan?.Missing.Count ?? 0) + (_plan?.Mismatches.Count ?? 0);
private void OnGameSelected(long index) private void OnGameSelected(long index)
{ {
@@ -580,6 +894,7 @@ namespace Mods.ModManager
return; return;
_currentId = id; _currentId = id;
_fitAfterBuild = true;
_selectedGroupKey = null; _selectedGroupKey = null;
_selectedModId = null; _selectedModId = null;
Rebuild(); Rebuild();
@@ -587,6 +902,7 @@ namespace Mods.ModManager
private void OnNewGamePressed() private void OnNewGamePressed()
{ {
_fitAfterBuild = true;
var id = UniqueGameId(); var id = UniqueGameId();
var draft = new ModCollection var draft = new ModCollection
{ {
@@ -753,6 +1069,8 @@ namespace Mods.ModManager
_tabPlan.ButtonPressed = _tab == "plan"; _tabPlan.ButtonPressed = _tab == "plan";
ClearChildren(_detailExtra); ClearChildren(_detailExtra);
_versionEditor.Visible = false;
_versionReference = null;
_apply.Visible = !Embedded && _tab == "plan"; _apply.Visible = !Embedded && _tab == "plan";
if (_tab == "problems") if (_tab == "problems")
@@ -801,6 +1119,7 @@ namespace Mods.ModManager
_detailMeta.Text = string.Join(" · ", metaParts); _detailMeta.Text = string.Join(" · ", metaParts);
_detailDescription.Text = string.IsNullOrEmpty(group.Description) ? "—" : group.Description; _detailDescription.Text = string.IsNullOrEmpty(group.Description) ? "—" : group.Description;
AddInfoLine(TranslationServer.Translate("mod_manager/collection_role_hint"));
var collection = EffectiveCollection(group.CollectionId); var collection = EffectiveCollection(group.CollectionId);
if (collection != null) if (collection != null)
@@ -836,18 +1155,23 @@ namespace Mods.ModManager
{ {
var node = _graph?.Nodes?.FirstOrDefault(n => string.Equals(n.ModId, modId, StringComparison.OrdinalIgnoreCase)); var node = _graph?.Nodes?.FirstOrDefault(n => string.Equals(n.ModId, modId, StringComparison.OrdinalIgnoreCase));
_detailTitle.Text = node?.Title ?? modId; _detailTitle.Text = node?.Title ?? modId;
_detailMeta.Text = $"{modId} · {CollectionStateNames.Translate(node?.State ?? ModCollectionNodeState.Missing)}"; _detailMeta.Text = $"{modId} · {node?.InstalledVersion ?? "—"} · {CollectionStateNames.Translate(node?.State ?? ModCollectionNodeState.Missing)}";
_detailDescription.Text = LookupModDescription(modId); _detailDescription.Text = LookupModDescription(modId);
if (node != null && !string.IsNullOrEmpty(node.RequiredVersion))
AddInfoLine($"{TranslationServer.Translate("mod_manager/collection_requires")}: {node.RequiredVersion}");
if (node != null && !string.IsNullOrEmpty(node.InstalledVersion))
AddInfoLine($"{TranslationServer.Translate("mod_manager/installed_version")}: {node.InstalledVersion}");
var group = _selectedGroupKey == null ? null : FindGroup(_selectedGroupKey); var group = _selectedGroupKey == null ? null : FindGroup(_selectedGroupKey);
if (group == null && node != null)
{
foreach (var requirement in node.Requirements)
{
if (!string.IsNullOrEmpty(requirement.Operator))
AddInfoLine(string.Format(TranslationServer.Translate("mod_manager/collection_requires"), requirement.ToString()));
}
}
if (group != null) if (group != null)
{ {
AddInfoLine($"{TranslationServer.Translate("mod_manager/collection_in_group")}: {group.Name}"); AddInfoLine($"{TranslationServer.Translate("mod_manager/collection_in_group")}: {group.Name}");
BindVersionRule(group, modId);
AddModRelationships(modId);
var remove = MakeInspectorButton(TranslationServer.Translate("mod_manager/collection_remove_mod"), var remove = MakeInspectorButton(TranslationServer.Translate("mod_manager/collection_remove_mod"),
() => RemoveModFromGroup(group, modId), true); () => RemoveModFromGroup(group, modId), true);
@@ -888,13 +1212,16 @@ namespace Mods.ModManager
_detailMeta.Text = string.Empty; _detailMeta.Text = string.Empty;
_detailDescription.Text = string.Empty; _detailDescription.Text = string.Empty;
if (_graph == null || _graph.Problems.Count == 0) if (_graph == null || CountErrors() == 0)
{ {
_detailExtra.AddChild(UiKit.Label(TranslationServer.Translate("mod_manager/collection_problems_none"), UiTokens.FontBody, UiTokens.SuccessText)); _detailExtra.AddChild(UiKit.Label(TranslationServer.Translate("mod_manager/collection_problems_none"), UiTokens.FontBody, UiTokens.SuccessText));
UpdateApplyButton(); UpdateApplyButton();
return; return;
} }
AddPlanSection("mod_manager/collection_missing", _plan?.Missing, UiTokens.DangerText, findInCatalog: true);
AddPlanSection("mod_manager/collection_mismatch", _plan?.Mismatches, UiTokens.DangerText, findInCatalog: true);
foreach (var problem in _graph.Problems) foreach (var problem in _graph.Problems)
{ {
var row = UiKit.Column(UiTokens.Space1); var row = UiKit.Column(UiTokens.Space1);
@@ -975,7 +1302,7 @@ namespace Mods.ModManager
AddPlanSection("mod_manager/collection_install_count", _plan.Install.Select(i => i.ModId), UiTokens.Info); AddPlanSection("mod_manager/collection_install_count", _plan.Install.Select(i => i.ModId), UiTokens.Info);
AddPlanSection("mod_manager/collection_enable_count", _plan.Enable, UiTokens.Warning); AddPlanSection("mod_manager/collection_enable_count", _plan.Enable, UiTokens.Warning);
AddPlanSection("mod_manager/collection_missing", _plan.Missing, UiTokens.Danger, findInCatalog: true); AddPlanSection("mod_manager/collection_missing", _plan.Missing, UiTokens.Danger, findInCatalog: true);
AddPlanSection("mod_manager/collection_mismatch", _plan.Mismatches, UiTokens.Warning); AddPlanSection("mod_manager/collection_mismatch", _plan.Mismatches, UiTokens.Warning, findInCatalog: true);
UpdateApplyButton(); UpdateApplyButton();
} }
@@ -1013,7 +1340,7 @@ namespace Mods.ModManager
var canApply = !_installing var canApply = !_installing
&& _plan != null && _plan != null
&& !_plan.IsEmpty && !_plan.IsEmpty
&& _plan.Missing.Count == 0 && !_plan.HasProblems
&& CountErrors() == 0; && CountErrors() == 0;
_apply.Disabled = !canApply; _apply.Disabled = !canApply;
if (_buildApply != null) if (_buildApply != null)
@@ -1048,7 +1375,7 @@ namespace Mods.ModManager
_buildHint.Text = TranslationServer.Translate(blocked _buildHint.Text = TranslationServer.Translate(blocked
? "mod_manager/build_hint_problems" ? "mod_manager/build_hint_problems"
: "mod_manager/collection_plan_ready"); : _overrides.Count > 0 ? "mod_manager/collection_unsaved" : "mod_manager/collection_plan_ready");
_buildHint.AddThemeColorOverride("font_color", blocked ? UiTokens.Warning : UiTokens.SuccessText); _buildHint.AddThemeColorOverride("font_color", blocked ? UiTokens.Warning : UiTokens.SuccessText);
} }
@@ -1095,7 +1422,6 @@ namespace Mods.ModManager
private Button MakeInspectorButton(string text, Action action, bool danger = false) private Button MakeInspectorButton(string text, Action action, bool danger = false)
{ {
var button = UiKit.Button(text, danger ? ButtonKind.Danger : ButtonKind.Secondary); var button = UiKit.Button(text, danger ? ButtonKind.Danger : ButtonKind.Secondary);
button.TooltipText = text;
button.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall); button.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
button.Pressed += action; button.Pressed += action;
return button; return button;
@@ -1606,12 +1932,17 @@ namespace Mods.ModManager
if (collection == null) if (collection == null)
return; return;
var view = new TextEdit var view = new CodeEdit
{ {
Text = JsonSerializer.Serialize(collection, JsonOptionsProvider.WriteIndentedOptions), Text = JsonSerializer.Serialize(collection, JsonOptionsProvider.WriteIndentedOptions),
Editable = true, Editable = true,
GuttersDrawLineNumbers = true,
SyntaxHighlighter = SourceSyntax.CreateJsonHighlighter(),
IndentUseSpaces = true,
IndentSize = 2,
CustomMinimumSize = new Vector2(620, 420), CustomMinimumSize = new Vector2(620, 420),
}; };
view.AddThemeFontOverride("font", ToolWindow.GetMonospaceFont());
var dialog = new ConfirmationDialog var dialog = new ConfirmationDialog
{ {
@@ -1798,6 +2129,8 @@ namespace Mods.ModManager
} }
include.Ref = forkId; include.Ref = forkId;
if (string.Equals(_selectedGroupKey, group.Key, StringComparison.OrdinalIgnoreCase))
_selectedGroupKey = $"i:{group.ParentCollectionId}:{forkId}";
return Draft(forkId); return Draft(forkId);
} }
@@ -1849,7 +2182,7 @@ namespace Mods.ModManager
private async void OnApplyPressed() private async void OnApplyPressed()
{ {
if (_installing || _plan == null || _plan.IsEmpty if (_installing || _plan == null || _plan.IsEmpty
|| _plan.Missing.Count > 0 || CountErrors() > 0) || _plan.HasProblems || CountErrors() > 0)
return; return;
var plan = _plan; var plan = _plan;
+27 -23
View File
@@ -103,14 +103,14 @@ namespace Mods.ModManager
_tabMods.ToggleMode = true; _tabMods.ToggleMode = true;
_tabMods.ButtonGroup = group; _tabMods.ButtonGroup = group;
_tabMods.Pressed += () => ShowTab(TabMods); _tabMods.Pressed += () => ShowTab(TabMods);
_tabMods.TooltipText = _tabMods.Text; ModHelpPanel.Attach(_tabMods, "mods_tab");
_segment.AddChild(_tabMods); _segment.AddChild(_tabMods);
_tabCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"), ButtonKind.Tab); _tabCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"), ButtonKind.Tab);
_tabCollections.ToggleMode = true; _tabCollections.ToggleMode = true;
_tabCollections.ButtonGroup = group; _tabCollections.ButtonGroup = group;
_tabCollections.Pressed += () => ShowTab(TabCollections); _tabCollections.Pressed += () => ShowTab(TabCollections);
_tabCollections.TooltipText = _tabCollections.Text; ModHelpPanel.Attach(_tabCollections, "collections_tab");
_segment.AddChild(_tabCollections); _segment.AddChild(_tabCollections);
header.AddChild(UiKit.Spacer()); header.AddChild(UiKit.Spacer());
@@ -122,24 +122,22 @@ namespace Mods.ModManager
SizeFlagsVertical = Control.SizeFlags.ShrinkCenter, SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
}; };
_gallerySearch.TextChanged += OnGallerySearchChanged; _gallerySearch.TextChanged += OnGallerySearchChanged;
_gallerySearch.TooltipText = _gallerySearch.PlaceholderText; ModHelpPanel.Attach(_gallerySearch, "collection_search");
header.AddChild(_gallerySearch); header.AddChild(_gallerySearch);
_galleryRefresh = UiKit.Button(TranslationServer.Translate("mod_manager/refresh")); _galleryRefresh = UiKit.Button(TranslationServer.Translate("mod_manager/refresh"));
_galleryRefresh.Pressed += OnGalleryRefreshPressed; _galleryRefresh.Pressed += OnGalleryRefreshPressed;
_galleryRefresh.TooltipText = _galleryRefresh.Text; ModHelpPanel.Attach(_galleryRefresh, "refresh");
header.AddChild(_galleryRefresh); header.AddChild(_galleryRefresh);
_helpButton = UiKit.IconButton("?", TranslationServer.Translate("mod_manager/help_open")); _helpButton = UiKit.IconButton("?", TranslationServer.Translate("mod_manager/help_open"));
_helpButton.Name = "HelpButton"; _helpButton.Name = "HelpButton";
_helpButton.ToggleMode = true;
_helpButton.Pressed += OnHelpPressed; _helpButton.Pressed += OnHelpPressed;
header.AddChild(_helpButton); header.AddChild(_helpButton);
} }
/// <summary> /// <summary>Builds the non-modal inspection layer above every hub level.</summary>
/// Builds the full-screen help overlay once; it stays hidden until the
/// header's "?" button opens the page of the level the player is on.
/// </summary>
private void BuildHelp() private void BuildHelp()
{ {
_help = TscnScenes.LoadAndBuild(HelpScene) as ModHelpPanel; _help = TscnScenes.LoadAndBuild(HelpScene) as ModHelpPanel;
@@ -157,19 +155,14 @@ namespace Mods.ModManager
private void OnHelpPressed() private void OnHelpPressed()
{ {
_help?.Open(CurrentHelpLevel()); if (_help == null)
} return;
/// <summary>The help page follows the hub level: the mods grid, an open mod page, the collections gallery or the scheme editor.</summary> if (_help.Visible)
private string CurrentHelpLevel() _help.Close();
{ else
if (_editorOpen) _help.Open();
return "scheme"; _helpButton.ButtonPressed = _help.Visible;
if (_activeTab == TabCollections)
return "collections";
if (ModOpen)
return "mod_page";
return "mods";
} }
private void BuildBanner() private void BuildBanner()
@@ -265,7 +258,6 @@ namespace Mods.ModManager
if (_editorOpen && _editor != null) if (_editorOpen && _editor != null)
{ {
_back.Text = TranslationServer.Translate("mod_manager/hub_back_to_gallery"); _back.Text = TranslationServer.Translate("mod_manager/hub_back_to_gallery");
_back.TooltipText = _back.Text;
_title.Text = _editor.CurrentCollectionTitle ?? TranslationServer.Translate("mod_manager/collections_title"); _title.Text = _editor.CurrentCollectionTitle ?? TranslationServer.Translate("mod_manager/collections_title");
_segment.Visible = false; _segment.Visible = false;
_gallerySearch.Visible = false; _gallerySearch.Visible = false;
@@ -274,7 +266,6 @@ namespace Mods.ModManager
} }
_back.Text = TranslationServer.Translate(ModOpen ? "mod_manager/hub_back_to_mods" : "mod_manager/hub_back_to_menu"); _back.Text = TranslationServer.Translate(ModOpen ? "mod_manager/hub_back_to_mods" : "mod_manager/hub_back_to_menu");
_back.TooltipText = _back.Text;
_title.Text = TranslationServer.Translate("mod_manager/menu_title"); _title.Text = TranslationServer.Translate("mod_manager/menu_title");
_segment.Visible = true; _segment.Visible = true;
_tabMods.ButtonPressed = _activeTab == TabMods; _tabMods.ButtonPressed = _activeTab == TabMods;
@@ -406,15 +397,28 @@ namespace Mods.ModManager
/* ═══════════════ Escape chain ═══════════════ */ /* ═══════════════ Escape chain ═══════════════ */
/// <summary>Escape walks back one level: editor → gallery → mods tab, mod card → mods grid.</summary> /// <summary>F1 explains the focused control; Escape leaves help before walking back a level.</summary>
public override void _UnhandledInput(InputEvent @event) public override void _UnhandledInput(InputEvent @event)
{ {
if (@event is InputEventKey key && key.Pressed && !key.Echo && key.Keycode == Key.F1)
{
if (_help != null)
{
_help.Open();
_help.ShowFor(GetViewport().GuiGetFocusOwner());
_helpButton.ButtonPressed = true;
}
GetViewport().SetInputAsHandled();
return;
}
if (!@event.IsActionPressed("ui_cancel")) if (!@event.IsActionPressed("ui_cancel"))
return; return;
if (_help != null && _help.Visible) if (_help != null && _help.Visible)
{ {
_help.Close(); _help.Close();
_helpButton.ButtonPressed = false;
GetViewport().SetInputAsHandled(); GetViewport().SetInputAsHandled();
return; return;
} }
+3 -3
View File
@@ -117,7 +117,6 @@ namespace Mods.ModManager
_bannerLabel = GetNode<Label>("Margin/Root/Banner/BannerLabel"); _bannerLabel = GetNode<Label>("Margin/Root/Banner/BannerLabel");
close.Text = TranslationServer.Translate("mod_manager/back"); close.Text = TranslationServer.Translate("mod_manager/back");
close.TooltipText = close.Text;
title.Text = TranslationServer.Translate("mod_manager/title"); title.Text = TranslationServer.Translate("mod_manager/title");
if (Embedded) if (Embedded)
{ {
@@ -127,9 +126,7 @@ namespace Mods.ModManager
title.Visible = false; title.Visible = false;
} }
_search.PlaceholderText = TranslationServer.Translate("mod_manager/search_placeholder"); _search.PlaceholderText = TranslationServer.Translate("mod_manager/search_placeholder");
_search.TooltipText = _search.PlaceholderText;
_refreshButton.Text = TranslationServer.Translate("mod_manager/refresh"); _refreshButton.Text = TranslationServer.Translate("mod_manager/refresh");
_refreshButton.TooltipText = _refreshButton.Text;
_resultLabel.AddThemeColorOverride("font_color", UiTokens.Muted); _resultLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
_sort.AddItem(TranslationServer.Translate("mod_manager/sort_name")); _sort.AddItem(TranslationServer.Translate("mod_manager/sort_name"));
@@ -137,6 +134,9 @@ namespace Mods.ModManager
_sort.AddItem(TranslationServer.Translate("mod_manager/sort_status")); _sort.AddItem(TranslationServer.Translate("mod_manager/sort_status"));
_sort.Selected = (int)_filter.Sort; _sort.Selected = (int)_filter.Sort;
_sort.TooltipText = TranslationServer.Translate("mod_manager/sort_tooltip"); _sort.TooltipText = TranslationServer.Translate("mod_manager/sort_tooltip");
ModHelpPanel.Attach(_search, "mod_search");
ModHelpPanel.Attach(_sort, "mod_sort");
ModHelpPanel.Attach(_refreshButton, "refresh");
_bannerPanel.AddThemeStyleboxOverride("panel", ModManagerTheme.MakeBox( _bannerPanel.AddThemeStyleboxOverride("panel", ModManagerTheme.MakeBox(
UiTokens.Alpha(UiTokens.Warning, 0.15f), UiTokens.Alpha(UiTokens.Warning, 0.15f),
+6 -4
View File
@@ -9,10 +9,9 @@ namespace Mods.ModManager
/// live global rectangles, so dragging a chip or a whole group keeps the /// live global rectangles, so dragging a chip or a whole group keeps the
/// lines attached without recomputing them. /// lines attached without recomputing them.
/// <para/> /// <para/>
/// <see cref="Cross"/> is the "dangerous layout" flag: the dependency's two /// A dependency may cross group boundaries normally: groups describe its
/// mods live in different mechanics, which means the player separated what /// purpose in one game, not compatibility. Only <see cref="Problem"/> carries
/// belongs together. Cross edges are painted amber (red when the selection /// an actual availability/version failure and receives warning styling.
/// touches them) so the mistake is visible across the whole canvas.
/// </summary> /// </summary>
public class CollectionEdge public class CollectionEdge
{ {
@@ -25,6 +24,9 @@ namespace Mods.ModManager
/// <summary>True when the two mods belong to different groups.</summary> /// <summary>True when the two mods belong to different groups.</summary>
public bool Cross { get; set; } public bool Cross { get; set; }
/// <summary>At least one endpoint is missing or has an incompatible installed version.</summary>
public bool Problem { get; set; }
/// <summary>True when the selection touches one of the endpoints.</summary> /// <summary>True when the selection touches one of the endpoints.</summary>
public bool Active { get; set; } public bool Active { get; set; }
} }
+23 -6
View File
@@ -60,7 +60,7 @@ namespace Mods.ModManager
if (_canvas == null || !GodotObject.IsInstanceValid(_canvas)) if (_canvas == null || !GodotObject.IsInstanceValid(_canvas))
return; return;
var origin = _canvas.GetGlobalRect().Position; var inverse = _canvas.GetGlobalTransform().AffineInverse();
foreach (var edge in _edges) foreach (var edge in _edges)
{ {
@@ -69,13 +69,16 @@ namespace Mods.ModManager
if (!_chips.TryGetValue(edge.ToModId, out var to) || !GodotObject.IsInstanceValid(to)) if (!_chips.TryGetValue(edge.ToModId, out var to) || !GodotObject.IsInstanceValid(to))
continue; continue;
var a = from.GetGlobalRect().GetCenter() - origin; var fromRect = from.GetGlobalRect();
var b = to.GetGlobalRect().GetCenter() - origin; var toRect = to.GetGlobalRect();
var a = inverse * BorderPoint(fromRect, toRect.GetCenter());
var b = inverse * BorderPoint(toRect, fromRect.GetCenter());
var points = BuildCurve(a, b); var points = BuildCurve(a, b);
var color = edge.Cross ? (edge.Active ? UiTokens.Danger : UiTokens.Warning) : (edge.Active ? UiTokens.Accent : UiTokens.LineStrong); var color = edge.Problem ? UiTokens.DangerText
var width = edge.Cross ? (edge.Active ? 3f : 2f) : (edge.Active ? 2.5f : 1.5f); : edge.Active ? UiTokens.Accent : new Color(UiTokens.LineStrong, 0.45f);
var width = edge.Active || edge.Problem ? 2f : 1.2f;
if (edge.Cross) if (edge.Problem)
{ {
for (var i = 0; i < points.Count - 1; i++) for (var i = 0; i < points.Count - 1; i++)
DrawDashedLine(points[i], points[i + 1], color, width, 5f); DrawDashedLine(points[i], points[i + 1], color, width, 5f);
@@ -85,9 +88,23 @@ namespace Mods.ModManager
{ {
DrawPolyline(points.ToArray(), color, width, true); DrawPolyline(points.ToArray(), color, width, true);
} }
DrawCircle(a, 2.5f, color);
var direction = (points[^1] - points[^2]).Normalized();
var normal = new Vector2(-direction.Y, direction.X);
DrawLine(b, b - direction * 8f + normal * 3.5f, color, width, true);
DrawLine(b, b - direction * 8f - normal * 3.5f, color, width, true);
} }
} }
private static Vector2 BorderPoint(Rect2 rect, Vector2 target)
{
var center = rect.GetCenter();
var delta = target - center;
if (Mathf.Abs(delta.X) >= Mathf.Abs(delta.Y))
return new Vector2(delta.X >= 0 ? rect.End.X : rect.Position.X, center.Y);
return new Vector2(center.X, delta.Y >= 0 ? rect.End.Y : rect.Position.Y);
}
private static List<Vector2> BuildCurve(Vector2 a, Vector2 b) private static List<Vector2> BuildCurve(Vector2 a, Vector2 b)
{ {
var dx = b.X - a.X; var dx = b.X - a.X;
+2 -2
View File
@@ -158,7 +158,7 @@ namespace Mods.ModManager
MouseFilter = MouseFilterEnum.Ignore; MouseFilter = MouseFilterEnum.Ignore;
_baseBorder = UiTokens.LineCard; _baseBorder = UiTokens.LineCard;
_style = UiKit.Frame(12, new Color(UiTokens.Surface1, 0.92f), _baseBorder); _style = UiKit.Frame(UiTokens.RadiusLg, new Color(UiTokens.Surface1, 0.92f), _baseBorder);
_panel = new PanelContainer { MouseFilter = MouseFilterEnum.Stop }; _panel = new PanelContainer { MouseFilter = MouseFilterEnum.Stop };
_panel.AddThemeStyleboxOverride("panel", _style); _panel.AddThemeStyleboxOverride("panel", _style);
_panel.GuiInput += OnPanelGuiInput; _panel.GuiInput += OnPanelGuiInput;
@@ -217,7 +217,7 @@ namespace Mods.ModManager
_desc = UiKit.Label(string.Empty, UiTokens.FontSmall, UiTokens.Muted); _desc = UiKit.Label(string.Empty, UiTokens.FontSmall, UiTokens.Muted);
_desc.AutowrapMode = TextServer.AutowrapMode.WordSmart; _desc.AutowrapMode = TextServer.AutowrapMode.WordSmart;
_desc.MaxLinesVisible = 3; _desc.MaxLinesVisible = 1;
_content.AddChild(_desc); _content.AddChild(_desc);
_watermark = UiKit.Label(string.Empty, 42, new Color(1f, 1f, 1f, 0.04f)); _watermark = UiKit.Label(string.Empty, 42, new Color(1f, 1f, 1f, 0.04f));
+67
View File
@@ -0,0 +1,67 @@
namespace Mods.ModManager
{
using System;
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
using Godot;
/// <summary>
/// Scene-positioned overview of the occupied collection canvas. Reads the
/// existing logical frames; clicking centers the scroll view without changing
/// composition or persisted layout. Drawing uses the signal-based mod path.
/// </summary>
public partial class CollectionMiniMap : ModWidget
{
private CollectionSchemeCanvas _canvas;
private StyleBoxFlat _style;
/// <summary>Attaches the live canvas; this control owns no copy of its layout.</summary>
public void Bind(CollectionSchemeCanvas canvas)
{
EnsureConstructed();
_canvas = canvas;
QueueRedraw();
}
protected override void Construct()
{
MouseFilter = MouseFilterEnum.Stop;
ClipContents = true;
_style = UiKit.Frame(UiTokens.RadiusSm, UiTokens.Surface2, UiTokens.LineStrong);
GuiInput += OnMapInput;
Connect("draw", Callable.From((Action)DrawMap));
}
private void DrawMap()
{
DrawStyleBox(_style, new Rect2(Vector2.Zero, Size));
if (_canvas == null || !GodotObject.IsInstanceValid(_canvas))
return;
var scale = (Size - Vector2.One * 8f) / _canvas.ContentBounds;
foreach (var pair in _canvas.GroupFrames)
{
var frame = pair.Value;
if (!frame.X.HasValue || !frame.Y.HasValue)
continue;
var rect = new Rect2(new Vector2(frame.X.Value, frame.Y.Value) * scale + Vector2.One * 4f,
new Vector2(frame.W, frame.H) * scale);
DrawRect(rect, UiTokens.Surface3);
DrawRect(rect, pair.Key == _canvas.SelectedGroupKey ? UiTokens.Accent : UiTokens.LineStrong, false);
}
var visible = _canvas.VisibleBounds;
DrawRect(new Rect2(visible.Position * scale + Vector2.One * 4f, visible.Size * scale), UiTokens.Accent, false);
}
private void OnMapInput(InputEvent @event)
{
if (_canvas == null || @event is not InputEventMouseButton button
|| !button.Pressed || button.ButtonIndex != MouseButton.Left)
return;
var scale = (Size - Vector2.One * 8f) / _canvas.ContentBounds;
_canvas.CenterOn((button.Position - Vector2.One * 4f) / scale);
AcceptEvent();
}
}
}
+17 -8
View File
@@ -32,11 +32,14 @@ namespace Mods.ModManager
/// <summary>Canonical mod id this chip represents.</summary> /// <summary>Canonical mod id this chip represents.</summary>
public string ModId { get; private set; } public string ModId { get; private set; }
/// <summary>The displayed occurrence's group; a mod may have another chip in another group.</summary>
public string GroupKey { get; private set; }
/// <summary>Raised on left click; the canvas turns it into a selection.</summary> /// <summary>Raised on left click; the canvas turns it into a selection.</summary>
public event Action<string> Selected; public event Action<CollectionModChip> Selected;
/// <summary>Raised while dragging with the desired position inside the group's mods layer.</summary> /// <summary>Raised while dragging with the desired position inside the group's mods layer.</summary>
public event Action<string, Vector2> Moved; public event Action<CollectionModChip, Vector2> Moved;
/// <summary>Raised when a drag ends, after the last <see cref="Moved"/>.</summary> /// <summary>Raised when a drag ends, after the last <see cref="Moved"/>.</summary>
public event Action DragFinished; public event Action DragFinished;
@@ -57,17 +60,20 @@ namespace Mods.ModManager
/// the chip is added to the tree; safe to call again to refresh the /// the chip is added to the tree; safe to call again to refresh the
/// status without rebuilding the node. /// status without rebuilding the node.
/// </summary> /// </summary>
public void Init(string modId, string role, string title, string version, string stateLabel, Color stateColor, string tooltip) public void Init(string groupKey, string modId, string role, string title, string version, string requirement, string stateLabel, Color stateColor, string tooltip)
{ {
EnsureConstructed(); EnsureConstructed();
ModId = modId; ModId = modId;
GroupKey = groupKey;
_stateColor = stateColor; _stateColor = stateColor;
_role.Text = string.IsNullOrEmpty(role) ? string.Empty : role.ToUpperInvariant(); _role.Text = string.IsNullOrEmpty(role) ? string.Empty : role.ToUpperInvariant();
_name.Text = title; _name.Text = title;
_meta.Text = string.IsNullOrEmpty(version) ? stateLabel : $"{version} · {stateLabel}"; _meta.Text = string.IsNullOrEmpty(version) ? stateLabel
: version + (string.IsNullOrEmpty(requirement) ? string.Empty : " · " + requirement);
_dot.Color = stateColor; _dot.Color = stateColor;
_style.BorderColor = stateColor; _style.BorderColor = stateColor == UiTokens.Danger || stateColor == UiTokens.DangerText || stateColor == UiTokens.Warning
? stateColor : UiTokens.LineCard;
_panel.TooltipText = tooltip ?? string.Empty; _panel.TooltipText = tooltip ?? string.Empty;
} }
@@ -75,7 +81,10 @@ namespace Mods.ModManager
public void SetSelected(bool selected) public void SetSelected(bool selected)
{ {
EnsureConstructed(); EnsureConstructed();
_style.BorderColor = selected ? UiTokens.Accent : _stateColor; _style.BorderColor = selected ? UiTokens.Accent : UiTokens.LineCard;
_style.BgColor = selected ? UiTokens.SurfaceSelected : UiTokens.Surface1;
if (!selected && (_stateColor == UiTokens.Danger || _stateColor == UiTokens.DangerText || _stateColor == UiTokens.Warning))
_style.BorderColor = _stateColor;
} }
protected override void Construct() protected override void Construct()
@@ -136,7 +145,7 @@ namespace Mods.ModManager
{ {
_dragging = true; _dragging = true;
_moved = false; _moved = false;
Selected?.Invoke(ModId); Selected?.Invoke(this);
} }
else else
{ {
@@ -168,7 +177,7 @@ namespace Mods.ModManager
_moved = true; _moved = true;
Position = clamped; Position = clamped;
Moved?.Invoke(ModId, clamped); Moved?.Invoke(this, clamped);
AcceptEvent(); AcceptEvent();
} }
} }
+235 -21
View File
@@ -59,6 +59,9 @@ namespace Mods.ModManager
/// <summary>Raised after any drag or resize ends, so the layout can be flushed to the store.</summary> /// <summary>Raised after any drag or resize ends, so the layout can be flushed to the store.</summary>
public event Action LayoutChanged; public event Action LayoutChanged;
/// <summary>Scale or scroll changed; the toolbar and overview may refresh their presentation.</summary>
public event Action ViewChanged;
/// <summary>Whether dependency lines are painted; toggled from the header.</summary> /// <summary>Whether dependency lines are painted; toggled from the header.</summary>
public bool ShowEdges { get; set; } = true; public bool ShowEdges { get; set; } = true;
@@ -74,6 +77,116 @@ namespace Mods.ModManager
private readonly Dictionary<string, CollectionGroupCard> _cards = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, CollectionGroupCard> _cards = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, CollectionModChip> _chips = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, CollectionModChip> _chips = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _chipGroups = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, string> _chipGroups = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, CollectionModChip> _instances = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ModCollectionLayoutRect> _visibleFrames = new(StringComparer.OrdinalIgnoreCase);
private Control _content;
private CollectionMiniMap _miniMap;
private long _edgeMode;
private float _zoom = 1f;
private string _search = string.Empty;
/// <summary>Scale of the canvas contents, independent of persisted logical coordinates.</summary>
public float Zoom => _zoom;
/// <summary>Occupied logical bounds, excluding the empty scrollable workspace floor.</summary>
public Vector2 ContentBounds { get; private set; } = new(460f, 240f);
/// <summary>Current visible rectangle in logical coordinates, used by the overview map.</summary>
public Rect2 VisibleBounds
{
get
{
var scroll = GetParent<ScrollContainer>();
return scroll == null ? new Rect2() : new Rect2(
new Vector2(scroll.ScrollHorizontal, scroll.ScrollVertical) / _zoom,
scroll.Size / _zoom);
}
}
/// <summary>Overview rectangles use the same logical geometry as the group frames.</summary>
public IReadOnlyDictionary<string, ModCollectionLayoutRect> GroupFrames => _visibleFrames;
/// <summary>Connects the scene-defined overview control after both controls entered the tree.</summary>
public void SetMiniMap(CollectionMiniMap miniMap)
{
_miniMap = miniMap;
_miniMap.Bind(this);
}
/// <summary>0: selected mod only, 1: all dependencies, 2: hidden; group boundaries never change severity.</summary>
public void SetEdgeMode(long mode)
{
_edgeMode = mode;
SetEdgesVisible(mode != 2);
RebuildEdges();
}
/// <summary>Filters presentation only; hidden search matches do not leave the game's composition.</summary>
public void SetSearch(string query)
{
_search = query?.Trim() ?? string.Empty;
foreach (var chip in _instances.Values)
{
var node = FindNode(chip.ModId);
var match = _search.Length == 0 || chip.ModId.Contains(_search, StringComparison.OrdinalIgnoreCase)
|| (node?.Title?.Contains(_search, StringComparison.OrdinalIgnoreCase) ?? false);
chip.Modulate = new Color(1, 1, 1, match ? 1f : 0.2f);
}
}
/// <summary>Increases scale without rewriting any stored group or chip positions.</summary>
public void ZoomIn() => SetZoom(_zoom * 1.15f);
/// <summary>Decreases scale without rewriting any stored group or chip positions.</summary>
public void ZoomOut() => SetZoom(_zoom / 1.15f);
/// <summary>Fits occupied group bounds horizontally; the empty scroll floor does not shrink the scheme.</summary>
public void FitWidth()
{
var scroll = GetParent<ScrollContainer>();
if (scroll == null || scroll.Size.X <= 0f)
return;
SetZoom(Mathf.Min(1f, (scroll.Size.X - 24f) / Mathf.Max(1f, ContentBounds.X)));
scroll.ScrollHorizontal = 0;
scroll.ScrollVertical = 0;
RefreshViewport();
}
/// <summary>Re-packs the current groups; composition and individual chip locations stay unchanged.</summary>
public void ArrangeGroups()
{
var defaults = ComputeDefaultLayout();
foreach (var pair in defaults)
{
var rect = Rect(pair.Key);
rect.X = pair.Value.X;
rect.Y = pair.Value.Y;
_cards[pair.Key].SetFrame(new Rect2(pair.Value, new Vector2(rect.W, rect.H)));
}
UpdateCanvasSize();
RebuildEdges();
LayoutChanged?.Invoke();
}
/// <summary>Centers the scroll view on a logical canvas position (selection or overview click).</summary>
public void CenterOn(Vector2 position)
{
var scroll = GetParent<ScrollContainer>();
if (scroll == null)
return;
scroll.ScrollHorizontal = Mathf.RoundToInt(position.X * _zoom - scroll.Size.X / 2f);
scroll.ScrollVertical = Mathf.RoundToInt(position.Y * _zoom - scroll.Size.Y / 2f);
RefreshViewport();
}
/// <summary>Reveals the selected occurrence, keeping another group's copy from stealing focus.</summary>
public void ShowSelection()
{
if (SelectedModId != null && _instances.TryGetValue(SelectedGroupKey + "|" + SelectedModId, out var chip))
CenterOn(chip.Position + _cards[chip.GroupKey].Position + chip.Size / 2f);
else if (SelectedGroupKey != null && _cards.TryGetValue(SelectedGroupKey, out var card))
CenterOn(card.Position + card.Size / 2f);
}
private bool _panning; private bool _panning;
private Vector2 _panStartMouse; private Vector2 _panStartMouse;
@@ -94,7 +207,7 @@ namespace Mods.ModManager
_layout.Groups ??= new Dictionary<string, ModCollectionLayoutRect>(); _layout.Groups ??= new Dictionary<string, ModCollectionLayoutRect>();
_layout.Mods ??= new Dictionary<string, ModCollectionLayoutPoint>(); _layout.Mods ??= new Dictionary<string, ModCollectionLayoutPoint>();
foreach (var child in GetChildren()) foreach (var child in _content.GetChildren())
{ {
if (child != _edges) if (child != _edges)
child.QueueFree(); child.QueueFree();
@@ -103,6 +216,8 @@ namespace Mods.ModManager
_cards.Clear(); _cards.Clear();
_chips.Clear(); _chips.Clear();
_chipGroups.Clear(); _chipGroups.Clear();
_instances.Clear();
_visibleFrames.Clear();
if (graph != null && graph.Groups.Count > 0) if (graph != null && graph.Groups.Count > 0)
{ {
@@ -110,11 +225,12 @@ namespace Mods.ModManager
BuildCards(); BuildCards();
} }
_edges.SetEdges(null, _chips, this); _edges.SetEdges(null, _chips, _content);
MoveChild(_edges, GetChildCount() - 1); _content.MoveChild(_edges, _content.GetChildCount() - 1);
_edges.Visible = ShowEdges; _edges.Visible = ShowEdges;
UpdateCanvasSize(); UpdateCanvasSize();
RebuildEdges(); RebuildEdges();
SetSearch(_search);
} }
/// <summary>Highlights one group and/or one mod; pass nulls to clear.</summary> /// <summary>Highlights one group and/or one mod; pass nulls to clear.</summary>
@@ -127,8 +243,20 @@ namespace Mods.ModManager
foreach (var pair in _cards) foreach (var pair in _cards)
pair.Value.SetSelected(string.Equals(pair.Key, groupKey, StringComparison.OrdinalIgnoreCase)); pair.Value.SetSelected(string.Equals(pair.Key, groupKey, StringComparison.OrdinalIgnoreCase));
foreach (var pair in _chips) foreach (var pair in _chipGroups)
pair.Value.SetSelected(string.Equals(pair.Key, modId, StringComparison.OrdinalIgnoreCase)); {
if (_instances.TryGetValue(pair.Value + "|" + pair.Key, out var primary))
_chips[pair.Key] = primary;
}
foreach (var chip in _instances.Values)
{
var selected = string.Equals(chip.GroupKey, groupKey, StringComparison.OrdinalIgnoreCase)
&& string.Equals(chip.ModId, modId, StringComparison.OrdinalIgnoreCase);
chip.SetSelected(selected);
if (selected)
_chips[chip.ModId] = chip;
}
RebuildEdges(); RebuildEdges();
} }
@@ -145,15 +273,42 @@ namespace Mods.ModManager
/// <summary>Finds the group key owning a rendered mod chip, for inspector navigation.</summary> /// <summary>Finds the group key owning a rendered mod chip, for inspector navigation.</summary>
public string GetGroupKeyOfMod(string modId) public string GetGroupKeyOfMod(string modId)
=> modId != null && _chipGroups.TryGetValue(modId, out var key) ? key : null; {
if (modId != null && string.Equals(modId, SelectedModId, StringComparison.OrdinalIgnoreCase)
&& SelectedGroupKey != null)
return SelectedGroupKey;
return modId != null && _chipGroups.TryGetValue(modId, out var key) ? key : null;
}
protected override void Construct() protected override void Construct()
{ {
MouseFilter = MouseFilterEnum.Stop; MouseFilter = MouseFilterEnum.Stop;
GuiInput += OnCanvasGuiInput; GuiInput += OnCanvasGuiInput;
_content = new Control { MouseFilter = MouseFilterEnum.Ignore };
AddChild(_content);
_content.Connect("draw", Callable.From((Action)DrawGrid));
_edges = new CollectionEdgeLayer(); _edges = new CollectionEdgeLayer();
AddChild(_edges); _content.AddChild(_edges);
_edges.EnsureConstructed(); _edges.EnsureConstructed();
var scroll = GetParent<ScrollContainer>();
if (scroll != null)
{
scroll.GetHScrollBar().ValueChanged += OnScrollChanged;
scroll.GetVScrollBar().ValueChanged += OnScrollChanged;
scroll.Resized += RefreshViewport;
}
}
protected override void Destruct()
{
GuiInput -= OnCanvasGuiInput;
var scroll = GetParent<ScrollContainer>();
if (scroll != null && GodotObject.IsInstanceValid(scroll))
{
scroll.GetHScrollBar().ValueChanged -= OnScrollChanged;
scroll.GetVScrollBar().ValueChanged -= OnScrollChanged;
scroll.Resized -= RefreshViewport;
}
} }
private void BuildCards() private void BuildCards()
@@ -162,7 +317,7 @@ namespace Mods.ModManager
{ {
var rect = _layout.Groups[group.Key]; var rect = _layout.Groups[group.Key];
var card = new CollectionGroupCard(); var card = new CollectionGroupCard();
AddChild(card); _content.AddChild(card);
var summary = Summarize(group); var summary = Summarize(group);
card.Init(group, summary.Label, summary.Color, string.Equals(group.Key, SelectedGroupKey, StringComparison.OrdinalIgnoreCase)); card.Init(group, summary.Label, summary.Color, string.Equals(group.Key, SelectedGroupKey, StringComparison.OrdinalIgnoreCase));
@@ -174,6 +329,7 @@ namespace Mods.ModManager
card.Resized += OnCardResized; card.Resized += OnCardResized;
card.DragFinished += OnDragFinished; card.DragFinished += OnDragFinished;
_cards[group.Key] = card; _cards[group.Key] = card;
_visibleFrames[group.Key] = rect;
foreach (var mod in group.Mods) foreach (var mod in group.Mods)
{ {
@@ -183,11 +339,14 @@ namespace Mods.ModManager
card.ModsLayer.AddChild(chip); card.ModsLayer.AddChild(chip);
var node = FindNode(mod.ModId); var node = FindNode(mod.ModId);
var requirement = ModDependency.Parse(mod.Requirement);
chip.Init( chip.Init(
group.Key,
mod.ModId, mod.ModId,
mod.Role, mod.Role,
node?.Title ?? mod.ModId, node?.Title ?? mod.ModId,
VersionOf(node), VersionOf(node),
requirement.IsValid && requirement.VersionRaw != null ? requirement.Operator + requirement.VersionRaw : null,
StateLabel(node), StateLabel(node),
StateColor(node), StateColor(node),
BuildModTooltip(mod, node)); BuildModTooltip(mod, node));
@@ -195,6 +354,7 @@ namespace Mods.ModManager
chip.Selected += OnChipSelected; chip.Selected += OnChipSelected;
chip.Moved += OnChipMoved; chip.Moved += OnChipMoved;
chip.DragFinished += OnDragFinished; chip.DragFinished += OnDragFinished;
_instances[key] = chip;
if (!_chips.ContainsKey(mod.ModId)) if (!_chips.ContainsKey(mod.ModId))
{ {
@@ -396,11 +556,11 @@ namespace Mods.ModManager
GroupSelected?.Invoke(groupKey); GroupSelected?.Invoke(groupKey);
} }
private void OnChipSelected(string modId) private void OnChipSelected(CollectionModChip chip)
{ {
SelectedModId = modId; SelectedModId = chip.ModId;
SelectedGroupKey = null; SelectedGroupKey = chip.GroupKey;
ModSelected?.Invoke(modId); ModSelected?.Invoke(chip.ModId);
} }
private void OnCardCommand(string groupKey, string command) private void OnCardCommand(string groupKey, string command)
@@ -427,12 +587,9 @@ namespace Mods.ModManager
RebuildEdges(); RebuildEdges();
} }
private void OnChipMoved(string modId, Vector2 position) private void OnChipMoved(CollectionModChip chip, Vector2 position)
{ {
if (!_chipGroups.TryGetValue(modId, out var groupKey)) var key = chip.GroupKey + "|" + chip.ModId;
return;
var key = groupKey + "|" + modId;
if (!_layout.Mods.TryGetValue(key, out var point) || point == null) if (!_layout.Mods.TryGetValue(key, out var point) || point == null)
{ {
point = new ModCollectionLayoutPoint(); point = new ModCollectionLayoutPoint();
@@ -472,11 +629,15 @@ namespace Mods.ModManager
var size = new Vector2( var size = new Vector2(
Mathf.Max(maxX + GapX + 160f, MinCanvasWidth), Mathf.Max(maxX + GapX + 160f, MinCanvasWidth),
Mathf.Max(maxY + GapY + 120f, MinCanvasHeight)); Mathf.Max(maxY + GapY + 120f, MinCanvasHeight));
ContentBounds = new Vector2(Mathf.Max(460f, maxX + GapX), Mathf.Max(240f, maxY + GapY));
// Drives the canvas rect through the surrounding ScrollContainer. // Drives the canvas rect through the surrounding ScrollContainer.
// _edges carries FullRect anchors (CollectionEdgeLayer.Construct), // _edges carries FullRect anchors (CollectionEdgeLayer.Construct),
// so it follows this resize automatically; assigning its Size // so it follows this resize automatically; assigning its Size
// directly would be overridden and logs an anchors warning. // directly would be overridden and logs an anchors warning.
CustomMinimumSize = size; _content.Size = size;
_content.Scale = Vector2.One * _zoom;
CustomMinimumSize = size * _zoom;
RefreshViewport();
} }
private void RebuildEdges() private void RebuildEdges()
@@ -491,18 +652,60 @@ namespace Mods.ModManager
var fromGroup = GetGroupKeyOfMod(edge.FromModId); var fromGroup = GetGroupKeyOfMod(edge.FromModId);
var toGroup = GetGroupKeyOfMod(edge.ToModId); var toGroup = GetGroupKeyOfMod(edge.ToModId);
var active = string.Equals(edge.FromModId, SelectedModId, StringComparison.OrdinalIgnoreCase)
|| string.Equals(edge.ToModId, SelectedModId, StringComparison.OrdinalIgnoreCase);
if (_edgeMode == 0 && !active)
continue;
edges.Add(new CollectionEdge edges.Add(new CollectionEdge
{ {
FromModId = edge.FromModId, FromModId = edge.FromModId,
ToModId = edge.ToModId, ToModId = edge.ToModId,
Cross = !string.Equals(fromGroup, toGroup, StringComparison.OrdinalIgnoreCase), Cross = !string.Equals(fromGroup, toGroup, StringComparison.OrdinalIgnoreCase),
Active = string.Equals(edge.FromModId, SelectedModId, StringComparison.OrdinalIgnoreCase) Active = active,
|| string.Equals(edge.ToModId, SelectedModId, StringComparison.OrdinalIgnoreCase), Problem = HasProblem(edge.FromModId) || HasProblem(edge.ToModId),
}); });
} }
} }
_edges.SetEdges(edges, _chips, this); _edges.SetEdges(edges, _chips, _content);
RefreshViewport();
}
private bool HasProblem(string modId)
{
var node = FindNode(modId);
return node == null || node.State == ModCollectionNodeState.Missing
|| node.State == ModCollectionNodeState.VersionMismatch;
}
private void SetZoom(float value)
{
var scroll = GetParent<ScrollContainer>();
var center = scroll == null ? Vector2.Zero : VisibleBounds.GetCenter();
_zoom = Mathf.Clamp(value, 0.3f, 1.5f);
UpdateCanvasSize();
CenterOn(center);
}
private void OnScrollChanged(double value) => RefreshViewport();
private void RefreshViewport()
{
_content?.QueueRedraw();
_miniMap?.QueueRedraw();
ViewChanged?.Invoke();
}
private void DrawGrid()
{
var visible = VisibleBounds;
var start = visible.Position.Snapped(new Vector2(20f, 20f));
var color = new Color(UiTokens.LineCard, 0.55f);
for (var x = start.X; x <= visible.End.X + 20f; x += 20f)
{
for (var y = start.Y; y <= visible.End.Y + 20f; y += 20f)
_content.DrawCircle(new Vector2(x, y), 0.8f, color);
}
} }
/// <summary> /// <summary>
@@ -516,6 +719,17 @@ namespace Mods.ModManager
/// </summary> /// </summary>
private void OnCanvasGuiInput(InputEvent @event) private void OnCanvasGuiInput(InputEvent @event)
{ {
if (@event is InputEventMouseButton wheel && wheel.Pressed && wheel.CtrlPressed)
{
if (wheel.ButtonIndex == MouseButton.WheelUp)
ZoomIn();
else if (wheel.ButtonIndex == MouseButton.WheelDown)
ZoomOut();
else
return;
AcceptEvent();
return;
}
if (@event is InputEventMouseButton button && button.ButtonIndex == MouseButton.Left) if (@event is InputEventMouseButton button && button.ButtonIndex == MouseButton.Left)
{ {
if (button.Pressed) if (button.Pressed)
+4 -4
View File
@@ -151,7 +151,7 @@ namespace Mods.ModManager
row.AddChild(UiKit.Spacer()); row.AddChild(UiKit.Spacer());
var newGame = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_new_game"), ButtonKind.Primary); var newGame = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_new_game"), ButtonKind.Primary);
newGame.TooltipText = newGame.Text; ModHelpPanel.Attach(newGame, "collection_new");
newGame.Pressed += () => NewGameRequested?.Invoke(); newGame.Pressed += () => NewGameRequested?.Invoke();
row.AddChild(newGame); row.AddChild(newGame);
_host.AddChild(row); _host.AddChild(row);
@@ -402,7 +402,7 @@ namespace Mods.ModManager
button.Disabled = true; button.Disabled = true;
} }
button.TooltipText = button.Text; ModHelpPanel.Attach(button, "collection_apply");
if (action != null) if (action != null)
button.Pressed += action; button.Pressed += action;
return button; return button;
@@ -411,7 +411,7 @@ namespace Mods.ModManager
private Button BuildEditButton(ModCollectionGameSummary summary) private Button BuildEditButton(ModCollectionGameSummary summary)
{ {
var button = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_edit_scheme")); var button = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_edit_scheme"));
button.TooltipText = button.Text; ModHelpPanel.Attach(button, "collection_edit");
button.Disabled = _installingId != null; button.Disabled = _installingId != null;
button.Pressed += () => EditSchemeRequested?.Invoke(summary.Collection.Id); button.Pressed += () => EditSchemeRequested?.Invoke(summary.Collection.Id);
return button; return button;
@@ -420,7 +420,7 @@ namespace Mods.ModManager
private Button BuildDeleteButton(ModCollectionGameSummary summary) private Button BuildDeleteButton(ModCollectionGameSummary summary)
{ {
var button = UiKit.Button(TranslationServer.Translate("mod_manager/collection_delete"), ButtonKind.Danger); var button = UiKit.Button(TranslationServer.Translate("mod_manager/collection_delete"), ButtonKind.Danger);
button.TooltipText = button.Text; ModHelpPanel.Attach(button, "collection_delete");
button.Disabled = _installingId != null; button.Disabled = _installingId != null;
button.Pressed += () => CollectionDeletion.Confirm(this, summary.Collection, () => button.Pressed += () => CollectionDeletion.Confirm(this, summary.Collection, () =>
{ {
+13 -9
View File
@@ -76,12 +76,18 @@ namespace Mods.ModManager
_source.MouseFilter = Control.MouseFilterEnum.Pass; _source.MouseFilter = Control.MouseFilterEnum.Pass;
AddThemeStyleboxOverride("panel", ModManagerTheme.CreateCardStyle(data.IsConflict)); AddThemeStyleboxOverride("panel", ModManagerTheme.CreateCardStyle(data.IsConflict));
ModHelpPanel.Attach(this, "mod_card");
ModHelpPanel.Attach(_versions, "mod_version");
ModHelpPanel.Attach(_action, "mod_action");
ModHelpPanel.Attach(_enabled, "mod_enabled");
_versions.ItemSelected += OnVersionSelected; _versions.ItemSelected += OnVersionSelected;
_action.Pressed += OnActionPressed; _action.Pressed += OnActionPressed;
_enabled.Toggled += OnEnabledPressed; _enabled.Toggled += OnEnabledPressed;
GuiInput += OnGuiInput; GuiInput += OnGuiInput;
BuildCollectionsLink(); BuildCollectionsLink();
ModHelpPanel.Attach(_collectionsLink, "mod_collections");
PopulateVersions(); PopulateVersions();
RefreshState(); RefreshState();
LoadPreview(); LoadPreview();
@@ -138,7 +144,7 @@ namespace Mods.ModManager
{ {
_versions.AddItem("v" + (_data.LocalVersion ?? "—")); _versions.AddItem("v" + (_data.LocalVersion ?? "—"));
_versions.Disabled = true; _versions.Disabled = true;
_versions.TooltipText = _versions.GetItemText(0); _versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
return; return;
} }
@@ -164,9 +170,9 @@ namespace Mods.ModManager
} }
_versions.Selected = selectedIndex; _versions.Selected = selectedIndex;
// "v1.2.0 · latest · installed" does not fit the narrow combo: it ends in an // The combo clips its long version label, so its tooltip explains
// ellipsis (clip_text in the scene), the full label is in the tooltip. // the version markers instead of repeating the truncated caption.
_versions.TooltipText = _versions.GetItemText(selectedIndex); _versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
} }
/// <summary> /// <summary>
@@ -200,7 +206,6 @@ namespace Mods.ModManager
var text = ModCollectionLinks.BadgeText(_data.CollectionsCount); var text = ModCollectionLinks.BadgeText(_data.CollectionsCount);
_collectionsLink.Visible = text.Length > 0; _collectionsLink.Visible = text.Length > 0;
_collectionsLink.Text = text; _collectionsLink.Text = text;
_collectionsLink.TooltipText = text;
} }
private void UpdateActionButton() private void UpdateActionButton()
@@ -214,7 +219,6 @@ namespace Mods.ModManager
_action.Visible = true; _action.Visible = true;
_action.Text = ModActionNames.Translate(kind.Value); _action.Text = ModActionNames.Translate(kind.Value);
_action.TooltipText = _action.Text;
if (ModActionNames.IsDanger(kind.Value)) if (ModActionNames.IsDanger(kind.Value))
CthangoverTheme.ApplyDanger(_action); CthangoverTheme.ApplyDanger(_action);
@@ -232,8 +236,8 @@ namespace Mods.ModManager
_enabled.ButtonPressed = !_data.IsDisabled; _enabled.ButtonPressed = !_data.IsDisabled;
_updatingEnabled = false; _updatingEnabled = false;
_enabled.TooltipText = TranslationServer.Translate(_data.IsDisabled _enabled.TooltipText = TranslationServer.Translate(_data.IsDisabled
? "mod_manager/action_enable" ? "mod_manager/enable_tooltip"
: "mod_manager/action_disable"); : "mod_manager/disable_tooltip");
} }
private void LoadPreview() private void LoadPreview()
@@ -291,7 +295,7 @@ namespace Mods.ModManager
return; return;
_data.SelectedDownload = _data.Downloads[(int)index]; _data.SelectedDownload = _data.Downloads[(int)index];
_versions.TooltipText = _versions.GetItemText((int)index); _versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
_data.RecomputeStatus(); _data.RecomputeStatus();
RefreshState(); RefreshState();
VersionChanged?.Invoke(_data); VersionChanged?.Invoke(_data);
+9 -7
View File
@@ -86,6 +86,8 @@ namespace Mods.ModManager
_conflictsBox = GetNode<VBoxContainer>("Scroll/Content/ConflictsBox"); _conflictsBox = GetNode<VBoxContainer>("Scroll/Content/ConflictsBox");
_conflictsText = GetNode<Label>("Scroll/Content/ConflictsBox/Text"); _conflictsText = GetNode<Label>("Scroll/Content/ConflictsBox/Text");
ModHelpPanel.Attach(_versions, "mod_version");
_versions.ItemSelected += OnVersionSelected; _versions.ItemSelected += OnVersionSelected;
_author.AddThemeColorOverride("font_color", UiTokens.Muted); _author.AddThemeColorOverride("font_color", UiTokens.Muted);
@@ -177,7 +179,7 @@ namespace Mods.ModManager
} }
_versions.Selected = selectedIndex; _versions.Selected = selectedIndex;
_versions.TooltipText = _versions.GetItemText(selectedIndex); _versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
} }
private void BuildActions() private void BuildActions()
@@ -191,19 +193,21 @@ namespace Mods.ModManager
var primary = ModActions.GetPrimary(_data); var primary = ModActions.GetPrimary(_data);
if (primary != null) if (primary != null)
{ {
var button = new Button { Text = ModActionNames.Translate(primary.Value), TooltipText = ModActionNames.Translate(primary.Value) }; var button = new Button { Text = ModActionNames.Translate(primary.Value) };
if (ModActionNames.IsDanger(primary.Value)) if (ModActionNames.IsDanger(primary.Value))
CthangoverTheme.ApplyDanger(button); CthangoverTheme.ApplyDanger(button);
else else
CthangoverTheme.ApplyPrimary(button); CthangoverTheme.ApplyPrimary(button);
ModHelpPanel.Attach(button, "mod_action");
button.Pressed += OnPrimaryPressed; button.Pressed += OnPrimaryPressed;
_actionsRow.AddChild(button); _actionsRow.AddChild(button);
} }
if (primary != ModActionKind.Delete && _data.IsDeletable) if (primary != ModActionKind.Delete && _data.IsDeletable)
{ {
var delete = new Button { Text = ModActionNames.Translate(ModActionKind.Delete), TooltipText = ModActionNames.Translate(ModActionKind.Delete) }; var delete = new Button { Text = ModActionNames.Translate(ModActionKind.Delete) };
CthangoverTheme.ApplyDanger(delete); CthangoverTheme.ApplyDanger(delete);
ModHelpPanel.Attach(delete, "mod_delete");
delete.Pressed += OnDeletePressed; delete.Pressed += OnDeletePressed;
_actionsRow.AddChild(delete); _actionsRow.AddChild(delete);
} }
@@ -215,14 +219,12 @@ namespace Mods.ModManager
Text = ModActionNames.Translate(_data.IsDisabled Text = ModActionNames.Translate(_data.IsDisabled
? ModActionKind.Enable ? ModActionKind.Enable
: ModActionKind.Disable), : ModActionKind.Disable),
TooltipText = ModActionNames.Translate(_data.IsDisabled
? ModActionKind.Enable
: ModActionKind.Disable),
}; };
if (_data.IsDisabled) if (_data.IsDisabled)
CthangoverTheme.ApplyPrimary(toggle); CthangoverTheme.ApplyPrimary(toggle);
else else
CthangoverTheme.ApplyDanger(toggle); CthangoverTheme.ApplyDanger(toggle);
ModHelpPanel.Attach(toggle, "mod_enabled");
toggle.Pressed += OnTogglePressed; toggle.Pressed += OnTogglePressed;
_actionsRow.AddChild(toggle); _actionsRow.AddChild(toggle);
} }
@@ -245,12 +247,12 @@ namespace Mods.ModManager
var link = new Button var link = new Button
{ {
Text = collectionsText, Text = collectionsText,
TooltipText = collectionsText,
FocusMode = FocusModeEnum.None, FocusMode = FocusModeEnum.None,
}; };
link.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall); link.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
link.AddThemeColorOverride("font_color", UiTokens.TypeTarget); link.AddThemeColorOverride("font_color", UiTokens.TypeTarget);
link.AddThemeColorOverride("font_hover_color", UiTokens.TypeTarget); link.AddThemeColorOverride("font_hover_color", UiTokens.TypeTarget);
ModHelpPanel.Attach(link, "mod_collections");
link.Pressed += () => CollectionsLinkRequested?.Invoke(); link.Pressed += () => CollectionsLinkRequested?.Invoke();
_actionsRow.AddChild(link); _actionsRow.AddChild(link);
} }
+2 -3
View File
@@ -56,8 +56,9 @@ namespace Mods.ModManager
_statusCaption.Text = TranslationServer.Translate("mod_manager/filter_status"); _statusCaption.Text = TranslationServer.Translate("mod_manager/filter_status");
_categoryCaption.Text = TranslationServer.Translate("mod_manager/filter_categories"); _categoryCaption.Text = TranslationServer.Translate("mod_manager/filter_categories");
_groupToggle.Text = TranslationServer.Translate("mod_manager/group_by_category"); _groupToggle.Text = TranslationServer.Translate("mod_manager/group_by_category");
_groupToggle.TooltipText = _groupToggle.Text;
_groupToggle.ButtonPressed = _state.GroupByCategory; _groupToggle.ButtonPressed = _state.GroupByCategory;
ModHelpPanel.Attach(_reset, "filters_reset");
ModHelpPanel.Attach(_groupToggle, "group_categories");
_statusCaption.AddThemeColorOverride("font_color", UiTokens.Muted); _statusCaption.AddThemeColorOverride("font_color", UiTokens.Muted);
_categoryCaption.AddThemeColorOverride("font_color", UiTokens.Muted); _categoryCaption.AddThemeColorOverride("font_color", UiTokens.Muted);
@@ -80,14 +81,12 @@ namespace Mods.ModManager
{ {
var count = statusCounts.GetValueOrDefault(pair.Key); var count = statusCounts.GetValueOrDefault(pair.Key);
pair.Value.Text = $"{ModStatusNames.Translate(pair.Key)} ({count})"; pair.Value.Text = $"{ModStatusNames.Translate(pair.Key)} ({count})";
pair.Value.TooltipText = pair.Value.Text;
} }
foreach (var pair in _categoryBoxes) foreach (var pair in _categoryBoxes)
{ {
var count = categoryCounts.GetValueOrDefault(pair.Key); var count = categoryCounts.GetValueOrDefault(pair.Key);
pair.Value.Text = $"{ModCategoryNames.Translate(pair.Key)} ({count})"; pair.Value.Text = $"{ModCategoryNames.Translate(pair.Key)} ({count})";
pair.Value.TooltipText = pair.Value.Text;
} }
} }
+200 -24
View File
@@ -1,54 +1,230 @@
namespace Mods.ModManager namespace Mods.ModManager
{ {
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.Theming;
using Godot; using Godot;
/// <summary> /// <summary>
/// Full-screen help overlay of the mods hub: one page per hub level — the mods /// Non-modal, contextual help for the mods hub. Controls carry a locale suffix in
/// grid, a mod's page, the games gallery and the scheme editor. The layout is the /// <see cref="HintKeyMetadata"/>; the layer finds the annotated ancestor under the
/// <c>mod_help_panel.tscn</c> prefab (dim backdrop, centred frame, title, body, /// pointer or keyboard focus, outlines it and places a compact card next to it.
/// close button); this class only binds the nodes and fills the texts, so a new /// The root ignores mouse input, so the inspected control still performs its normal
/// page is a locale pair <c>mod_manager/help_&lt;level&gt;_title/_body</c> — no code. /// action. A click pins the current card without swallowing that click; Escape or
/// Opened by the hub header's help button, closed by the button, a click on the /// the header button leaves help mode through <see cref="ModHubScreen"/>.
/// dim or Escape (the hub closes the panel first in its own Escape chain).
/// </summary> /// </summary>
public partial class ModHelpPanel : Cthangover.Mods.Interface.Base.Lists.ModWidget public partial class ModHelpPanel : Cthangover.Mods.Interface.Base.Lists.ModWidget
{ {
public const string HintKeyMetadata = "mod_manager_hint";
/// <summary>Optional locale-pair suffix for a contextual explanation without changing the target's stable help id.</summary>
public const string HintTranslationMetadata = "mod_manager_hint_translation";
private const float Gap = 10f;
private const float ScreenMargin = 12f;
private const float FooterClearance = 46f;
private Panel _highlight;
private PanelContainer _card;
private Label _title; private Label _title;
private RichTextLabel _body; private RichTextLabel _body;
private Label _footer;
/// <summary>The annotated control currently explained by the card.</summary>
public Control CurrentTarget { get; private set; }
/// <summary>Whether the card stays on its target after the pointer moves away.</summary>
public bool IsPinned { get; private set; }
/// <summary>Associates a control with a mod_manager/hint_* locale pair.</summary>
public static void Attach(Control control, string hintKey)
{
control?.SetMeta(HintKeyMetadata, hintKey);
}
protected override void Construct() protected override void Construct()
{ {
MouseFilter = MouseFilterEnum.Stop; MouseFilter = MouseFilterEnum.Ignore;
_title = GetNode<Label>("Center/Frame/Margin/Column/Header/Title"); _highlight = GetNode<Panel>("Highlight");
_body = GetNode<RichTextLabel>("Center/Frame/Margin/Column/Body"); _card = GetNode<PanelContainer>("Card");
_title = GetNode<Label>("Card/Margin/Column/Title");
_body = GetNode<RichTextLabel>("Card/Margin/Column/Body");
_footer = GetNode<Label>("Footer/Margin/FooterText");
var close = GetNode<Button>("Center/Frame/Margin/Column/Header/Close"); _highlight.AddThemeStyleboxOverride("panel", UiKit.Frame(
close.Text = TranslationServer.Translate("mod_manager/help_close"); UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.Accent, 0.025f), UiTokens.AccentLineStrong));
close.TooltipText = TranslationServer.Translate("mod_manager/help_close"); _card.AddThemeStyleboxOverride("panel", UiKit.Frame(
close.Pressed += Close; UiTokens.RadiusSm, UiTokens.TooltipBg, UiTokens.AccentLine));
GetNode<PanelContainer>("Footer").AddThemeStyleboxOverride("panel", UiKit.Frame(
0, UiTokens.Surface0, UiTokens.Line));
_title.AddThemeColorOverride("font_color", UiTokens.InkBright);
_body.AddThemeColorOverride("default_color", UiTokens.Body);
_footer.AddThemeColorOverride("font_color", UiTokens.Faint);
var dim = GetNode<ColorRect>("Dim"); _footer.Text = TranslationServer.Translate("mod_manager/help_active");
dim.GuiInput += OnDimGuiInput; _highlight.Visible = false;
_card.Visible = false;
} }
/// <summary>Shows the page of one hub level; the level names match the test driver's: mods, mod_page, collections, scheme.</summary> /// <summary>Enables inspection without choosing a page or blocking the hub.</summary>
public void Open(string level) public void Open()
{ {
_title.Text = TranslationServer.Translate($"mod_manager/help_{level}_title");
_body.Text = TranslationServer.Translate($"mod_manager/help_{level}_body");
Visible = true; Visible = true;
IsPinned = false;
CurrentTarget = null;
_highlight.Visible = false;
_card.Visible = false;
UpdateHoveredTarget();
} }
/// <summary>Hides the overlay; the hub calls this from its Escape chain and the driver checks it.</summary> /// <summary>Leaves help mode and removes every help-only visual.</summary>
public void Close() public void Close()
{ {
Visible = false; Visible = false;
IsPinned = false;
CurrentTarget = null;
if (GodotObject.IsInstanceValid(_highlight))
_highlight.Visible = false;
if (GodotObject.IsInstanceValid(_card))
_card.Visible = false;
} }
private void OnDimGuiInput(InputEvent @event) /// <summary>
/// Explains and pins one annotated control. Also used by F1 so keyboard users
/// get the same stable card as pointer users get after a click.
/// </summary>
public void ShowFor(Control target)
{ {
if (@event is InputEventMouseButton mouse && mouse.Pressed) Control annotated = FindTarget(target);
Close(); if (annotated == null)
return;
Visible = true;
IsPinned = true;
SetTarget(annotated);
}
public override void _Process(double delta)
{
if (!Visible)
return;
if (IsPinned)
{
if (!GodotObject.IsInstanceValid(CurrentTarget) || !CurrentTarget.IsVisibleInTree())
{
IsPinned = false;
CurrentTarget = null;
_highlight.Visible = false;
_card.Visible = false;
return;
}
LayoutAroundTarget();
return;
}
UpdateHoveredTarget();
}
public override void _Input(InputEvent @event)
{
if (!Visible || @event is not InputEventMouseButton mouse
|| !mouse.Pressed || mouse.ButtonIndex != MouseButton.Left)
return;
Control target = FindTarget(GetViewport().GuiGetHoveredControl());
if (target != null)
{
IsPinned = true;
SetTarget(target);
}
else
{
IsPinned = false;
}
}
private void UpdateHoveredTarget()
{
Control target = FindTarget(GetViewport().GuiGetHoveredControl());
SetTarget(target);
}
private Control FindTarget(Control control)
{
Node node = control;
while (node != null && node != this)
{
if (node is Control candidate
&& candidate.IsVisibleInTree()
&& candidate.HasMeta(HintKeyMetadata))
{
return candidate;
}
node = node.GetParent();
}
return null;
}
private void SetTarget(Control target)
{
if (CurrentTarget == target)
{
if (target != null)
LayoutAroundTarget();
return;
}
CurrentTarget = target;
bool hasTarget = target != null;
_highlight.Visible = hasTarget;
_card.Visible = hasTarget;
if (!hasTarget)
return;
string key = target.GetMeta(HintKeyMetadata).AsString();
if (target.HasMeta(HintTranslationMetadata))
key = target.GetMeta(HintTranslationMetadata).AsString();
_title.Text = TranslationServer.Translate($"mod_manager/hint_{key}_title");
_body.Text = TranslationServer.Translate($"mod_manager/hint_{key}_body");
_card.ResetSize();
LayoutAroundTarget();
}
private void LayoutAroundTarget()
{
if (CurrentTarget == null || !CurrentTarget.IsVisibleInTree())
return;
_highlight.GlobalPosition = CurrentTarget.GlobalPosition - new Vector2(3, 3);
_highlight.Size = CurrentTarget.Size + new Vector2(6, 6);
Vector2 viewport = GetViewportRect().Size;
Vector2 cardSize = _card.GetCombinedMinimumSize();
_card.Size = cardSize;
Rect2 target = new(CurrentTarget.GlobalPosition, CurrentTarget.Size);
float x = Mathf.Clamp(target.GetCenter().X - cardSize.X / 2f,
ScreenMargin, Mathf.Max(ScreenMargin, viewport.X - cardSize.X - ScreenMargin));
float y = target.End.Y + Gap;
float bottomLimit = viewport.Y - FooterClearance;
if (y + cardSize.Y > bottomLimit)
y = target.Position.Y - cardSize.Y - Gap;
if (y < ScreenMargin)
{
y = Mathf.Clamp(target.GetCenter().Y - cardSize.Y / 2f,
ScreenMargin, Mathf.Max(ScreenMargin, bottomLimit - cardSize.Y));
x = target.End.X + Gap;
if (x + cardSize.X > viewport.X - ScreenMargin)
x = target.Position.X - cardSize.X - Gap;
}
x = Mathf.Clamp(x, ScreenMargin, Mathf.Max(ScreenMargin, viewport.X - cardSize.X - ScreenMargin));
_card.GlobalPosition = new Vector2(x, Mathf.Max(ScreenMargin, y));
} }
} }
} }
+466 -52
View File
@@ -6,19 +6,25 @@ using Cthangover.Core.Bootstrap;
using Cthangover.Core.UI.Menu; using Cthangover.Core.UI.Menu;
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
using Cthangover.Mods.Interface.Menu; using Cthangover.Mods.Interface.Menu;
using Cthangover.Mods.Interface.Theming;
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.View; using Cthangover.Mods.Interface.View;
using Godot; using Godot;
using Mods.ModManager; using Mods.ModManager;
namespace Mods.ModManagerHelpTest namespace Mods.ModManagerHelpTest
{ {
using System.Text.Json;
using Cthangover.Core.Mods;
/// <summary> /// <summary>
/// Walks the Mods &amp; Collections hub like a player who hovers over everything: the mods grid, a mod's /// Exercises contextual help as a player uses it: pin a hint beside a mod search,
/// page, the games gallery, the scheme editor (with a mechanic and then a mod selected) and the help /// keep the hub interactive while help is open, inspect a mod page and the scheme
/// panel of each level. On every level each visible button, toggle, list and input must carry a tooltip, /// legend, invoke F1 from keyboard focus and leave with Escape without navigating
/// and no tooltip may sit on a control that ignores the mouse (Godot never shows it there). One step /// away. Also rejects tooltips that merely repeat their button captions and
/// per tick, so deferred layout settles. <c>--shots=&lt;dir&gt;</c> saves every level. /// exercises Graphite's scoped version edits, duplicate occurrences, zoom and
/// Prints <c>[TEST_RESULT] name=mod_help</c>. /// overview, shared dialog chrome and title-bar close hit areas. Collection
/// edits remain drafts and the user's layout is restored.
/// <c>--shots=&lt;dir&gt;</c> saves the inspected states. Prints mod_help's result.
/// </summary> /// </summary>
public partial class ModHelpTestDriverNode : Node public partial class ModHelpTestDriverNode : Node
{ {
@@ -33,6 +39,11 @@ namespace Mods.ModManagerHelpTest
private double stepAt; private double stepAt;
private bool done; private bool done;
private ModHubScreen hub; private ModHubScreen hub;
private ModCollectionsScreen collectionEditor;
private CollectionSchemeCanvas scheme;
private Vector2 firstOccurrencePosition;
private string layoutPath;
private byte[] layoutBackup;
public override void _Process(double delta) public override void _Process(double delta)
{ {
@@ -84,29 +95,53 @@ namespace Mods.ModManagerHelpTest
break; break;
case 1: case 1:
{
PressTab("mod_manager/hub_tab_mods"); PressTab("mod_manager/hub_tab_mods");
CheckLevel("mods");
PressHelp();
ModHelpPanel help = Help();
Check("the header enables help mode", help != null && help.Visible);
Check("help has no dim backdrop", help != null && help.GetNodeOrNull("Dim") == null);
Check("help does not capture the mouse", help != null && help.MouseFilter == Control.MouseFilterEnum.Ignore);
Control search = Annotated("mod_search");
CheckHelp("mods_search", search);
break; break;
}
case 2: case 2:
CheckLevel("mods"); {
ModHelpPanel help = Help();
Check("a shown hint is pinned", help != null && help.IsPinned);
var card = hub.FindChildren("*", "", true, false).OfType<ModCard>().FirstOrDefault(c => c.IsVisibleInTree()); var card = hub.FindChildren("*", "", true, false).OfType<ModCard>().FirstOrDefault(c => c.IsVisibleInTree());
Check("the grid shows mod cards", card != null); Check("the grid shows mod cards", card != null);
card?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true }); card?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
break; break;
}
case 3: case 3:
{
Check("the hub stays interactive while help is open", hub.FindChildren("*", "", true, false).OfType<ModDetailView>().Any(d => d.IsVisibleInTree()));
CheckLevel("mod_page"); CheckLevel("mod_page");
CheckHelp("mod_version", Annotated("mod_version"));
PressTab("mod_manager/hub_tab_collections"); PressTab("mod_manager/hub_tab_collections");
break; break;
}
case 4: case 4:
{
Check("collections open without closing help", Gallery() != null && Gallery().IsVisibleInTree() && Help().Visible);
CheckLevel("gallery"); CheckLevel("gallery");
EscapeHelp();
hub.OpenCollectionEditor(GameId); hub.OpenCollectionEditor(GameId);
break; break;
}
case 5: case 5:
{ {
CheckLevel("scheme"); CheckLevel("scheme");
Control legend = Annotated("load_order");
Check("the scheme always shows its load-order legend", legend != null && legend.IsVisibleInTree());
CheckHelp("scheme_load_order", legend);
var group = hub.FindChildren("*", "", true, false).OfType<CollectionGroupCard>().FirstOrDefault(g => g.IsVisibleInTree()); var group = hub.FindChildren("*", "", true, false).OfType<CollectionGroupCard>().FirstOrDefault(g => g.IsVisibleInTree());
Check("the scheme shows mechanics", group != null); Check("the scheme shows mechanics", group != null);
Click(group); Click(group);
@@ -123,31 +158,184 @@ namespace Mods.ModManagerHelpTest
} }
case 7: case 7:
{
CheckLevel("scheme_mod"); CheckLevel("scheme_mod");
PressHelp(); Control links = Annotated("collection_links");
Check("the links toggle has keyboard focus", links != null);
links?.GrabFocus();
hub._UnhandledInput(new InputEventKey { Keycode = Key.F1, Pressed = true });
ModHelpPanel help = Help();
Check("F1 explains the focused control", help != null && help.CurrentTarget == links && help.IsPinned);
Shot("scheme_f1");
break; break;
}
case 8: case 8:
CheckHelp("help_scheme"); // Leave the F1 state visible for the deferred screenshot.
PressTab("mod_manager/hub_tab_collections"); EscapeHelp();
Check("Escape leaves help without closing the editor", Annotated("load_order") != null);
hub.OpenCollectionEditor("cthangover");
break; break;
case 9: case 9:
PressHelp(); collectionEditor = FindControl<ModCollectionsScreen>(hub);
scheme = FindControl<CollectionSchemeCanvas>(collectionEditor);
Check("graphite has the existing native canvas", scheme != null);
Check("graphite has a scene-defined version form", collectionEditor?.FindChild("VersionEditor", true, false) != null);
Check("graphite has a visible overview", FindControl<CollectionMiniMap>(collectionEditor)?.Size.X > 0);
CheckSourceSyntax();
if (GameEnvironment.IsRuntime)
{
layoutPath = ProjectSettings.GlobalizePath("user://collection_layout.json");
layoutBackup = File.Exists(layoutPath) ? File.ReadAllBytes(layoutPath) : null;
}
Shot("graphite_01_collection");
break; break;
case 10: case 10:
CheckHelp("help_collections"); SetGraphiteFixture();
PressTab("mod_manager/hub_tab_mods");
break; break;
case 11: case 11:
PressHelp(); var second = FindOccurrence("m:cthangover:second");
firstOccurrencePosition = FindOccurrence("m:cthangover:first").Position;
Click(second);
Check("shared mod selection retains its clicked group", scheme.SelectedGroupKey == "m:cthangover:second");
Check("version form reads the selected occurrence's pin", VersionField<LineEdit>("Value").Text == "1");
Shot("graphite_02_shared_context");
break; break;
case 12: case 12:
CheckHelp("help_mods"); SelectVersionPolicy("=");
hub.QueueFree(); VersionField<LineEdit>("Value").Text = "999.0.0";
PressEditorButton("SaveRule");
Shot("graphite_03_version_conflict");
break;
case 13:
Check("incompatible version blocks apply", collectionEditor.FindChild("Apply", true, false) is Button apply && apply.Disabled);
PressEditorButton("Json");
Shot("graphite_06_json_dialog");
break;
case 14:
var jsonDialog = FindControl<ConfirmationDialog>(collectionEditor);
CheckDialogChrome("JSON", jsonDialog);
var jsonText = FindControl<TextEdit>(jsonDialog);
Check("collection JSON uses a highlighted code editor", jsonText is CodeEdit && jsonText.SyntaxHighlighter != null);
var edited = JsonSerializer.Deserialize<ModCollection>(jsonText.Text);
Check("version edit preserves the other group", edited.Mechanics[0].Mods[0] == "ui_base>=0");
Check("version edit survives the real JSON editor", edited.Mechanics[1].Mods[0] == "ui_base=999.0.0");
jsonDialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
break;
case 15:
SelectVersionPolicy(">=");
VersionField<LineEdit>("Value").Text = "1";
PressEditorButton("SaveRule");
PressEditorButton("TabProblems");
Check("restoring a compatible pin clears problems", HasLabel(collectionEditor, TranslationServer.Translate("mod_manager/collection_problems_none")));
PressEditorButton("TabInspector");
var zoom = scheme.Zoom;
PressEditorButton("ZoomOut");
Check("zoom updates the view scale", scheme.Zoom < zoom);
Check("zoom preserves logical chip positions", FindOccurrence("m:cthangover:first").Position == firstOccurrencePosition);
break;
case 16:
var movedChip = FindOccurrence("m:cthangover:second");
var beforeMove = movedChip.Position;
MoveOccurrence(movedChip, new Vector2(12f, 4f));
var stored = CollectionLayoutStore.Load("cthangover");
var position = stored.Mods[movedChip.GroupKey + "|" + movedChip.ModId];
Check("drag persists the selected occurrence", Mathf.IsEqualApprox(position.X, beforeMove.X + 12f));
Check("drag leaves the first occurrence unchanged", FindOccurrence("m:cthangover:first").Position == firstOccurrencePosition);
scheme.CenterOn(new Vector2(2000f, 1500f));
var beforePan = scheme.VisibleBounds.Position;
var map = FindControl<CollectionMiniMap>(collectionEditor);
map.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton
{
ButtonIndex = MouseButton.Left, Pressed = true, Position = map.Size / 2f,
});
Check("overview click moves the scroll viewport", scheme.VisibleBounds.Position != beforePan);
break;
case 17:
PressEditorButton("Fit");
Shot("graphite_04_layout");
break;
case 18:
SetIncludedFixture();
break;
case 19:
Click(FindOccurrence("i:cthangover:dev_tools>=1", "editor_tools"));
SelectVersionPolicy("=");
VersionField<LineEdit>("Value").Text = "not a version?";
PressEditorButton("SaveRule");
Check("invalid input is explained", VersionField<Label>("Feedback").Text == TranslationServer.Translate("mod_manager/collection_version_invalid"));
PressEditorButton("Json");
break;
case 20:
var invalidDialog = FindControl<ConfirmationDialog>(collectionEditor);
var unchanged = JsonSerializer.Deserialize<ModCollection>(FindControl<TextEdit>(invalidDialog).Text);
Check("invalid input does not fork or repoint an included block", unchanged.Includes[0].Ref == "dev_tools>=1");
invalidDialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
break;
case 21:
SelectVersionPolicy("=");
VersionField<LineEdit>("Value").Text = ModRegistry.Instance.AllDiscoveredMods["editor_tools"].Version;
PressEditorButton("SaveRule");
Check("valid edit retains selection in the per-game block copy", scheme.SelectedGroupKey == "i:cthangover:dev_tools.cthangover");
PressEditorButton("Json");
break;
case 22:
var copiedDialog = FindControl<ConfirmationDialog>(collectionEditor);
var copied = JsonSerializer.Deserialize<ModCollection>(FindControl<TextEdit>(copiedDialog).Text);
Check("valid edit repoints only this game's include", copied.Includes[0].Ref == "dev_tools.cthangover");
copiedDialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
break;
case 23:
Shot("graphite_05_included_version");
break;
case 24:
SetGraphiteFixture();
break;
case 25:
var renameCard = FindControl<CollectionGroupCard>(collectionEditor);
Click(renameCard);
break;
case 26:
var rename = Buttons(collectionEditor).OfType<Button>().First(b => b.IsVisibleInTree() && b.Text == TranslationServer.Translate("mod_manager/collection_rename"));
rename.EmitSignal(BaseButton.SignalName.Pressed);
Shot("graphite_07_rename_dialog");
break;
case 27:
var renameDialog = FindControl<AcceptDialog>(collectionEditor);
CheckDialogChrome("rename", renameDialog);
ClickDialogClose(renameDialog);
break;
case 28:
Check("title-bar close dismisses rename", FindControl<AcceptDialog>(collectionEditor) == null);
PressEditorButton("Json");
break;
case 29:
ClickDialogClose(FindControl<ConfirmationDialog>(collectionEditor));
break;
case 30:
Check("title-bar close dismisses JSON", FindControl<ConfirmationDialog>(collectionEditor) == null);
Finish(); Finish();
break; break;
} }
@@ -155,7 +343,188 @@ namespace Mods.ModManagerHelpTest
step++; step++;
} }
/// <summary>A left click on the frame of a scheme mechanic or mod chip (their first panel takes the mouse).</summary> private void CheckSourceSyntax()
{
var editor = new TextEdit { Visible = false };
collectionEditor.AddChild(editor);
editor.SyntaxHighlighter = SourceSyntax.CreateJsonHighlighter();
editor.Text = "{\"name\": \"value # ; 123\", \"enabled\": true, \"count\": -2.5, \"none\": null}";
CheckSyntaxColor(editor, 0, editor.Text.IndexOf("value", StringComparison.Ordinal), UiTokens.AccentBright, "JSON string");
CheckSyntaxColor(editor, 0, editor.Text.IndexOf('#'), UiTokens.AccentBright, "JSON comment marker inside a string");
CheckSyntaxColor(editor, 0, editor.Text.IndexOf("true", StringComparison.Ordinal), UiTokens.TypeTarget, "JSON boolean");
CheckSyntaxColor(editor, 0, editor.Text.IndexOf("2.5", StringComparison.Ordinal), UiTokens.Info, "JSON number");
CheckSyntaxColor(editor, 0, editor.Text.IndexOf("null", StringComparison.Ordinal), UiTokens.TypeTarget, "JSON null");
editor.Text = "{\"value\": \"a\\\"#b\", \"enabled\": false}";
CheckSyntaxColor(editor, 0, editor.Text.IndexOf('#'), UiTokens.AccentBright, "escaped JSON quote");
CheckSyntaxColor(editor, 0, editor.Text.IndexOf("false", StringComparison.Ordinal), UiTokens.TypeTarget, "JSON literal after an escaped string");
editor.Text = "{\"unfinished\": \"text";
CheckSyntaxColor(editor, 0, editor.Text.IndexOf("text", StringComparison.Ordinal), UiTokens.AccentBright, "unfinished JSON after editing");
editor.SyntaxHighlighter = SourceSyntax.CreateTscnHighlighter();
editor.Text = "[gd_scene load_steps=2 format=3]\n[node name=\"Control\" type=\"Control\"]\nposition = Vector2(12, -4)\n; comment 7";
CheckSyntaxColor(editor, 0, 1, UiTokens.TypeTarget, "TSCN section");
CheckSyntaxColor(editor, 1, 12, UiTokens.AccentBright, "TSCN string");
CheckSyntaxColor(editor, 2, 11, UiTokens.TypeTarget, "TSCN constructor");
CheckSyntaxColor(editor, 2, 19, UiTokens.Info, "TSCN number");
CheckSyntaxColor(editor, 3, 2, UiTokens.Faint, "TSCN comment");
editor.SyntaxHighlighter = SourceSyntax.CreateScenarioHighlighter(new[] { "mod_command" });
editor.Text = "mod_command target=\"res://#path\" wait=2\n# comment\n:next\noption \"Continue\" -> :next";
CheckSyntaxColor(editor, 0, 0, UiTokens.Info, "registered scenario command");
CheckSyntaxColor(editor, 0, 23, UiTokens.AccentBright, "scenario path inside a string");
CheckSyntaxColor(editor, 0, 33, UiTokens.Accent, "scenario named argument");
CheckSyntaxColor(editor, 1, 2, UiTokens.Faint, "scenario comment");
CheckSyntaxColor(editor, 2, 1, UiTokens.TypeTarget, "scenario label");
CheckSyntaxColor(editor, 3, 0, UiTokens.Info, "scenario option");
Check("highlighting preserves source text", editor.Text == "mod_command target=\"res://#path\" wait=2\n# comment\n:next\noption \"Continue\" -> :next");
editor.Free();
}
private void CheckSyntaxColor(TextEdit editor, int line, int column, Color expected, string token)
{
var spans = editor.SyntaxHighlighter.GetLineSyntaxHighlighting(line);
var nearest = -1;
var actual = Colors.Transparent;
foreach (var key in spans.Keys)
{
var start = key.AsInt32();
if (start > column || start < nearest)
continue;
nearest = start;
actual = spans[key].AsGodotDictionary()["color"].AsColor();
}
Check(token + " is highlighted", actual.IsEqualApprox(expected));
}
private void CheckDialogChrome(string name, AcceptDialog dialog)
{
var icon = dialog.GetThemeIcon("close", "Window");
var top = dialog.GetThemeConstant("close_v_offset", "Window");
var right = dialog.GetThemeConstant("close_h_offset", "Window");
Check(name + ": close icon fits entirely inside the title bar",
top >= icon.GetHeight() && top <= dialog.GetThemeConstant("title_height", "Window")
&& right >= icon.GetWidth());
var frame = (StyleBoxFlat)dialog.GetThemeStylebox("embedded_border", "Window");
Check(name + ": rounded outer corners clear the opaque content viewport",
frame.ExpandMarginTop >= dialog.GetThemeConstant("title_height", "Window")
&& frame.ExpandMarginLeft >= frame.CornerRadiusBottomLeft
&& frame.ExpandMarginRight >= frame.CornerRadiusBottomRight
&& frame.ExpandMarginBottom >= Math.Max(frame.CornerRadiusBottomLeft, frame.CornerRadiusBottomRight));
}
private void ClickDialogClose(AcceptDialog dialog)
{
// Headless has no rendered chrome to hit-test. Deliver the same
// window-manager request there; the GPU run clicks the real icon.
if (DisplayServer.GetName() == "headless")
{
dialog.Notification((int)NotificationWMCloseRequest);
return;
}
var icon = dialog.GetThemeIcon("close", "Window");
var point = (Vector2)dialog.Position + new Vector2(
dialog.Size.X - dialog.GetThemeConstant("close_h_offset", "Window"),
-dialog.GetThemeConstant("close_v_offset", "Window")) + icon.GetSize() / 2f;
GetViewport().PushInput(new InputEventMouseButton { Position = point, ButtonIndex = MouseButton.Left, Pressed = true });
GetViewport().PushInput(new InputEventMouseButton { Position = point, ButtonIndex = MouseButton.Left, Pressed = false });
}
private void SetGraphiteFixture()
{
var fixture = new ModCollection
{
Id = "cthangover", Name = "Graphite selection check",
Mechanics = new List<ModCollectionMechanic>
{
new() { Id = "first", Name = "First role", Role = "engine", Mods = new List<string> { "ui_base>=0" } },
new() { Id = "second", Name = "Second role", Role = "extra", Mods = new List<string> { "ui_base>=1" } },
},
};
PressEditorButton("Json");
var dialog = FindControl<ConfirmationDialog>(collectionEditor);
FindControl<TextEdit>(dialog).Text = JsonSerializer.Serialize(fixture);
dialog.EmitSignal(ConfirmationDialog.SignalName.Confirmed);
}
private void SetIncludedFixture()
{
var fixture = new ModCollection
{
Id = "cthangover", Name = "Graphite included version check",
Includes = new List<ModCollectionInclude> { new() { Ref = "dev_tools>=1", Role = "tools" } },
};
PressEditorButton("Json");
var dialog = FindControl<ConfirmationDialog>(collectionEditor);
FindControl<TextEdit>(dialog).Text = JsonSerializer.Serialize(fixture);
dialog.EmitSignal(ConfirmationDialog.SignalName.Confirmed);
}
private CollectionModChip FindOccurrence(string group, string modId = "ui_base")
{
foreach (var child in scheme.FindChildren("*", "", true, false))
{
if (child is CollectionModChip chip && chip.GroupKey == group && chip.ModId == modId)
return chip;
}
throw new InvalidOperationException("Mod occurrence is absent: " + group);
}
private T VersionField<T>(string name) where T : Node
=> collectionEditor.FindChild("VersionEditor", true, false)?.FindChild(name, true, false) as T;
private static T FindControl<T>(Node root) where T : Node
{
if (root == null)
return null;
foreach (var child in root.FindChildren("*", "", true, false))
{
if (child is T match)
return match;
}
return null;
}
private static bool HasLabel(Node root, string text)
{
foreach (var child in root.FindChildren("*", "", true, false))
{
if (child is Label label && label.Text.Contains(text, StringComparison.Ordinal))
return true;
}
return false;
}
private void PressEditorButton(string name)
{
var button = collectionEditor.FindChild(name, true, false) as Button;
if (button == null)
throw new InvalidOperationException("Editor button is absent: " + name);
button.EmitSignal(BaseButton.SignalName.Pressed);
}
private void SelectVersionPolicy(string op)
{
var policy = VersionField<OptionButton>("Policy");
for (var i = 0; i < policy.ItemCount; i++)
{
if (policy.GetItemMetadata(i).AsString() != op)
continue;
policy.Selected = i;
policy.EmitSignal(OptionButton.SignalName.ItemSelected, (long)i);
return;
}
throw new InvalidOperationException("Version policy is absent: " + op);
}
private static void MoveOccurrence(CollectionModChip chip, Vector2 delta)
{
var panel = FindControl<PanelContainer>(chip);
panel.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
panel.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseMotion { Relative = delta, ButtonMask = MouseButtonMask.Left });
panel.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = false });
}
/// <summary>A left click on the frame of a scheme mechanic or mod chip.</summary>
private static void Click(Control owner) private static void Click(Control owner)
{ {
var panel = owner?.GetChildren().OfType<PanelContainer>().FirstOrDefault(); var panel = owner?.GetChildren().OfType<PanelContainer>().FirstOrDefault();
@@ -175,57 +544,83 @@ namespace Mods.ModManagerHelpTest
private void PressHelp() private void PressHelp()
{ {
var help = Buttons(hub).FirstOrDefault(b => b.Name == "HelpButton" && b.IsVisibleInTree()); var help = Buttons(hub).FirstOrDefault(b => b.Name == "HelpButton" && b.IsVisibleInTree());
Check("the header offers help", help != null); Check("the header offers contextual help", help != null);
help?.EmitSignal(BaseButton.SignalName.Pressed); help?.EmitSignal(BaseButton.SignalName.Pressed);
} }
private void CheckHelp(string level) private void EscapeHelp()
{ {
var panel = hub.FindChildren("*", "", true, false).OfType<ModHelpPanel>().FirstOrDefault(p => p.IsVisibleInTree()); hub._UnhandledInput(new InputEventAction { Action = "ui_cancel", Pressed = true });
Check($"{level}: the help panel opens", panel != null); Check("Escape closes help", Help() != null && !Help().Visible);
var body = panel?.FindChildren("*", "RichTextLabel", true, false).OfType<RichTextLabel>().FirstOrDefault();
Check($"{level}: the help explains something", body != null && body.GetParsedText().Length > 200);
Shot(level);
CheckLevel(level);
panel?.Close();
} }
/// <summary>Every visible control the player can press, pick or type into explains itself on hover.</summary> private ModHelpPanel Help()
=> hub.FindChildren("*", "", true, false).OfType<ModHelpPanel>().FirstOrDefault();
private CollectionsGallery Gallery()
=> hub.FindChildren("*", "", true, false).OfType<CollectionsGallery>().FirstOrDefault();
private Control Annotated(string key)
=> hub.FindChildren("*", "", true, false).OfType<Control>().FirstOrDefault(c
=> c.IsVisibleInTree()
&& c.HasMeta(ModHelpPanel.HintKeyMetadata)
&& c.GetMeta(ModHelpPanel.HintKeyMetadata).AsString() == key);
private void CheckHelp(string level, Control target)
{
Check($"{level}: an annotated target exists", target != null);
ModHelpPanel help = Help();
if (help == null || target == null)
return;
help.Open();
help.ShowFor(target);
Check($"{level}: the target is explained", help.CurrentTarget == target && help.IsPinned);
var card = help.GetNode<PanelContainer>("Card");
var body = help.GetNode<RichTextLabel>("Card/Margin/Column/Body");
var title = help.GetNode<Label>("Card/Margin/Column/Title");
Check($"{level}: localized title and body are shown",
card.Visible
&& title.Text.Length > 0
&& body.GetParsedText().Length > 20
&& !title.Text.Contains("mod_manager/hint_")
&& !body.Text.Contains("mod_manager/hint_"));
Rect2 cardRect = card.GetGlobalRect();
Rect2 viewport = help.GetViewportRect();
Check($"{level}: the card stays inside the viewport",
cardRect.Position.X >= viewport.Position.X - 1f
&& cardRect.Position.Y >= viewport.Position.Y - 1f
&& cardRect.End.X <= viewport.End.X + 1f
&& cardRect.End.Y <= viewport.End.Y + 1f);
Check($"{level}: the card does not cover its target", !cardRect.Intersects(target.GetGlobalRect()));
Shot(level);
}
/// <summary>Tooltips must add information, not repeat visible captions or sit on mouse-ignoring controls.</summary>
private void CheckLevel(string level) private void CheckLevel(string level)
{ {
Shot(level); Shot(level);
var silent = new List<string>(); var echoes = new List<string>();
var deaf = new List<string>(); var deaf = new List<string>();
foreach (var control in hub.FindChildren("*", "", true, false).OfType<Control>()) foreach (var control in hub.FindChildren("*", "", true, false).OfType<Control>())
{ {
if (!control.IsVisibleInTree()) if (!control.IsVisibleInTree())
continue; continue;
var tip = control.TooltipText; var tip = control.TooltipText;
if (Interactive(control) && string.IsNullOrWhiteSpace(tip)) if (control is Button button
silent.Add(Describe(control)); && !string.IsNullOrWhiteSpace(button.Text)
&& string.Equals(button.Text.Trim(), tip?.Trim(), StringComparison.Ordinal))
{
echoes.Add(Describe(control));
}
if (!string.IsNullOrWhiteSpace(tip) && control.MouseFilter == Control.MouseFilterEnum.Ignore) if (!string.IsNullOrWhiteSpace(tip) && control.MouseFilter == Control.MouseFilterEnum.Ignore)
deaf.Add(Describe(control)); deaf.Add(Describe(control));
} }
Check($"{level}: every control has a tooltip" + (silent.Count > 0 ? ": missing on " + string.Join(", ", silent) : string.Empty), silent.Count == 0); Check($"{level}: no tooltip repeats its button" + (echoes.Count > 0 ? ": " + string.Join(", ", echoes) : string.Empty), echoes.Count == 0);
Check($"{level}: no tooltip on a control that ignores the mouse" + (deaf.Count > 0 ? ": " + string.Join(", ", deaf) : string.Empty), deaf.Count == 0); Check($"{level}: no tooltip ignores the mouse" + (deaf.Count > 0 ? ": " + string.Join(", ", deaf) : string.Empty), deaf.Count == 0);
}
private static bool Interactive(Control control)
=> (control is BaseButton || control is LineEdit || control is TextEdit)
&& control.MouseFilter != Control.MouseFilterEnum.Ignore
&& !InsidePopup(control);
/// <summary>Items inside a popup menu follow the menu itself, not their own tooltips.</summary>
private static bool InsidePopup(Control control)
{
for (Node parent = control.GetParent(); parent != null; parent = parent.GetParent())
{
if (parent is PopupMenu)
return true;
}
return false;
} }
private static string Describe(Control control) private static string Describe(Control control)
@@ -245,8 +640,8 @@ namespace Mods.ModManagerHelpTest
private static string TextOf(BaseButton button) private static string TextOf(BaseButton button)
=> button is Button clickable ? clickable.Text : string.Empty; => button is Button clickable ? clickable.Text : string.Empty;
/// <summary>With <c>--shots=&lt;dir&gt;</c> saves the window as <paramref name="name"/>.png (windowed runs only).</summary> /// <summary>With <c>--shots=&lt;dir&gt;</c> saves the settled window as <paramref name="name"/>.png.</summary>
private void Shot(string name) private async void Shot(string name)
{ {
string dir = null; string dir = null;
foreach (var arg in OS.GetCmdlineUserArgs()) foreach (var arg in OS.GetCmdlineUserArgs())
@@ -256,6 +651,13 @@ namespace Mods.ModManagerHelpTest
} }
if (string.IsNullOrEmpty(dir) || DisplayServer.GetName() == "headless") if (string.IsNullOrEmpty(dir) || DisplayServer.GetName() == "headless")
return; return;
// ViewportCapture reads the last rendered frame. Wait until the new
// target, highlight and card have actually been presented.
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
if (done && name != "scheme_f1")
return;
dir = ProjectSettings.GlobalizePath(dir); dir = ProjectSettings.GlobalizePath(dir);
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
ViewportCapture.SavePng(GetViewport(), Path.Combine(dir, name + ".png")); ViewportCapture.SavePng(GetViewport(), Path.Combine(dir, name + ".png"));
@@ -275,11 +677,23 @@ namespace Mods.ModManagerHelpTest
} }
} }
private void Finish() private async void Finish()
{ {
if (done) if (done)
return; return;
done = true; done = true;
hub?.QueueFree();
if (layoutPath != null)
{
if (layoutBackup == null)
File.Delete(layoutPath);
else
File.WriteAllBytes(layoutPath, layoutBackup);
}
// Let queued scene teardown reach the renderer before terminating
// the native window; otherwise GPU checks report live viewport RIDs.
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
GameLogger.Log("TEST", $"[TEST_RESULT] name=mod_help passed={passed} failed={failures.Count}"); GameLogger.Log("TEST", $"[TEST_RESULT] name=mod_help passed={passed} failed={failures.Count}");
foreach (var failure in failures) foreach (var failure in failures)
GameLogger.Log("TEST", $"ModHelpTest: failure: {failure}", LogLevel.Error); GameLogger.Log("TEST", $"ModHelpTest: failure: {failure}", LogLevel.Error);