Compare commits

...
6 Commits
72 changed files with 6800 additions and 88 deletions

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ obj/
gen/
obj/
bin/
art_sources/
*.tres~
*.tscn~
+23
View File
@@ -0,0 +1,23 @@
Copyright (c) 2018-2019, Alexandre Mutel
All rights reserved.
Redistribution and use in source and binary forms, with or without modification
, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+81
View File
@@ -0,0 +1,81 @@
# Mod Manager — mods, games and their documentation
Mod Manager is the **Mods & Collections** screen of the main menu. Use it to see which mods are installed, read what each one does, install or update mods from the online catalog, switch mods off, and look inside the ready-made games to see which mods they are built from.
It is part of the shell that is always loaded with the menu, so it works even before you start a game.
## Open it
In the main menu choose **Mods & Collections**. The screen has two tabs:
- **Mods** — every mod the game knows: installed ones and those offered by the catalog.
- **Collections** — the ready-made games, and the scheme of mods each game is built from.
**← Menu** returns to the main menu. The **?** button in the top-right corner, or **F1**, turns on contextual hints: hover over a control to see what it does, click to pin the hint, **Esc** to leave.
## Browse the mods
![The Mods tab: filters on the left, a grid of mod cards with cover pictures, the search box with a hint open, sorting and the Refresh catalog button.](assets/mods-list.jpg)
Each card shows the mod's cover, title, author, the catalog it comes from, how many games use it (**in 4 collections**), its state and version, and an on/off switch.
- **Search mods…** filters by title, author or identifier.
- **By name / By author / By status** sorts the list.
- **Filters** on the left narrow the list by **Status** (Not installed, Installed, Update available, Older version available, Modified locally, Disabled) and by **Categories** such as Story, Gameplay, Visual, Audio, Library, Engine and Tools. **Reset** clears filters and search.
- **Refresh catalog** downloads the current list of published mods.
*Modified locally* means the installed files differ from the catalog's copy of the same version — for example, a mod you are editing yourself.
## Read about a mod
Click a card to open its page: the cover, version selector, actions, the short description from the mod's manifest and, when the mod provides one, its **Documentation** — a full guide like this one.
![A mod page: the Audio Engine cover and version, the Reinstall and Disable buttons, "in 4 collections", the description, and the Documentation section with its language selector.](assets/mod-page.jpg)
The documentation is the guide of the **installed** version. If you choose another version in the version selector, a note reminds you that the guide below still belongs to the installed one.
**Documentation language.** The selector at the right of the Documentation heading starts on **Auto · interface language**: the guide follows the language of the game's interface. If the mod has no guide in that language, the manager shows the mod's default guide, then English, then the first available one; the line above the guide names the language actually shown (for example `EN`). You can also pick a language from the selector yourself. A mod without any guide keeps showing its manifest description.
For mod authors: put the guide in `docs/en.md` (and `docs/<language>.md` for translations, or `docs/default.md`) and images next to it, for example `docs/assets/screen.png`, referenced as `assets/screen.png`. The viewer shows headings, paragraphs, lists, quotes, code blocks, links and PNG/JPEG/WebP images; it does not run HTML, tables or animations.
## Install, update, switch off, delete
The buttons on a mod page and the switch on its card:
- **Install / Update / Downgrade / Reinstall** download the selected version from the catalog into your user folder (`user://mods/`).
- **Disable / Enable** (or the card's switch) decide whether the mod is loaded. A disabled mod keeps its files and the data in your saves.
- **Delete** removes a downloaded mod after confirmation.
After any of these a banner says **Changes will take effect after restarting the game**. Mod Manager itself cannot be deleted or disabled from its own screen.
**Installed is not the same as used.** A game loads the mods named in its collection, plus the menu shell. Installing a mod makes it available; it appears in a game only when that game's collection includes it.
## Games and collections
The **Collections** tab first shows the ready-made games. A game is a *collection*: a named set of mods with the place to start.
![The game gallery: Cthangover, My Novel and RPG Plus cards, each "Ready to play" with Mods 32/32, 11/11 and 36/36, and Edit scheme buttons.](assets/games-gallery.jpg)
A card tells you whether everything the game needs is installed (**Ready to play**, or **Missing N** / **Install (N)** / **Enable (N)**). One button installs or enables what is missing. **Edit scheme** opens the game's structure; **+ Build your own game** starts a new collection.
### The scheme of a game
![The scheme of My Novel: Story, Assets and 2D engine blocks with their mods, arrows from each dependency to the mod that needs it, and the Inspector showing the version requirement of My Novel.](assets/collection-scheme.jpg)
The scheme shows the game's *mechanics* (blocks such as Story, Assets, 2D engine) and the mods inside them. Arrows go from a dependency to the mod that needs it. Card positions and blocks are only for reading: they do not decide load order.
Select a block or a mod to see it in the **Inspector**. There you can change the version the game requires — **Any**, **At least**, **Exactly**, **Branch** (`^1` accepts any 1.x) and others. This is the game's requirement, not the version you have installed. **Check** lists problems; **Plan** shows what applying the collection would install or enable.
![The Inspector of a mod whose requirement "Exactly 999.0.0" does not match the installed 1.0.0: the requirement and the installed version are shown in red.](assets/version-mismatch.jpg)
Other buttons of the scheme: **+ Mechanic** and **Add mod** change the game, **Duplicate** makes your own copy (and offers to copy the saves), **JSON** shows the raw definition, **Save** and **Reset** keep or discard changes, **Apply collection** installs and enables what the plan lists. A built-in game you changed can be restored with **Revert to built-in**; your saves stay.
Changes to a game apply the next time it starts. Saves of a game remain usable when its mods change: data of mods that left the game is kept aside and comes back with the mod.
## If something goes wrong
1. **A mod does not appear in a game.** Check that it is enabled and that the game's collection includes it (open **Edit scheme**).
2. **"Install failed".** Check the internet connection and try **Refresh catalog** again.
3. **A game card says Missing or Problems.** Open its scheme and the **Check** tab: it lists missing mods, version gaps and conflicts.
4. **Catalog conflict.** The same mod and version is published with different content by two catalogs; prefer the catalog you trust.
5. **No documentation section.** The mod has no `docs/` guide yet; its manifest description is shown instead.
+81
View File
@@ -0,0 +1,81 @@
# Mod Manager — mods, juegos y su documentación
Mod Manager es la pantalla **Mods y colecciones** del menú principal. Sirve para ver qué mods hay instalados, leer qué hace cada uno, instalar o actualizar mods desde el catálogo en línea, desactivar mods y mirar dentro de los juegos ya hechos para ver de qué mods están hechos.
Forma parte de la carcasa que siempre se carga junto con el menú, así que funciona incluso antes de que empieces una partida.
## Cómo abrirlo
En el menú principal elige **Mods y colecciones**. La pantalla tiene dos pestañas:
- **Mods** — todos los mods que el juego conoce: los instalados y los que ofrece el catálogo.
- **Colecciones** — los juegos ya hechos y el esquema de mods con el que está montado cada uno.
**← Menú** vuelve al menú principal. El botón **?** de la esquina superior derecha, o **F1**, activa las sugerencias contextuales: pasa el ratón sobre un control para ver qué hace, haz clic para fijar la sugerencia y pulsa **Esc** para salir.
## Explorar los mods
![La pestaña Mods: filtros a la izquierda, una cuadrícula de tarjetas de mods con imágenes de portada, el cuadro de búsqueda con una sugerencia abierta, el orden y el botón Refresh catalog.](assets/mods-list.jpg)
Cada tarjeta muestra la portada del mod, su título, el autor, el catálogo del que viene, cuántos juegos lo usan (**en 4 colecciones**), su estado y versión, y un interruptor de encendido/apagado.
- **Buscar mods...** filtra por título, autor o identificador.
- **Por nombre / Por autor / Por estado** ordenan la lista.
- **Filtros** a la izquierda acotan la lista por **Estado** (No instalado, Instalado, Hay actualización, Hay una versión anterior, Modificado localmente, Desactivado) y por **Categorías** como Historia, Jugabilidad, Visual, Audio, Biblioteca, Motor y Herramientas. **Restablecer** limpia filtros y búsqueda.
- **Actualizar catálogo** descarga la lista actual de mods publicados.
*Modificado localmente* significa que los archivos instalados difieren de la copia de esa misma versión en el catálogo; por ejemplo, un mod que estás editando tú.
## Leer sobre un mod
Haz clic en una tarjeta para abrir su página: la portada, el selector de versión, las acciones, la descripción corta del manifiesto del mod y, cuando el mod la ofrece, su **Documentación**: una guía completa como esta.
![Una página de mod: la portada y la versión de Audio Engine, los botones Reinstall y Disable, "in 4 collections", la descripción y la sección Documentation con su selector de idioma.](assets/mod-page.jpg)
La documentación es la guía de la versión **instalada**. Si eliges otra versión en el selector, un aviso te recuerda que la guía de abajo sigue siendo la del mod instalado.
**Idioma de la documentación.** El selector a la derecha del encabezado Documentación arranca en **Auto · idioma de la interfaz**: la guía sigue el idioma de la interfaz del juego. Si el mod no tiene guía en ese idioma, el gestor muestra primero la guía predeterminada del mod, luego la inglesa y luego la primera disponible; la línea sobre la guía indica el idioma realmente mostrado (por ejemplo `EN`). También puedes elegir un idioma en el selector. Un mod sin ninguna guía sigue mostrando la descripción de su manifiesto.
Para autores de mods: pon la guía en `docs/en.md` (las traducciones en `docs/<idioma>.md`, o `docs/default.md`) y las imágenes al lado, por ejemplo `docs/assets/screen.png`, referenciada como `assets/screen.png`. El visor entiende encabezados, párrafos, listas, citas, bloques de código, enlaces e imágenes PNG/JPEG/WebP; no ejecuta HTML, tablas ni animaciones.
## Instalar, actualizar, desactivar, eliminar
Los botones de la página del mod y el interruptor de su tarjeta:
- **Instalar / Actualizar / Volver a versión anterior / Reinstalar** descargan la versión elegida del catálogo a tu carpeta de usuario (`user://mods/`).
- **Desactivar / Activar** (o el interruptor de la tarjeta) deciden si el mod se carga. Un mod desactivado conserva sus archivos y los datos de tus partidas.
- **Eliminar** borra un mod descargado tras confirmar.
Después de cualquiera de estas acciones aparece un aviso: **Los cambios surtirán efecto al reiniciar el juego**. El propio Mod Manager no puede eliminarse ni desactivarse desde su propia pantalla.
**Instalado no es lo mismo que usado.** Un juego carga los mods que nombra su colección, más la carcasa del menú. Instalar un mod lo deja disponible; solo aparece en un juego cuando la colección de ese juego lo incluye.
## Juegos y colecciones
La pestaña **Colecciones** muestra primero los juegos ya hechos. Un juego es una *colección*: un conjunto de mods con nombre y el lugar donde empezar.
![La galería de juegos: tarjetas de Cthangover, My Novel y RPG Plus, cada una "Ready to play" con Mods 32/32, 11/11 y 36/36, y botones Edit scheme.](assets/games-gallery.jpg)
Una tarjeta te dice si está instalado todo lo que el juego necesita (**Listo para jugar**, o **Faltan N** / **Instalar (N)** / **Activar (N)**). Un solo botón instala o activa lo que falta. **Editar esquema** abre la estructura del juego; **+ Crea tu propio juego** empieza una colección nueva.
### El esquema de un juego
![El esquema de My Novel: los bloques Story, Assets y 2D engine con sus mods, flechas de cada dependencia al mod que la necesita, y el Inspector mostrando el requisito de versión de My Novel.](assets/collection-scheme.jpg)
El esquema muestra las *mecánicas* del juego (bloques como Story, Assets, 2D engine) y los mods dentro de ellas. Las flechas van de una dependencia al mod que la necesita. Las posiciones de las tarjetas y los bloques son solo para leer: no deciden el orden de carga.
Selecciona un bloque o un mod para verlo en el **Inspector**. Ahí puedes cambiar la versión que exige el juego: **Cualquiera**, **Como mínimo**, **Exacta**, **Rama** (`^1` acepta cualquier 1.x) y otras. Es el requisito del juego, no la versión que tienes instalada. **Comprobar** enumera problemas; **Plan** muestra qué instalaría o activaría aplicar la colección.
![El Inspector de un mod cuyo requisito "Exactly 999.0.0" no coincide con la versión instalada 1.0.0: el requisito y la versión instalada aparecen en rojo.](assets/version-mismatch.jpg)
Otros botones del esquema: **+ Mecánica** y **Añadir mod** cambian el juego, **Duplicar** hace tu propia copia (y ofrece copiar las partidas), **JSON** muestra la definición en crudo, **Guardar** y **Restablecer** conservan o descartan los cambios, **Aplicar colección** instala y activa lo que enumera el plan. Un juego integrado que hayas cambiado se restaura con **Restaurar la integrada**; tus partidas se quedan.
Los cambios de un juego se aplican la próxima vez que arranque. Las partidas siguen sirviendo cuando cambian sus mods: los datos de los mods que salieron del juego se guardan aparte y vuelven con el mod.
## Si algo falla
1. **Un mod no aparece en un juego.** Comprueba que está activado y que la colección del juego lo incluye (abre **Editar esquema**).
2. **«No se pudo instalar».** Revisa la conexión a internet y prueba otra vez **Actualizar catálogo**.
3. **Una tarjeta dice Faltan o Problemas.** Abre su esquema y la pestaña **Comprobar**: enumera los mods que faltan, las diferencias de versión y los conflictos.
4. **Conflicto de catálogos.** El mismo mod y versión está publicado con contenido distinto por dos catálogos; fíate del catálogo en el que confíes.
5. **No hay sección de documentación.** El mod todavía no tiene guía en `docs/`; en su lugar se muestra la descripción del manifiesto.
+81
View File
@@ -0,0 +1,81 @@
# Mod Manager — MOD、ゲーム、そのドキュメント
Mod Manager はメインメニューの **MOD とコレクション** 画面です。どの MOD がインストールされているか、それぞれが何をするのかを確認し、オンラインカタログから MOD をインストール・更新し、無効にし、でき上がりのゲームの中身を見て、どの MOD で構成されているかを確かめられます。
常にメニューとともに読み込まれるシェルの一部なので、ゲームを始める前から使えます。
## 開き方
メインメニューで **MOD とコレクション** を選びます。画面には二つのタブがあります:
- **MOD** — ゲームが知っているすべての MOD:インストール済みのものとカタログが提案するもの。
- **コレクション** — でき上がりのゲームと、それぞれを構成する MOD の構成。
**← メニュー** でメインメニューに戻ります。右上の **?** ボタン、または **F1** でコンテキストヒントが有効になります。コントロールにマウスを乗せると働きが分かり、クリックでヒントを固定し、**Esc** で終わります。
## MOD を見て回る
![MOD タブ:左にフィルター、カバー付き MOD カードのグリッド、ヒントが開いた検索ボックス、並べ替えとカタログを更新ボタン。](assets/mods-list.jpg)
各カードには、MOD のカバー、タイトル、作者、出どころのカタログ、使っているゲームの数(**4 個のコレクションに含まれる**)、状態とバージョン、そしてオン/オフのスイッチが並びます。
- **MOD を検索...** はタイトル・作者・識別子で絞り込みます。
- **名前順 / 作者順 / 状態順** がリストを並べ替えます。
- **フィルター** は左側で、**状態**(未インストール、インストール済み、更新あり、古いバージョンあり、ローカルで変更、無効)と**カテゴリー**(ストーリー、ゲームプレイ、ビジュアル、オーディオ、ライブラリ、エンジン、ツールなど)でリストを狭めます。**リセット**でフィルターと検索を解除します。
- **カタログを更新** は公開中 MOD の最新一覧をダウンロードします。
*ローカルで変更* は、インストール済みのファイルがカタログの同じバージョンのコピーと違うことを意味します。たとえば自分で編集中の MOD などです。
## MOD の詳細を見る
カードをクリックするとページが開きます:カバー、バージョン選択、アクション、manifest から来る短い説明、そして MOD が用意していれば**ドキュメント** — この文書のような完全なガイドです。
![ある MOD のページ:Audio Engine のカバーとバージョン、Reinstall と Disable のボタン、"in 4 collections"、説明、そして言語セレクターつきの Documentation セクション。](assets/mod-page.jpg)
ドキュメントは**インストール済み**バージョンのガイドです。バージョン選択で別のバージョンを選ぶと、下のガイドが依然としてインストール済み版のものであることを注意書きが教えてくれます。
**ドキュメントの言語。** 見出し「ドキュメント」の右にあるセレクターは **自動 · UIの言語** で始まります。ガイドはゲームのインターフェース言語に従います。その言語のガイドが MOD にない場合、マネージャーは MOD の既定のガイド、次に英語、そして最初に見つかったものを表示します。ガイドの上の行が実際に表示している言語(たとえば `EN`)を示します。セレクターで言語を自分で選ぶこともできます。ガイドを一つも持たない MOD は、manifest の説明を表示し続けます。
MOD 作者向け:ガイドは `docs/en.md` に(翻訳は `docs/<言語>.md` か `docs/default.md`)、画像はその隣に置きます。たとえば `docs/assets/screen.png` を置いて `assets/screen.png` と参照します。ビューワーは見出し・段落・リスト・引用・コードブロック・リンク・PNG/JPEG/WebP 画像を表示します。HTML、表、アニメーションは実行しません。
## インストール、更新、無効化、削除
MOD ページのボタンとカードのスイッチ:
- **インストール / 更新 / ダウングレード / 再インストール** は選んだバージョンをカタログからユーザーフォルダー(`user://mods/`)へダウンロードします。
- **無効にする / 有効にする**(またはカードのスイッチ)が MOD を読み込むかどうかを決めます。無効の MOD もファイルとセーブ内のデータは残ります。
- **削除** は確認のうえダウンロード済みの MOD を取り除きます。
いずれの操作の後も **変更はゲームの再起動後に反映されます** というバナーが出ます。Mod Manager 自身はこの画面から削除も無効化もできません。
**インストール済みは使用中ではない。** ゲームはコレクションに名前のある MOD とメニューシェルを読み込みます。インストールは MOD を利用可能にするだけです。ゲームに現れるのは、そのゲームのコレクションが MOD を含むときです。
## ゲームとコレクション
**コレクション** タブはまずでき上がりのゲームを示します。ゲームとは *コレクション* のこと:名前のついた MOD のセットと、開始場所です。
![ゲームギャラリー:Cthangover、My Novel、RPG Plus のカード。それぞれ "Ready to play" で Mods 32/32、11/11、36/36、構成を編集ボタンつき。](assets/games-gallery.jpg)
カードは、ゲームに必要なものがすべて揃っているかを教えます(**プレイできます**、または **N 個不足** / **インストール(N)** / **有効化(N)**)。一つのボタンで不足分をインストールまたは有効化します。**構成を編集** はゲームの中身を開き、**+ 自分のゲームを作る** は新しいコレクションを始めます。
### ゲームの構成
![My Novel の構成:Story、Assets、2D engine のブロックとそれぞれの MOD、各依存関係からそれを必要とする MOD への矢印、そして My Novel のバージョン要件を表示するインスペクター。](assets/collection-scheme.jpg)
構成図はゲームの *メカニクス*(Story、Assets、2D engine などのブロック)とその中の MOD を示します。矢印は依存関係から、それを必要とする MOD へ向かいます。カードの位置とブロックは読むためのもの:ロード順序は決めません。
ブロックか MOD を選ぶと **インスペクター** に表示されます。そこでゲームが要求するバージョンを変えられます:**任意**、**以上**、**固定**、**ブランチ**(`^1` なら 1.x すべて)など。これはゲーム側の要求であり、インストール済みのバージョンではありません。**チェック** は問題を一覧し、**計画** はコレクション適用で何がインストール・有効化されるかを示します。
![要件 "Exactly 999.0.0" がインストール済み 1.0.0 と合わない MOD のインスペクター:要件とインストール済みバージョンが赤で表示されます。](assets/version-mismatch.jpg)
構成図のその他のボタン:**+ メカニクス** と **MOD を追加** がゲームを変え、**複製** が自分のコピーを作り(セーブのコピーも提案します)、**JSON** が生の定義を示し、**保存** と **リセット** が変更を残すか捨て、**コレクションを適用** が計画どおりにインストールと有効化を行います。変更した組み込みのゲームは **組み込み版に戻す** で元に戻せます。セーブは残ります。
ゲームへの変更は次回の起動で反映されます。ゲームのセーブは MOD が変わっても使えます:ゲームから離れた MOD のデータは脇に保管され、MOD とともに戻ります。
## うまく動かないとき
1. **MOD がゲームに現れない。** 有効になっているか、ゲームのコレクションに含まれているか確認してください(**構成を編集**を開く)。
2. **「インストールできませんでした」。** インターネット接続を確認し、**カタログを更新** をもう一度試してください。
3. **カードに「不足」や「問題」と出る。** その構成と **チェック** タブを開いてください:足りない MOD、バージョンのずれ、競合が一覧が出ます。
4. **カタログの競合。** 同じ MOD とバージョンが異なる内容で二つのカタログに公開されています。信頼できるカタログの方を使ってください。
5. **ドキュメントのセクションがない。** その MOD にはまだ `docs/` のガイドがありません。manifest の説明が代わりに表示されます。
+81
View File
@@ -0,0 +1,81 @@
# Mod Manager — 모드, 게임, 그리고 모드 문서
Mod Manager는 메인 메뉴의 **모드와 컬렉션** 화면입니다. 어떤 모드가 설치되어 있는지, 각 모드가 무엇을 하는지 살펴보고, 온라인 카탈로그에서 모드를 설치·업데이트하고, 모드를 끄고, 만들어진 게임의 속을 들여다보며 어떤 모드로 이루어져 있는지 확인할 수 있습니다.
메뉴와 함께 항상 로드되는 셸의 일부이므로 게임을 시작하기 전에도 동작합니다.
## 열기
메인 메뉴에서 **모드와 컬렉션**을 고르세요. 화면에는 탭이 둘 있습니다:
- **모드** — 게임이 아는 모든 모드: 설치된 것과 카탈로그가 내놓은 것.
- **컬렉션** — 만들어진 게임들과 각 게임을 이룬 모드 구성.
**← 메뉴**는 메인 메뉴로 돌아갑니다. 오른쪽 위의 **?** 버튼이나 **F1**은 상황별 힌트를 켭니다. 컨트롤에 포인터를 올리면 하는 일이 보이고, 클릭하면 힌트가 고정되며, **Esc**로 나옵니다.
## 모드 둘러보기
![모드 탭: 왼쪽 필터, 표지가 붙은 모드 카드 격자, 힌트가 열린 검색 상자, 정렬과 Refresh catalog 버튼.](assets/mods-list.jpg)
각 카드에는 모드의 표지, 제목, 제작자, 나온 카탈로그, 몇 개의 게임이 쓰는지(**컬렉션 4개에 포함**), 상태와 버전, 그리고 켜기/끄기 스위치가 나옵니다.
- **모드 검색...** — 제목, 제작자 또는 식별자로 걸러 봅니다.
- **이름순 / 작성자순 / 상태순** — 목록을 정렬합니다.
- **필터**는 왼쪽에서 목록을 **상태**(설치 안 됨, 설치됨, 업데이트 있음, 이전 버전 있음, 로컬에서 수정됨, 비활성화됨)와 **분류**(스토리, 게임플레이, 비주얼, 오디오, 라이브러리, 엔진, 도구 등)로 좁힙니다. **초기화**는 필터와 검색을 지웁니다.
- **카탈로그 새로 고침**은 공개된 모드의 현재 목록을 내려받습니다.
*로컬에서 수정됨*은 설치된 파일이 카탈로그의 같은 버전 사본과 다르다는 뜻입니다. 예컨대 직접 고치고 있는 모드이지요.
## 모드 읽어 보기
카드를 클릭하면 페이지가 열립니다: 표지, 버전 선택, 작업들, 모드 manifest의 짧은 설명, 그리고 모드가 갖추고 있다면 **문서** — 이 문서 같은 완전한 안내서입니다.
![모드 페이지: Audio Engine의 표지와 버전, Reinstall과 Disable 버튼, "in 4 collections", 설명, 그리고 언어 선택기가 있는 Documentation 섹션.](assets/mod-page.jpg)
문서는 **설치된** 버전의 안내서입니다. 버전 선택기에서 다른 버전을 고르면, 아래 안내서가 여전히 설치된 버전의 것이라는 알림이 떠옵니다.
**문서 언어.** 문서 제목 오른쪽 선택기는 **자동 · 인터페이스 언어**로 시작합니다. 안내서는 게임 인터페이스 언어를 따릅니다. 그 언어의 안내서가 모드에 없으면 관리자는 모드의 기본 안내서, 그다음 영어, 그다음 처음 발견되는 문서를 보여 줍니다. 안내서 위의 한 줄이 실제 표시 언어(예: `EN`)를 알려 줍니다. 선택기에서 직접 언어를 고를 수도 있습니다. 안내서가 하나도 없는 모드는 manifest 설명을 계속 보여 줍니다.
모드 제작자를 위해: 안내서는 `docs/en.md`에(번역은 `docs/<언어>.md` 또는 `docs/default.md`), 이미지는 곁에 두세요. 예컨대 `docs/assets/screen.png`로 두고 `assets/screen.png`로 참조합니다. 뷰어는 제목, 문단, 목록, 인용, 코드 블록, 링크, PNG/JPEG/WebP 이미지를 이해합니다. HTML, 표, 애니메이션은 실행하지 않습니다.
## 설치, 업데이트, 끄기, 삭제
모드 페이지의 버튼과 카드의 스위치:
- **설치 / 업데이트 / 다운그레이드 / 다시 설치**는 고른 버전을 카탈로그에서 사용자 폴더(`user://mods/`)로 내려받습니다.
- **끄기 / 켜기**(또는 카드의 스위치)는 모드를 불러올지 정합니다. 꺼진 모드도 파일과 저장 속 데이터는 유지됩니다.
- **삭제**는 확인 후 내려받은 모드를 제거합니다.
이들 작업 뒤에는 **변경 사항은 게임을 다시 시작한 뒤에 적용됩니다** 배너가 뜹니다. Mod Manager 자신은 이 화면에서 삭제하거나 끌 수 없습니다.
**설치됨은 사용 중이 아닙니다.** 게임은 자기 컬렉션이 이름을 적은 모드와 메뉴 셸을 불러옵니다. 설치는 모드를 이용 가능하게 할 뿐입니다. 그 게임의 컬렉션이 모드를 담을 때 비로소 게임에 나타납니다.
## 게임과 컬렉션
**컬렉션** 탭은 먼저 만들어진 게임을 보여 줍니다. 게임이란 *컬렉션*입니다: 이름 붙은 모드 세트와 시작 장소.
![게임 갤러리: Cthangover, My Novel, RPG Plus 카드, 각각 "Ready to play"와 Mods 32/32, 11/11, 36/36, 그리고 Edit scheme 버튼.](assets/games-gallery.jpg)
카드는 게임에 필요한 모든 것이 설치됐는지 알려 줍니다(**플레이 준비 완료**, 또는 **N개 부족** / **설치 (N)** / **켜기 (N)**). 한 버튼으로 빠진 것을 설치하거나 켭니다. **구성 편집**은 게임의 구조를 열고, **+ 나만의 게임 만들기**는 새 컬렉션을 시작합니다.
### 게임 구성
![My Novel의 구성: Story, Assets, 2D engine 블록과 그 모드들, 각 의존성에서 이를 필요로 하는 모드로 가는 화살표, 그리고 My Novel의 버전 요구 사항을 보여 주는 인스펙터.](assets/collection-scheme.jpg)
구성도는 게임의 *메카닉*(Story, Assets, 2D engine 같은 블록)과 그 안의 모드를 보여 줍니다. 화살표는 의존성에서 그것을 필요로 하는 모드로 향합니다. 카드 위치와 블록은 읽기용일 뿐: 로드 순서를 정하지 않습니다.
블록이나 모드를 고르면 **인스펙터**에 나타납니다. 거기서 게임이 요구하는 버전을 바꿀 수 있습니다: **모든 버전**, **최소 버전**, **정확한 버전**, **버전 분기**(`^1`은 1.x 모두 허용) 등. 이것은 게임의 요구 사항이지 설치된 버전이 아닙니다. **검사**는 문제를 나열하고, **계획**은 컬렉션 적용이 무엇을 설치하고 켤지 보여 줍니다.
![요구 사항 "Exactly 999.0.0"이 설치된 1.0.0과 맞지 않는 모드의 인스펙터: 요구 사항과 설치 버전이 빨간색으로 표시됩니다.](assets/version-mismatch.jpg)
구성도의 다른 버튼들: **+ 메카닉**과 **모드 추가**는 게임을 바꾸고, **복제**는 자기 사본을 만들며(저장 복사도 제안합니다), **JSON**은 원본 정의를 보여 주고, **저장**과 **초기화**는 변경을 남기거나 버리며, **컬렉션 적용**은 계획에 나온 것을 설치하고 켭니다. 바꾼 기본 제공 게임은 **기본 제공 버전으로 되돌리기**로 복원할 수 있습니다. 저장 파일은 남습니다.
게임 변경은 다음 시작 때 적용됩니다. 게임의 저장은 모드가 바뀌어도 쓸 수 있습니다: 게임을 떠난 모드의 데이터는 따로 보관되고 모드와 함께 돌아옵니다.
## 문제가 생겼을 때
1. **모드가 게임에 나타나지 않는다.** 켜져 있는지, 그 게임의 컬렉션이 담고 있는지 확인하세요(**구성 편집**을 여세요).
2. **"설치 실패".** 인터넷 연결을 확인하고 **카탈로그 새로 고침**을 다시 시도하세요.
3. **게임 카드에 부족 또는 문제가 표시된다.** 그 구성과 **검사** 탭을 여세요: 빠진 모드, 버전 차이, 충돌이 나열됩니다.
4. **카탈로그 충돌.** 같은 모드와 버전이 두 카탈로그에 다른 내용으로 공개되었습니다. 신뢰하는 카탈로그를 따르세요.
5. **문서 섹션이 없다.** 그 모드에는 아직 `docs/` 안내서가 없습니다. 대신 manifest 설명이 보입니다.
+81
View File
@@ -0,0 +1,81 @@
# Mod Manager — моды, игры и их документация
Mod Manager — это экран **Моды и коллекции** главного меню. Здесь видно, какие моды установлены, что делает каждый из них, отсюда устанавливают и обновляют моды из онлайн-каталога, выключают моды и заглядывают внутрь готовых игр, чтобы понять, из каких модов они собраны.
Он входит в оболочку, которая всегда загружается вместе с меню, поэтому работает ещё до запуска любой игры.
## Как открыть
В главном меню выберите **Моды и коллекции**. У экрана две вкладки:
- **Моды** — все моды, которые известны игре: установленные и предложенные каталогом.
- **Коллекции** — готовые игры и схема модов, из которых собрана каждая.
Кнопка **← Меню** возвращает в главное меню. Кнопка **?** в правом верхнем углу, или **F1**, включает контекстные подсказки: наведите курсор на элемент управления, чтобы узнать, что он делает, щёлкните, чтобы закрепить подсказку, **Esc** — чтобы уйти.
## Обзор модов
![Вкладка «Моды»: фильтры слева, сетка карточек модов с обложками, поле поиска с открытой подсказкой, сортировка и кнопка Refresh catalog.](assets/mods-list.jpg)
На каждой карточке — обложка, название, автор, каталог, откуда мод, сколькими играми он используется (**в 4 коллекциях**), состояние и версия, а также переключатель вкл/выкл.
- **Поиск модов...** фильтрует по названию, автору или идентификатору.
- **По имени / По автору / По статусу** сортируют список.
- **Фильтры** слева сужают список по **статусу** (Не установлен, Установлен, Есть обновление, Доступна старая версия, Изменён локально, Выключен) и по **категориям** — например Сюжет, Геймплей, Визуал, Аудио, Библиотека, Движок, Инструменты. **Сбросить** очищает фильтры и поиск.
- **Обновить каталог** скачивает актуальный список опубликованных модов.
*Изменён локально* значит, что установленные файлы отличаются от копии той же версии в каталоге, — например, мод, который вы правите сами.
## Описание мода
Щёлкните по карточке, чтобы открыть её страницу: обложка, выбор версии, действия, краткое описание из манифеста мода и, если мод его предоставляет, **Документация** — подробное руководство вроде этого.
![Страница мода: обложка и версия Audio Engine, кнопки Reinstall и Disable, «in 4 collections», описание и раздел Documentation с выбором языка.](assets/mod-page.jpg)
Документация — это руководство **установленной** версии. Если выбрать в селекторе другую версию, пометка напомнит, что руководство ниже по-прежнему относится к установленной.
**Язык документации.** Селектор справа от заголовка «Документация» стартует на значении **Авто · язык интерфейса**: руководство следует за языком интерфейса игры. Если руководства на этом языке у мода нет, менеджер покажет руководство мода по умолчанию, затем английское, затем первое доступное; строка над руководством называет реально показанный язык (например, `EN`). Язык можно выбрать и вручную. У мода без руководств вместо них остаётся описание из манифеста.
Авторам модов: положите руководство в `docs/en.md` (переводы — в `docs/<язык>.md`, либо `docs/default.md`), а картинки рядом, например `docs/assets/screen.png`, со ссылкой `assets/screen.png`. Просмотрщик понимает заголовки, абзацы, списки, цитаты, блоки кода, ссылки и картинки PNG/JPEG/WebP; HTML, таблицы и анимации он не исполняет.
## Установка, обновление, выключение, удаление
Кнопки на странице мода и переключатель на карточке:
- **Установить / Обновить / Понизить / Переустановить** скачивают выбранную версию из каталога в вашу пользовательскую папку (`user://mods/`).
- **Выключить / Включить** (или переключатель на карточке) решают, загружается ли мод. Выключенный мод хранит файлы и данные в ваших сохранениях.
- **Удалить** стирает скачанный мод после подтверждения.
После любого из этих действий появляется плашка **Изменения вступят в силу после перезапуска игры**. Сам Mod Manager нельзя удалить или выключить с его собственного экрана.
**Установлен — ещё не значит используется.** Игра загружает моды, названные в её коллекции, плюс оболочку меню. Установка делает мод доступным; в игре он появится, только когда коллекция этой игры его включит.
## Игры и коллекции
Вкладка **Коллекции** сначала показывает готовые игры. Игра — это *коллекция*: поименованный набор модов и место, с которого начинается игра.
![Галерея игр: карточки Cthangover, My Novel и RPG Plus, у каждой «Ready to play», счетчики модов 32/32, 11/11 и 36/36 и кнопки Edit scheme.](assets/games-gallery.jpg)
Карточка сообщает, установлено ли всё, что нужно игре (**Готова к игре**, либо **Не хватает N** / **Установить (N)** / **Включить (N)**). Одна кнопка устанавливает или включает недостающее. **Изменить схему** открывает устройство игры; **+ Собрать свою игру** начинает новую коллекцию.
### Схема игры
![Схема My Novel: блоки Story, Assets и 2D engine со своими модами, стрелки от каждой зависимости к моду, которому она нужна, и инспектор с требованием к версии My Novel.](assets/collection-scheme.jpg)
Схема показывает *механики* игры (блоки вроде Story, Assets, 2D engine) и моды внутри них. Стрелки идут от зависимости к моду, которому она нужна. Положение карточек и сами блоки нужны только для чтения: порядок загрузки они не задают.
Выберите блок или мод — он откроется в **Инспекторе**. Там меняется версия, которую требует игра: **Любая**, **Не ниже**, **Точно**, **Ветка** (`^1` принимает любую 1.x) и другие. Это требование игры, а не версия, установленная у вас. **Проверка** перечисляет проблемы; **План** показывает, что применит установка коллекции.
![Инспектор мода, у которого требование «Exactly 999.0.0» не совпадает с установленной версией 1.0.0: требование и установленная версия показаны красным.](assets/version-mismatch.jpg)
Прочие кнопки схемы: **+ Механика** и **Добавить мод** меняют игру, **Дублировать** делает вашу копию (и предлагает скопировать сохранения), **JSON** показывает исходное описание, **Сохранить** и **Сбросить** оставляют или отбрасывают правки, **Применить коллекцию** устанавливает и включает то, что перечислено в плане. Изменённую встроенную игру можно вернуть кнопкой **Вернуть встроенную**; сохранения останутся на месте.
Изменения игры применятся при её следующем запуске. Сохранения игры остаются рабочими при смене её модов: данные модов, покинувших игру, откладываются в сторону и возвращаются вместе с модом.
## Если что-то пошло не так
1. **Мода нет в игре.** Проверьте, что он включён, и что коллекция игры его содержит (откройте **Изменить схему**).
2. **«Не удалось установить».** Проверьте подключение к интернету и снова нажмите **Обновить каталог**.
3. **На карточке игры «Не хватает» или «Проблемы».** Откройте её схему и вкладку **Проверка**: там перечислены недостающие моды, несовпадения версий и конфликты.
4. **Конфликт каталогов.** Один и тот же мод и версия опубликованы с разным содержимым в двух каталогах; предпочитайте каталог, которому доверяете.
5. **Нет раздела документации.** У мода пока нет руководства в `docs/`; вместо него показано описание из манифеста.
+81
View File
@@ -0,0 +1,81 @@
# Mod Manager — mod, game và tài liệu của chúng
Mod Manager là màn hình **Mod và bộ sưu tập** của menu chính. Dùng nó để xem mod nào đã cài, đọc xem mỗi mod làm gì, cài hoặc cập nhật mod từ danh mục trực tuyến, tắt mod, và soi vào các game làm sẵn để thấy chúng được ghép từ những mod nào.
Nó thuộc lớp vỏ luôn được tải cùng menu, nên hoạt động ngay cả trước khi bạn bắt đầu một game.
## Mở nó
Trong menu chính chọn **Mod và bộ sưu tập**. Màn hình có hai thẻ:
- **Mod** — mọi mod mà game biết: những mod đã cài và những mod danh mục đề nghị.
- **Bộ sưu tập** — các game làm sẵn, và sơ đồ mod cấu thành từng game.
**← Menu** quay về menu chính. Nút **?** ở góc trên phải, hoặc **F1**, bật gợi ý theo ngữ cảnh: đưa chuột lên một điều khiển để xem nó làm gì, nhấp để ghim gợi ý, **Esc** để thoát.
## Duyệt mod
![Thẻ Mod: bộ lọc bên trái, lưới thẻ mod có ảnh bìa, ô tìm kiếm đang mở gợi ý, sắp xếp và nút Refresh catalog.](assets/mods-list.jpg)
Mỗi thẻ cho thấy ảnh bìa của mod, tiêu đề, tác giả, danh mục mà nó đến từ, bao nhiêu game dùng nó (**trong 4 bộ sưu tập**), trạng thái và phiên bản, cùng một công tắc bật/tắt.
- **Tìm mod...** lọc theo tiêu đề, tác giả hoặc định danh.
- **Theo tên / Theo tác giả / Theo trạng thái** sắp xếp danh sách.
- **Bộ lọc** bên trái thu hẹp danh sách theo **Trạng thái** (Chưa cài, Đã cài, Có bản cập nhật, Có phiên bản cũ hơn, Đã sửa cục bộ, Đã tắt) và theo **Thể loại** như Cốt truyện, Lối chơi, Hình ảnh, Âm thanh, Thư viện, Engine và Công cụ. **Đặt lại** xóa bộ lọc và tìm kiếm.
- **Làm mới danh mục** tải về danh sách mod đang xuất bản hiện thời.
*Đã sửa cục bộ* nghĩa là các tệp đã cài khác với bản sao cùng phiên bản trên danh mục — ví dụ, một mod bạn đang tự sửa.
## Đọc về một mod
Nhấp vào một thẻ để mở trang của nó: ảnh bìa, bộ chọn phiên bản, các thao tác, mô tả ngắn từ manifest của mod và, khi mod có cung cấp, **Tài liệu** của nó — một cẩm nang đầy đủ như bản bạn đang đọc.
![Một trang mod: ảnh bìa và phiên bản của Audio Engine, các nút Reinstall và Disable, "in 4 collections", phần mô tả, và khối Documentation với bộ chọn ngôn ngữ.](assets/mod-page.jpg)
Tài liệu là cẩm nang của phiên bản **đã cài**. Nếu bạn chọn phiên bản khác trong bộ chọn, một dòng nhắc sẽ cho biết hướng dẫn bên dưới vẫn thuộc về bản đã cài.
**Ngôn ngữ tài liệu.** Bộ chọn bên phải tiêu đề Tài liệu khởi đầu ở **Tự động · ngôn ngữ giao diện**: cẩm nang theo ngôn ngữ giao diện của game. Nếu mod không có hướng dẫn bằng ngôn ngữ đó, trình quản lý hiển thị hướng dẫn mặc định của mod, rồi tiếng Anh, rồi tài liệu đầu tiên tìm được; dòng phía trên cẩm nang ghi ngôn ngữ thực tế đang hiện (ví dụ `EN`). Bạn cũng có thể tự chọn ngôn ngữ trong bộ chọn. Mod không có hướng dẫn nào sẽ tiếp tục hiển thị mô tả trong manifest.
Cho tác giả mod: đặt cẩm nang vào `docs/en.md` (bản dịch ở `docs/<ngôn ngữ>.md`, hoặc `docs/default.md`) và ảnh cạnh đó, ví dụ `docs/assets/screen.png`, được dẫn như `assets/screen.png`. Trình xem hiểu tiêu đề, đoạn văn, danh sách, trích dẫn, khối mã, liên kết và ảnh PNG/JPEG/WebP; nó không chạy HTML, bảng hay hoạt ảnh.
## Cài, cập nhật, tắt, xóa
Các nút trên trang mod và công tắc trên thẻ:
- **Cài đặt / Cập nhật / Hạ phiên bản / Cài lại** tải phiên bản đã chọn từ danh mục về thư mục người dùng của bạn (`user://mods/`).
- **Tắt / Bật** (hoặc công tắc trên thẻ) quyết định mod có được nạp hay không. Mod bị tắt vẫn giữ tệp và dữ liệu trong các bản lưu của bạn.
- **Xóa** gỡ một mod đã tải về sau khi xác nhận.
Sau bất kỳ thao tác nào ở trên, một dải thông báo xuất hiện: **Thay đổi có hiệu lực sau khi khởi động lại game**. Chính Mod Manager không thể bị xóa hay tắt từ màn hình của nó.
**Đã cài không có nghĩa là đang được dùng.** Một game nạp các mod được nêu trong bộ sưu tập của nó, cộng lớp vỏ menu. Cài mod chỉ làm mod khả dụng; nó xuất hiện trong game khi bộ sưu tập của game đó bao gồm nó.
## Game và bộ sưu tập
Thẻ **Bộ sưu tập** đầu tiên hiển thị các game làm sẵn. Một game là một *bộ sưu tập*: một bộ mod có tên cùng địa điểm bắt đầu.
![Phòng trưng bày game: các thẻ Cthangover, My Novel và RPG Plus, đều "Ready to play" với Mods 32/32, 11/11 và 36/36, và nút Edit scheme.](assets/games-gallery.jpg)
Thẻ cho bạn biết mọi thứ game cần đã được cài chưa (**Sẵn sàng để chơi**, hoặc **Thiếu N** / **Cài (N)** / **Bật (N)**). Một nút cài hoặc bật phần còn thiếu. **Sửa sơ đồ** mở cấu trúc của game; **+ Tự làm game của bạn** bắt đầu một bộ sưu tập mới.
### Sơ đồ của một game
![Sơ đồ của My Novel: các khối Story, Assets và 2D engine cùng mod của chúng, mũi tên từ mỗi phụ thuộc tới mod cần nó, và thanh thuộc tính cho thấy yêu cầu phiên bản của My Novel.](assets/collection-scheme.jpg)
Sơ đồ cho thấy các *cơ chế* của game (những khối như Story, Assets, 2D engine) và các mod bên trong. Mũi tên đi từ một phụ thuộc tới mod cần nó. Vị trí thẻ và các khối chỉ để đọc: chúng không quyết định thứ tự tải.
Chọn một khối hoặc một mod để xem nó trong **Thanh thuộc tính**. Ở đó bạn có thể đổi phiên bản mà game yêu cầu — **Bất kỳ**, **Tối thiểu**, **Chính xác**, **Nhánh** (`^1` chấp nhận mọi 1.x) và các loại khác. Đây là yêu cầu của game, không phải phiên bản bạn đã cài. **Kiểm tra** liệt kê vấn đề; **Kế hoạch** cho thấy việc áp dụng bộ sưu tập sẽ cài hay bật gì.
![Thanh thuộc tính của một mod có yêu cầu "Exactly 999.0.0" không khớp bản đã cài 1.0.0: yêu cầu và phiên bản đã cài hiện màu đỏ.](assets/version-mismatch.jpg)
Các nút khác của sơ đồ: **+ Cơ chế** và **Thêm mod** thay đổi game, **Nhân bản** tạo bản sao của riêng bạn (và đề nghị sao chép bản lưu), **JSON** cho thấy nội dung gốc, **Lưu** và **Đặt lại** giữ hoặc bỏ thay đổi, **Áp dụng bộ sưu tập** cài và bật những gì kế hoạch liệt kê. Game tích hợp từng bị bạn thay đổi có thể khôi phục bằng **Khôi phục bản tích hợp**; bản lưu của bạn vẫn còn.
Thay đổi với một game được áp dụng vào lần chạy tiếp theo. Bản lưu của game vẫn dùng được khi mod thay đổi: dữ liệu của mod rời khỏi game được giữ riêng và quay lại cùng với mod.
## Khi có trục trặc
1. **Một mod không xuất hiện trong game.** Kiểm tra nó đã bật chưa và bộ sưu tập của game có chứa nó không (mở **Sửa sơ đồ**).
2. **"Cài đặt thất bại".** Kiểm tra kết nối internet và thử lại **Làm mới danh mục**.
3. **Thẻ game báo Thiếu hoặc Vấn đề.** Mở sơ đồ của nó và thẻ **Kiểm tra**: nơi liệt kê mod còn thiếu, khác phiên bản và xung đột.
4. **Xung đột danh mục.** Cùng một mod và phiên bản được hai danh mục phát hành với nội dung khác nhau; hãy tin danh mục mà bạn tin tưởng.
5. **Không có mục tài liệu.** Mod chưa có cẩm nang trong `docs/`; thay vào đó là mô tả trong manifest.
+81
View File
@@ -0,0 +1,81 @@
# Mod Manager — 模组、游戏及其文档
Mod Manager 是主菜单中的**模组与合集**屏幕。在这里可以查看装了哪些模组、了解每个模组的用途、从在线目录安装或更新模组、停用模组,也可以翻看现成游戏的内部构成,弄清它们由哪些模组搭建。
它属于随菜单一起始终加载的外壳,因此在启动任何游戏之前就能使用。
## 打开方式
在主菜单选择**模组与合集**。该屏幕有两个标签页:
- **模组** — 游戏认识的所有模组:已安装的和目录提供的。
- **合集** — 现成的游戏,以及每款游戏由哪些模组搭成的结构。
**← 菜单**返回主菜单。右上角的 **?** 按钮,或按 **F1**,开启上下文提示:把鼠标悬停在控件上看它的作用,点击可固定提示,按 **Esc** 退出。
## 浏览模组
![模组标签页:左侧是筛选器,中间是带封面图的模组卡片网格,搜索框旁开着提示,还有排序和 Refresh catalog 按钮。](assets/mods-list.jpg)
每张卡片显示模组的封面、标题、作者、来源目录、有多少游戏在用它(**在 4 个合集中**)、状态与版本,以及一个开关。
- **搜索模组...** 按标题、作者或标识符过滤。
- **按名称 / 按作者 / 按状态** 调整列表排序。
- 左侧的**筛选**按**状态**(未安装、已安装、有可用更新、有旧版本可用、本地已修改、已停用)和**分类**(例如剧情、玩法、视觉、音频、库、引擎、工具)收窄列表。**重置**清除筛选和搜索。
- **刷新目录**下载当前已发布模组的列表。
*本地已修改*表示已安装的文件与目录上同一版本的副本不一致——例如你正在自己改动的模组。
## 查看模组介绍
点击卡片打开它的页面:封面、版本选择器、各种操作、来自 manifest 的简短描述,以及模组提供时的**文档**——一份像这样的完整指南。
![一个模组页面:Audio Engine 的封面和版本、Reinstall 和 Disable 按钮、“in 4 collections”、描述,以及带语言选择器的 Documentation 区块。](assets/mod-page.jpg)
文档是**已安装**版本的指南。如果在版本选择器里选了别的版本,提示会提醒你:下面的指南仍属于已安装的那一版。
**文档语言。** 文档标题右侧的选择器默认为**自动 · 界面语言**:指南跟随游戏界面语言。若模组没有该语言的指南,管理器会先显示模组的默认指南,再退回英文,最后退回第一份可用的指南;指南上方一行会标明实际显示的语言(例如 `EN`)。你也可以自己在选择器里挑语言。没有任何指南的模组会继续显示 manifest 描述。
给模组作者:把指南放进 `docs/en.md`(译文放 `docs/<语言>.md`,或 `docs/default.md`),图片放在旁边,例如 `docs/assets/screen.png`,以 `assets/screen.png` 引用。查看器支持标题、段落、列表、引用、代码块、链接和 PNG/JPEG/WebP 图片;不会运行 HTML、表格或动画。
## 安装、更新、停用、删除
模组页面上的按钮和卡片上的开关:
- **安装 / 更新 / 降级 / 重新安装**把所选版本从目录下载到你的用户目录(`user://mods/`)。
- **停用 / 启用**(或卡片上的开关)决定模组是否被加载。停用的模组保留文件和存档中的数据。
- **删除**在确认后移除一个下载来的模组。
任何一项操作之后都会出现横幅:**重启游戏后更改才会生效**。Mod Manager 自己无法在自己的屏幕上被删除或停用。
**已安装不等于在用。** 游戏只加载它合集里列出的模组,外加菜单外壳。安装只是让模组可用;只有当游戏的合集包含它时,它才会进入游戏。
## 游戏与合集
**合集**标签页首先展示现成的游戏。游戏就是一个*合集*:一组有名字的模组,加上起始地点。
![游戏画廊:Cthangover、My Novel 和 RPG Plus 的卡片,均显示“Ready to play”,模组计数 32/32、11/11、36/36,以及 Edit scheme 按钮。](assets/games-gallery.jpg)
卡片会告诉你游戏需要的东西是否都装齐了(**可以开始游戏**,或**缺少 N** / **安装(N)** / **启用(N)**)。一个按钮即可安装或启用缺失内容。**编辑结构**打开游戏的内部构造;**+ 打造你自己的游戏**从新建合集开始。
### 游戏的结构
![My Novel 的结构:Story、Assets 和 2D engine 区块及其模组,从每条依赖指向需要它的模组的箭头,以及显示 My Novel 版本要求的检查器。](assets/collection-scheme.jpg)
结构图展示游戏的*机制*(诸如 Story、Assets、2D engine 的区块)和其中的模组。箭头从依赖指向需要它的模组。卡片位置和区块只是用于阅读:它们不决定加载顺序。
选中一个区块或模组,它就会出现在**检查器**里。在那里可以修改游戏要求的版本:**任意版本**、**不低于**、**精确版本**、**版本分支**(`^1` 接受任何 1.x)等。这是游戏的要求,不是你已安装的版本。**检查**列出问题;**计划**显示应用该合集会安装或启用什么。
![一个模组的检查器,其要求“Exactly 999.0.0”与已安装的 1.0.0 不符:要求和已安装版本都标红。](assets/version-mismatch.jpg)
结构图的其他按钮:**+ 机制**和**添加模组**修改游戏,**复制**生成你自己的副本(并提议一并复制存档),**JSON**显示原始定义,**保存**和**重置**保留或放弃更改,**应用合集**安装并启用计划中列出的内容。改过的内置游戏可用**恢复内置版本**还原;存档保留。
对游戏的更改在下次启动时生效。游戏的存档在模组变动后依然可用:离开游戏的模组数据被单独保存,随模组回归。
## 出现问题时
1. **某个模组没有进入游戏。** 检查它是否已启用、游戏的合集是否包含它(打开**编辑结构**)。
2. **“安装失败”。** 检查网络连接,再点一次**刷新目录**。
3. **游戏卡片显示缺少或有 问题。** 打开它的结构和**检查**标签页:里面列出缺失的模组、版本差距和冲突。
4. **目录冲突。** 同一模组同一版本被两个目录以不同内容发布;相信你信任的那个目录。
5. **没有文档区。** 该模组还没有 `docs/` 指南;此时显示的是 manifest 描述。
+78
View File
@@ -0,0 +1,78 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: DejaVu fonts
Upstream-Author: Stepan Roh <src@users.sourceforge.net> (original author),
see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list
Source: https://dejavu-fonts.github.io/
Files: *
Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
Bitstream Vera is a trademark of Bitstream, Inc.
DejaVu changes are in public domain.
License: bitstream-vera
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
.
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
.
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org.
Files: debian/*
Copyright: (C) 2005-2006 Peter Cernak <pce@users.sourceforge.net>
(C) 2006-2011 Davide Viti <zinosat@tiscali.it>
(C) 2011-2013 Christian Perrier <bubulle@debian.org>
(C) 2013 Fabian Greffrath <fabian+debian@greffrath.com>
License: GPL-2+
This program is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later
version.
.
This program is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied
warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more
details.
.
You should have received a copy of the GNU General Public
License along with this package; if not, write to the Free
Software Foundation, Inc., 51 Franklin St, Fifth Floor,
Boston, MA 02110-1301 USA
.
On Debian systems, the full text of the GNU General Public
License version 2 can be found in the file
/usr/share/common-licenses/GPL-2'.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
# Collection parameter editor
mod_manager/game_settings_title = Game parameters
mod_manager/game_settings_hint = Configure this collection. After Apply, click Save; changes take effect on the next game launch.
mod_manager/game_settings_automatic = Automatic
mod_manager/game_settings_custom = Custom value
mod_manager/game_settings_unavailable = unavailable
mod_manager/game_settings_missing = This choice is unavailable in this collection. The saved value is preserved.
mod_manager/game_settings_owner_missing = The mod using this parameter is not included in the collection. Its value will be preserved.
mod_manager/game_settings_invalid = Check the format of the entered value.
mod_manager/game_settings_single_choice = Without an explicit value, the engine uses the only available choice. Select one if several are available.
mod_manager/game_settings_browse = Browse…
mod_manager/game_settings_choose = Choose
mod_manager/game_settings_search_hint = Search by name or ID
mod_manager/game_settings_advanced = Additional parameters
mod_manager/game_settings_advanced_hint = Parameters from mods without form declarations. Edit keys and values; deleting a row removes the parameter.
mod_manager/game_settings_add = Add parameter
mod_manager/game_settings_key = Parameter key
mod_manager/game_settings_value = Value
mod_manager/game_settings_remove = Remove
mod_manager/game_settings_remove_hint = Remove this parameter from the collection on Apply
mod_manager/game_settings_duplicate = Keys must be nonempty and unique.
mod_manager/game_settings_metadata_error = Could not read parameter declarations from mods:
mod_manager/hint_game_settings_title = Game parameters
mod_manager/hint_game_settings_body = Configure this collection. After Apply, click Save; changes take effect on the next game launch.
+25
View File
@@ -0,0 +1,25 @@
# Collection parameter editor
mod_manager/game_settings_title = Parámetros del juego
mod_manager/game_settings_hint = Configura esta colección. Después de Aplicar, pulsa Guardar; los cambios se aplicarán al iniciar la partida de nuevo.
mod_manager/game_settings_automatic = Automático
mod_manager/game_settings_custom = Valor personalizado
mod_manager/game_settings_unavailable = no disponible
mod_manager/game_settings_missing = Esta opción no está disponible en esta colección. Se conserva el valor guardado.
mod_manager/game_settings_owner_missing = El mod que utiliza este parámetro no está incluido en la colección. Se conservará el valor.
mod_manager/game_settings_invalid = Comprueba el formato del valor introducido.
mod_manager/game_settings_single_choice = Sin un valor explícito, el motor utiliza la única opción disponible. Si hay varias, elige una.
mod_manager/game_settings_browse = Examinar…
mod_manager/game_settings_choose = Elegir
mod_manager/game_settings_search_hint = Buscar por nombre o identificador
mod_manager/game_settings_advanced = Parámetros adicionales
mod_manager/game_settings_advanced_hint = Parámetros de mods sin descripción de formulario. Edita claves y valores; eliminar una fila quita el parámetro.
mod_manager/game_settings_add = Añadir parámetro
mod_manager/game_settings_key = Clave del parámetro
mod_manager/game_settings_value = Valor
mod_manager/game_settings_remove = Eliminar
mod_manager/game_settings_remove_hint = Quitar este parámetro de la colección al aplicar
mod_manager/game_settings_duplicate = Las claves no pueden estar vacías ni repetirse.
mod_manager/game_settings_metadata_error = No se pudieron leer las descripciones de parámetros de los mods:
mod_manager/hint_game_settings_title = Parámetros del juego
mod_manager/hint_game_settings_body = Configura esta colección. Después de Aplicar, pulsa Guardar; los cambios se aplicarán al iniciar la partida de nuevo.
+25
View File
@@ -0,0 +1,25 @@
# Collection parameter editor
mod_manager/game_settings_title = ゲームのパラメーター
mod_manager/game_settings_hint = このコレクションを設定します。適用後に保存してください。変更は次回のゲーム起動時に反映されます。
mod_manager/game_settings_automatic = 自動
mod_manager/game_settings_custom = カスタム値
mod_manager/game_settings_unavailable = 利用不可
mod_manager/game_settings_missing = この構成では利用できません。保存済みの値は保持されます。
mod_manager/game_settings_owner_missing = このパラメーターを使うMODは構成に含まれていません。値は保持されます。
mod_manager/game_settings_invalid = 入力値の形式を確認してください。
mod_manager/game_settings_single_choice = 未指定の場合は唯一の候補を使用します。候補が複数ある場合は選択してください。
mod_manager/game_settings_browse = 参照…
mod_manager/game_settings_choose = 選択
mod_manager/game_settings_search_hint = 名前またはIDで検索
mod_manager/game_settings_advanced = 追加パラメーター
mod_manager/game_settings_advanced_hint = フォーム定義のないMODのパラメーターです。キーと値を編集できます。行の削除でパラメーターを除去します。
mod_manager/game_settings_add = パラメーターを追加
mod_manager/game_settings_key = パラメーターキー
mod_manager/game_settings_value = 値
mod_manager/game_settings_remove = 削除
mod_manager/game_settings_remove_hint = 適用時にこのパラメーターをコレクションから除去
mod_manager/game_settings_duplicate = キーは空にできず、重複もできません。
mod_manager/game_settings_metadata_error = 次のMODのパラメーター定義を読めませんでした:
mod_manager/hint_game_settings_title = ゲームのパラメーター
mod_manager/hint_game_settings_body = このコレクションを設定します。適用後に保存してください。変更は次回のゲーム起動時に反映されます。
+25
View File
@@ -0,0 +1,25 @@
# Collection parameter editor
mod_manager/game_settings_title = 게임 매개변수
mod_manager/game_settings_hint = 이 컬렉션을 설정합니다. 적용 후 저장하세요. 변경 사항은 다음 게임 시작 시 반영됩니다.
mod_manager/game_settings_automatic = 자동
mod_manager/game_settings_custom = 사용자 지정 값
mod_manager/game_settings_unavailable = 사용 불가
mod_manager/game_settings_missing = 현재 컬렉션 구성에서 사용할 수 없습니다. 저장된 값은 유지됩니다.
mod_manager/game_settings_owner_missing = 이 매개변수를 사용하는 모드가 컬렉션에 없습니다. 값은 유지됩니다.
mod_manager/game_settings_invalid = 입력한 값의 형식을 확인하세요.
mod_manager/game_settings_single_choice = 값을 지정하지 않으면 유일한 선택지를 사용합니다. 여러 개면 하나를 선택하세요.
mod_manager/game_settings_browse = 찾아보기…
mod_manager/game_settings_choose = 선택
mod_manager/game_settings_search_hint = 이름 또는 ID로 검색
mod_manager/game_settings_advanced = 추가 매개변수
mod_manager/game_settings_advanced_hint = 양식 정의가 없는 모드의 매개변수입니다. 키와 값을 편집하며 행을 삭제하면 매개변수가 제거됩니다.
mod_manager/game_settings_add = 매개변수 추가
mod_manager/game_settings_key = 매개변수 키
mod_manager/game_settings_value = 값
mod_manager/game_settings_remove = 삭제
mod_manager/game_settings_remove_hint = 적용 시 컬렉션에서 이 매개변수 제거
mod_manager/game_settings_duplicate = 키는 비어 있거나 중복될 수 없습니다.
mod_manager/game_settings_metadata_error = 다음 모드의 매개변수 정의를 읽지 못했습니다:
mod_manager/hint_game_settings_title = 게임 매개변수
mod_manager/hint_game_settings_body = 이 컬렉션을 설정합니다. 적용 후 저장하세요. 변경 사항은 다음 게임 시작 시 반영됩니다.
+25
View File
@@ -0,0 +1,25 @@
# Collection parameter editor
mod_manager/game_settings_title = Параметры игры
mod_manager/game_settings_hint = Настройте выбранную коллекцию. После применения нажмите «Сохранить»; изменения вступят в силу при следующем запуске игры.
mod_manager/game_settings_automatic = Автоматически
mod_manager/game_settings_custom = Своё значение
mod_manager/game_settings_unavailable = недоступно
mod_manager/game_settings_missing = Этот вариант недоступен в текущем составе коллекции. Сохранённое значение оставлено без изменений.
mod_manager/game_settings_owner_missing = Мод, использующий этот параметр, не включён в коллекцию. Значение будет сохранено.
mod_manager/game_settings_invalid = Проверьте формат введённого значения.
mod_manager/game_settings_single_choice = Без явного значения движок выберет единственный доступный вариант. При нескольких вариантах укажите нужный.
mod_manager/game_settings_browse = Выбрать…
mod_manager/game_settings_choose = Выбрать
mod_manager/game_settings_search_hint = Поиск по названию или идентификатору
mod_manager/game_settings_advanced = Дополнительные параметры
mod_manager/game_settings_advanced_hint = Параметры сторонних модов без описания формы. Редактируйте ключи и значения; удаление строки убирает параметр.
mod_manager/game_settings_add = Добавить параметр
mod_manager/game_settings_key = Ключ параметра
mod_manager/game_settings_value = Значение
mod_manager/game_settings_remove = Удалить
mod_manager/game_settings_remove_hint = Убрать этот параметр из коллекции после применения
mod_manager/game_settings_duplicate = Ключ не должен быть пустым или повторять другой параметр.
mod_manager/game_settings_metadata_error = Не удалось прочитать описания параметров модов:
mod_manager/hint_game_settings_title = Параметры игры
mod_manager/hint_game_settings_body = Настройте выбранную коллекцию. После применения нажмите «Сохранить»; изменения вступят в силу при следующем запуске игры.
+25
View File
@@ -0,0 +1,25 @@
# Collection parameter editor
mod_manager/game_settings_title = Tham số trò chơi
mod_manager/game_settings_hint = Cấu hình bộ sưu tập này. Sau khi áp dụng, hãy lưu; thay đổi có hiệu lực khi khởi chạy trò chơi lần sau.
mod_manager/game_settings_automatic = Tự động
mod_manager/game_settings_custom = Giá trị tùy chỉnh
mod_manager/game_settings_unavailable = không khả dụng
mod_manager/game_settings_missing = Lựa chọn này không có trong cấu hình hiện tại. Giá trị đã lưu được giữ nguyên.
mod_manager/game_settings_owner_missing = Mod sử dụng tham số này không có trong bộ sưu tập. Giá trị sẽ được giữ nguyên.
mod_manager/game_settings_invalid = Kiểm tra định dạng giá trị đã nhập.
mod_manager/game_settings_single_choice = Nếu không đặt giá trị, hệ thống dùng lựa chọn duy nhất. Hãy chọn nếu có nhiều lựa chọn.
mod_manager/game_settings_browse = Duyệt…
mod_manager/game_settings_choose = Chọn
mod_manager/game_settings_search_hint = Tìm theo tên hoặc ID
mod_manager/game_settings_advanced = Tham số bổ sung
mod_manager/game_settings_advanced_hint = Tham số của mod chưa có mô tả biểu mẫu. Sửa khóa và giá trị; xóa dòng để bỏ tham số.
mod_manager/game_settings_add = Thêm tham số
mod_manager/game_settings_key = Khóa tham số
mod_manager/game_settings_value = Giá trị
mod_manager/game_settings_remove = Xóa
mod_manager/game_settings_remove_hint = Bỏ tham số khỏi bộ sưu tập khi áp dụng
mod_manager/game_settings_duplicate = Khóa không được trống hoặc trùng lặp.
mod_manager/game_settings_metadata_error = Không đọc được mô tả tham số của các mod:
mod_manager/hint_game_settings_title = Tham số trò chơi
mod_manager/hint_game_settings_body = Cấu hình bộ sưu tập này. Sau khi áp dụng, hãy lưu; thay đổi có hiệu lực khi khởi chạy trò chơi lần sau.
+25
View File
@@ -0,0 +1,25 @@
# Collection parameter editor
mod_manager/game_settings_title = 游戏参数
mod_manager/game_settings_hint = 配置此合集。应用后请点击保存;更改将在下次启动游戏时生效。
mod_manager/game_settings_automatic = 自动
mod_manager/game_settings_custom = 自定义值
mod_manager/game_settings_unavailable = 不可用
mod_manager/game_settings_missing = 此选项在当前合集配置中不可用。已保留保存的值。
mod_manager/game_settings_owner_missing = 使用此参数的模组未包含在合集中。该值将被保留。
mod_manager/game_settings_invalid = 请检查输入值的格式。
mod_manager/game_settings_single_choice = 未指定值时引擎将使用唯一可用选项。有多个选项时必须选择。
mod_manager/game_settings_browse = 浏览…
mod_manager/game_settings_choose = 选择
mod_manager/game_settings_search_hint = 按名称或标识搜索
mod_manager/game_settings_advanced = 其他参数
mod_manager/game_settings_advanced_hint = 没有表单描述的模组参数。可编辑键和值;删除行会移除该参数。
mod_manager/game_settings_add = 添加参数
mod_manager/game_settings_key = 参数键
mod_manager/game_settings_value = 值
mod_manager/game_settings_remove = 删除
mod_manager/game_settings_remove_hint = 应用后从合集中移除此参数
mod_manager/game_settings_duplicate = 键不能为空或重复。
mod_manager/game_settings_metadata_error = 无法读取以下模组的参数描述:
mod_manager/hint_game_settings_title = 游戏参数
mod_manager/hint_game_settings_body = 配置此合集。应用后请点击保存;更改将在下次启动游戏时生效。
+101 -1
View File
@@ -1,7 +1,7 @@
# mod_manager — mod manager localization
mod_mod_manager_name = Mod Manager
mod_mod_manager_desc = Nexus-style mod manager: catalog browsing, category filters, install, update and remove.
mod_mod_manager_desc = The Mods & Collections screen: what is installed, what each mod does, installing and updating from the online catalog, and which mods make up each ready-made game.
mod_manager/menu_title = Mods & Collections
mod_manager/title = Mod Manager
@@ -219,3 +219,103 @@ 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_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/help_open = Contextual hints (F1)
mod_manager/help_active = Hover over a control · Left click — pin the hint · Esc — exit
mod_manager/collection_order_legend = Dependency → the mod that needs it. Card positions and group boundaries do not define load order.
mod_manager/enable_tooltip = Load this mod in games that use it.
mod_manager/disable_tooltip = Do not load this mod. Its files and saves are kept.
mod_manager/hint_mods_tab_title = Mods
mod_manager/hint_mods_tab_body = The catalog and installed mods. Manage mod files, versions and whether games load them here.
mod_manager/hint_collections_tab_title = Collections
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/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/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_reset_tooltip = Discard all unsaved scheme changes.
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_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.
# Bundled mod documentation
mod_manager/documentation = Documentation
mod_manager/docs_auto = Auto · interface language
mod_manager/docs_default = Default
mod_manager/docs_source = Installed version documentation {0} · {1}
mod_manager/docs_installed_notice = A different download version is selected. The guide below belongs to the installed mod.
mod_manager/docs_image_unavailable = Image unavailable
mod_manager/hint_docs_language_title = Documentation language
mod_manager/hint_docs_language_body = Auto follows the interface language. Missing translations fall back to default.md, English, then the first available document. You can select a language manually.
+321
View File
@@ -0,0 +1,321 @@
# mod_manager — mod manager localization
mod_mod_manager_name = Gestor de mods
mod_mod_manager_desc = La pantalla Mods y colecciones: qué hay instalado, qué hace cada mod, instalación y actualización desde el catálogo en línea, y de qué mods está hecho cada juego ya preparado.
mod_manager/menu_title = Mods y colecciones
mod_manager/title = Gestor de mods
mod_manager/back = Cerrar
mod_manager/search_placeholder = Buscar mods...
mod_manager/sort_name = Por nombre
mod_manager/sort_author = Por autor
mod_manager/sort_status = Por estado
mod_manager/refresh = Actualizar catálogo
mod_manager/loading = Cargando catálogo...
mod_manager/results = Mods: {0}
mod_manager/filters = Filtros
mod_manager/reset_filters = Restablecer
mod_manager/filter_status = Estado
mod_manager/filter_categories = Categorías
mod_manager/group_by_category = Agrupar por categoría
mod_manager/status_installed = Instalado
mod_manager/status_not_installed = No instalado
mod_manager/status_update = Hay actualización
mod_manager/status_downgrade = Hay una versión anterior
mod_manager/status_modified = Modificado localmente
mod_manager/status_disabled = Desactivado
mod_manager/status_conflict = Conflicto de catálogo
mod_manager/conflict_title = Aviso
mod_manager/conflict_warning = El mismo mod con la misma versión y título lo publican catálogos distintos con contenido diferente. Una de las variantes podría ser falsa: instala solo desde una fuente de confianza.
mod_manager/source_catalog = Catálogo: {0}
mod_manager/source_local = Mod local
mod_manager/category_story = Historia
mod_manager/category_characters = Personajes
mod_manager/category_gameplay = Jugabilidad
mod_manager/category_visual = Visual
mod_manager/category_audio = Audio
mod_manager/category_ui = Interfaz
mod_manager/category_locale = Idioma
mod_manager/category_library = Biblioteca
mod_manager/category_engine = Motor
mod_manager/category_overhaul = Conversión total
mod_manager/category_tools = Herramientas
mod_manager/category_editor = Editor
mod_manager/category_debug = Depuración
mod_manager/category_other = Otros
mod_manager/action_install = Instalar
mod_manager/action_update = Actualizar
mod_manager/action_downgrade = Volver a versión anterior
mod_manager/action_reinstall = Reinstalar
mod_manager/action_delete = Eliminar
mod_manager/action_enable = Activar
mod_manager/action_disable = Desactivar
mod_manager/restart_required = Los cambios surtirán efecto al reiniciar el juego
mod_manager/progress_installing = Instalando «{0}»...
mod_manager/install_failed = La instalación falló. Comprueba la conexión e inténtalo de nuevo.
mod_manager/delete_title = Eliminar mod
mod_manager/delete_text = ¿Eliminar el mod «{0}»? No se puede deshacer y surte efecto tras reiniciar.
mod_manager/delete_confirm = Eliminar
mod_manager/delete_failed = No se pudo eliminar el mod. Mira el registro.
mod_manager/cancel = Cancelar
mod_manager/installed_version = Instalado: {0}
mod_manager/not_installed_hint = No instalado
mod_manager/latest = última
mod_manager/installed_marker = instalado
mod_manager/no_results = No se encontró nada
mod_manager/description = Descripción
mod_manager/depends = Depende de
mod_manager/used_by = Lo usan
mod_manager/conflicts = Conflictos
mod_manager/changelog = Cambios
mod_manager/version = Versión:
mod_manager/self_hint = Mod del sistema: no se puede eliminar ni desactivar
mod_manager/collections_menu_title = Colecciones
mod_manager/collections_title = Colecciones
mod_manager/collections_empty = No se encontraron colecciones
mod_manager/collection_result = Mods: {0}/{1}
mod_manager/collection_mechanics = Mecánicas
mod_manager/collection_node = Mod
mod_manager/collection_plan = Plan de instalación
mod_manager/collection_apply = Aplicar colección
mod_manager/collection_plan_ready = Todo está listo
mod_manager/collection_install_count = Por instalar: {0}
mod_manager/collection_enable_count = Por activar: {0}
mod_manager/collection_missing = No está en el catálogo: {0}
mod_manager/collection_mismatch = Versión no coincide: {0}
mod_manager/collection_optional = opcional
mod_manager/collection_externals = Dependencias
mod_manager/collection_external = dependencia
mod_manager/collection_node_hint = Selecciona un mod en el gráfico
mod_manager/collection_requires = Requiere: {0}
mod_manager/collection_origin_builtin = integrada
mod_manager/collection_origin_user = local
mod_manager/collection_origin_catalog = catálogo
mod_manager/collection_progress = Aplicando «{0}»...
mod_manager/collection_apply_failed = No se pudo aplicar la colección. Mira el registro.
mod_manager/collection_state_installed = Instalado
mod_manager/collection_state_disabled = Desactivado
mod_manager/collection_state_catalog = Disponible en el catálogo
mod_manager/collection_state_missing = No encontrado
mod_manager/collection_state_mismatch = Versión no coincide
# Scheme editor (collections pyramid)
mod_manager/collection_add_mechanic = + Mecánica
mod_manager/collection_new_game = + Juego
mod_manager/collection_new_game_name = Nuevo juego
mod_manager/collection_new_game_desc = Monta un juego a partir de mecánicas: añade bloques y mods.
mod_manager/collection_new_mechanic_name = Nueva mecánica
mod_manager/collection_group_ready = listo
mod_manager/collection_group_empty = vacío
mod_manager/collection_group_off = apagado
mod_manager/collection_links = Enlaces
mod_manager/collection_problems = Problemas
mod_manager/collection_tab_inspector = Inspector
mod_manager/collection_tab_problems = Comprobar
mod_manager/collection_tab_plan = Plan
mod_manager/collection_plan_install = por instalar
mod_manager/collection_plan_enable = por activar
mod_manager/collection_save = Guardar
mod_manager/collection_reset = Restablecer
mod_manager/collection_json = JSON
mod_manager/collection_save_ok = Guardado en user://collections.json
mod_manager/collection_no_changes = No hay nada que guardar
mod_manager/collection_reset_confirm = ¿Descartar los cambios sin guardar?
mod_manager/collection_rename_title = Nombre y descripción
mod_manager/collection_field_name = Nombre
mod_manager/collection_field_note = Descripción
mod_manager/collection_add_mod = Añadir mod
mod_manager/collection_empty_mechanic = Mecánica vacía
mod_manager/collection_empty_mechanic_desc = Crea un contorno desde cero: añade a mano el nombre, la descripción y los mods.
mod_manager/collection_pick_mechanic = Añadir mecánica
mod_manager/collection_pick_replace = Sustituir mecánica
mod_manager/collection_choose = Añadir
mod_manager/collection_replace = Sustituir
mod_manager/collection_remove = Quitar
mod_manager/collection_rename = Renombrar
mod_manager/collection_create = Crear
mod_manager/collection_where = Añadir a
mod_manager/collection_new_extra = + nueva mecánica «Extra»
mod_manager/collection_extra_name = Extra
mod_manager/collection_extra_note = Mods añadidos a mano.
mod_manager/collection_search_mechanic = Buscar mecánicas...
mod_manager/collection_search_mod = Buscar mods...
mod_manager/collection_problems_none = Sin problemas: el montaje es coherente.
mod_manager/collection_problem_missing = No se encontró el bloque «{0}»
mod_manager/collection_problem_cycle = Ciclo: «{0}» -> «{1}»
mod_manager/collection_problem_role = La ranura «{0}» se reclama dos veces
mod_manager/collection_problem_require = «{0}»: no se cumple el requisito «{1}»
mod_manager/collection_show = Mostrar
mod_manager/collection_hint = Selecciona un contorno o un mod. Arrastra los marcos por la cabecera, cambia su tamaño por la esquina y mueve los mods libremente. Arrastra el lienzo vacío con el botón izquierdo para desplazarte.
mod_manager/collection_mechanic = mecánica
mod_manager/collection_group = grupo
mod_manager/collection_mods = mods
mod_manager/collection_in_group = En el grupo
mod_manager/collection_remove_mod = Quitar de la mecánica
mod_manager/collection_turn_on = Activar
mod_manager/collection_turn_off = Desactivar
mod_manager/collection_unsaved = cambios sin guardar
# ─── Mods & collections hub ───
mod_manager/hub_tab_mods = Mods
mod_manager/hub_tab_collections = Colecciones
mod_manager/hub_back_to_menu = ← Menú
mod_manager/hub_back_to_gallery = ← Juegos
mod_manager/hub_back_to_mods = ← Mods
mod_manager/hub_in_one_collection = en {0} colección
mod_manager/hub_in_many_collections = en {0} colecciones
# Gallery of ready games
mod_manager/gallery_crumb = Juegos listos
mod_manager/gallery_title = Juegos
mod_manager/gallery_hint = conjuntos de mods listos: un botón lo instala todo
mod_manager/gallery_search_placeholder = Buscar colecciones...
mod_manager/gallery_new_game = + Crea tu propio juego
mod_manager/gallery_new_game_blank = + Crea tu propio juego desde cero
mod_manager/gallery_empty_hint = Añade a tu mod una carpeta collections/ con archivos JSON: las colecciones aparecerán aquí.
mod_manager/gallery_edit_scheme = Editar esquema
mod_manager/gallery_ready = Listo para jugar
mod_manager/gallery_ready_done = Listo ✓
mod_manager/gallery_missing_n = Faltan {0}
mod_manager/gallery_problems_n = Problemas: {0}
mod_manager/gallery_enable_badge_n = Activar {0}
mod_manager/gallery_install_btn_n = Instalar ({0})
mod_manager/gallery_enable_btn_n = Activar ({0})
mod_manager/gallery_draft = Borrador
mod_manager/gallery_continue = Seguir montando
# Bottom build bar (scheme editor)
mod_manager/build_caption = Montaje
mod_manager/build_stat_install = Instalar
mod_manager/build_stat_enable = Activar
mod_manager/build_stat_problems = Problemas
mod_manager/build_hint_problems = Corrige los problemas para aplicar la colección
mod_manager/build_open_manager = Abrir el gestor de mods
mod_manager/collection_crumb_scheme = Esquema
mod_manager/collection_find_in_catalog = Buscar en el catálogo
mod_manager/collection_duplicate = Duplicar
mod_manager/collection_revert = Restaurar la integrada
mod_manager/collection_saves = Partidas: {0}
mod_manager/collection_next_launch = Los cambios se aplican la próxima vez que empiece el juego.
mod_manager/collection_json_apply = Aplicar
mod_manager/collection_json_invalid = JSON no válido: {0}
mod_manager/collection_json_id_changed = El id de la colección no puede cambiar: usa Duplicar para un juego nuevo.
mod_manager/collection_copy_name = {0} (copia)
mod_manager/collection_copy_saves = El juego original tiene partidas guardadas ({0}). ¿Copiarlas al nuevo juego?
mod_manager/collection_copy_saves_ok = Copiar
mod_manager/collection_revert_confirm = ¿Descartar tus cambios y restaurar la versión integrada del juego? Las partidas se conservan.
mod_manager/collection_revert_done = Versión integrada restaurada.
mod_manager/collection_delete = Eliminar colección
mod_manager/collection_delete_confirm = Se eliminará la colección «{0}». Sus mods siguen instalados en el sistema y sus partidas ({1}) siguen en el disco.
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_forked = Bloque «{0}» copiado para este juego: los demás juegos no cambian.
mod_manager/help_open = Sugerencias contextuales (F1)
mod_manager/help_active = Pasa el cursor por un control · Clic izquierdo: fijar · Esc: salir
mod_manager/collection_order_legend = Dependencia → mod que la necesita. La posición y los grupos no definen el orden de carga.
mod_manager/enable_tooltip = Cargar este mod en los juegos que lo utilizan.
mod_manager/disable_tooltip = No cargar este mod. Sus archivos y partidas se conservan.
mod_manager/hint_mods_tab_title = Mods
mod_manager/hint_mods_tab_body = El catálogo y los mods instalados. Gestiona sus archivos, versiones y carga en los juegos.
mod_manager/hint_collections_tab_title = Colecciones
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/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/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_reset_tooltip = Descartar todos los cambios sin guardar del esquema.
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_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.
# Bundled mod documentation
mod_manager/documentation = Documentación
mod_manager/docs_auto = Auto · idioma de la interfaz
mod_manager/docs_default = Predeterminado
mod_manager/docs_source = Documentación de la versión instalada {0} · {1}
mod_manager/docs_installed_notice = Se ha seleccionado otra versión para descargar. Esta guía corresponde al mod instalado.
mod_manager/docs_image_unavailable = Imagen no disponible
mod_manager/hint_docs_language_title = Idioma de la documentación
mod_manager/hint_docs_language_body = Auto usa el idioma de la interfaz. Si falta una traducción: default.md, inglés y luego el primer documento disponible. También puedes elegir el idioma.
+321
View File
@@ -0,0 +1,321 @@
# mod_manager — mod manager localization
mod_mod_manager_name = MOD マネージャー
mod_mod_manager_desc = MOD とコレクションの画面:何がインストールされているか、各 MOD の働き、オンラインカタログからのインストールと更新、そして各ゲームがどの MOD で構成されているか。
mod_manager/menu_title = MOD とコレクション
mod_manager/title = MOD マネージャー
mod_manager/back = 閉じる
mod_manager/search_placeholder = MOD を検索...
mod_manager/sort_name = 名前順
mod_manager/sort_author = 作者順
mod_manager/sort_status = 状態順
mod_manager/refresh = カタログを更新
mod_manager/loading = カタログを読み込み中...
mod_manager/results = MOD:{0}
mod_manager/filters = フィルター
mod_manager/reset_filters = リセット
mod_manager/filter_status = 状態
mod_manager/filter_categories = カテゴリー
mod_manager/group_by_category = カテゴリーでまとめる
mod_manager/status_installed = インストール済み
mod_manager/status_not_installed = 未インストール
mod_manager/status_update = 更新あり
mod_manager/status_downgrade = 古いバージョンあり
mod_manager/status_modified = ローカルで変更
mod_manager/status_disabled = 無効
mod_manager/status_conflict = カタログの競合
mod_manager/conflict_title = 警告
mod_manager/conflict_warning = 同じバージョンとタイトルの同じ MOD が、異なるカタログから異なる内容で公開されています。どちらかが偽物の可能性があります:信頼できる提供元からのみインストールしてください。
mod_manager/source_catalog = カタログ:{0}
mod_manager/source_local = ローカルの MOD
mod_manager/category_story = ストーリー
mod_manager/category_characters = キャラクター
mod_manager/category_gameplay = ゲームプレイ
mod_manager/category_visual = ビジュアル
mod_manager/category_audio = オーディオ
mod_manager/category_ui = UI
mod_manager/category_locale = 言語
mod_manager/category_library = ライブラリ
mod_manager/category_engine = エンジン
mod_manager/category_overhaul = 大型改造
mod_manager/category_tools = ツール
mod_manager/category_editor = エディター
mod_manager/category_debug = デバッグ
mod_manager/category_other = その他
mod_manager/action_install = インストール
mod_manager/action_update = 更新
mod_manager/action_downgrade = ダウングレード
mod_manager/action_reinstall = 再インストール
mod_manager/action_delete = 削除
mod_manager/action_enable = 有効にする
mod_manager/action_disable = 無効にする
mod_manager/restart_required = 変更はゲームの再起動後に反映されます
mod_manager/progress_installing = 「{0}」をインストール中...
mod_manager/install_failed = インストールに失敗しました。接続を確認してもう一度お試しください。
mod_manager/delete_title = MOD を削除
mod_manager/delete_text = MOD「{0}」を削除しますか? 元に戻せず、再起動後に反映されます。
mod_manager/delete_confirm = 削除
mod_manager/delete_failed = MOD を削除できませんでした。ログを見てください。
mod_manager/cancel = キャンセル
mod_manager/installed_version = インストール済み:{0}
mod_manager/not_installed_hint = 未インストール
mod_manager/latest = 最新
mod_manager/installed_marker = インストール済み
mod_manager/no_results = 何も見つかりません
mod_manager/description = 説明
mod_manager/depends = 依存先
mod_manager/used_by = 利用元
mod_manager/conflicts = 競合
mod_manager/changelog = 変更履歴
mod_manager/version = バージョン:
mod_manager/self_hint = システム MOD:削除も無効化もできません
mod_manager/collections_menu_title = コレクション
mod_manager/collections_title = コレクション
mod_manager/collections_empty = コレクションが見つかりません
mod_manager/collection_result = MOD:{0}/{1}
mod_manager/collection_mechanics = メカニクス
mod_manager/collection_node = MOD
mod_manager/collection_plan = インストール計画
mod_manager/collection_apply = コレクションを適用
mod_manager/collection_plan_ready = すべて準備完了
mod_manager/collection_install_count = インストール:{0}
mod_manager/collection_enable_count = 有効化:{0}
mod_manager/collection_missing = カタログにない:{0}
mod_manager/collection_mismatch = バージョン不一致:{0}
mod_manager/collection_optional = 任意
mod_manager/collection_externals = 依存関係
mod_manager/collection_external = 依存関係
mod_manager/collection_node_hint = グラフで MOD を選んでください
mod_manager/collection_requires = 必要:{0}
mod_manager/collection_origin_builtin = 組み込み
mod_manager/collection_origin_user = ローカル
mod_manager/collection_origin_catalog = カタログ
mod_manager/collection_progress = 「{0}」を適用中...
mod_manager/collection_apply_failed = コレクションを適用できませんでした。ログを見てください。
mod_manager/collection_state_installed = インストール済み
mod_manager/collection_state_disabled = 無効
mod_manager/collection_state_catalog = カタログにあり
mod_manager/collection_state_missing = 見つからない
mod_manager/collection_state_mismatch = バージョン不一致
# Scheme editor (collections pyramid)
mod_manager/collection_add_mechanic = + メカニクス
mod_manager/collection_new_game = + ゲーム
mod_manager/collection_new_game_name = 新しいゲーム
mod_manager/collection_new_game_desc = メカニクスからゲームを組み立てます:ブロックと MOD を追加してください。
mod_manager/collection_new_mechanic_name = 新しいメカニクス
mod_manager/collection_group_ready = 準備完了
mod_manager/collection_group_empty = 空
mod_manager/collection_group_off = オフ
mod_manager/collection_links = リンク
mod_manager/collection_problems = 問題
mod_manager/collection_tab_inspector = インスペクター
mod_manager/collection_tab_problems = チェック
mod_manager/collection_tab_plan = 計画
mod_manager/collection_plan_install = インストール
mod_manager/collection_plan_enable = 有効化
mod_manager/collection_save = 保存
mod_manager/collection_reset = リセット
mod_manager/collection_json = JSON
mod_manager/collection_save_ok = user://collections.json に保存しました
mod_manager/collection_no_changes = 保存するものはありません
mod_manager/collection_reset_confirm = 保存していない変更を破棄しますか?
mod_manager/collection_rename_title = 名前と説明
mod_manager/collection_field_name = 名前
mod_manager/collection_field_note = 説明
mod_manager/collection_add_mod = MOD を追加
mod_manager/collection_empty_mechanic = 空のメカニクス
mod_manager/collection_empty_mechanic_desc = 一から枠を作ります:名前、説明、MOD を手で追加します。
mod_manager/collection_pick_mechanic = メカニクスを追加
mod_manager/collection_pick_replace = メカニクスを置き換え
mod_manager/collection_choose = 追加
mod_manager/collection_replace = 置き換え
mod_manager/collection_remove = 削除
mod_manager/collection_rename = 名前を変更
mod_manager/collection_create = 作成
mod_manager/collection_where = 追加先
mod_manager/collection_new_extra = + 新しい「追加」メカニクス
mod_manager/collection_extra_name = 追加
mod_manager/collection_extra_note = 手動で追加した MOD。
mod_manager/collection_search_mechanic = メカニクスを検索...
mod_manager/collection_search_mod = MOD を検索...
mod_manager/collection_problems_none = 問題なし——構成は一貫しています。
mod_manager/collection_problem_missing = ブロック「{0}」が見つかりません
mod_manager/collection_problem_cycle = 循環:「{0}」 -> 「{1}」
mod_manager/collection_problem_role = スロット「{0}」が二重に使われています
mod_manager/collection_problem_require = 「{0}」:要件「{1}」を満たしていません
mod_manager/collection_show = 表示
mod_manager/collection_hint = 枠か MOD を選んでください。枠は見出しでドラッグ、角でサイズ変更、MOD は自由に動かせます。空いたキャンバスを左ボタンでドラッグするとスクロールします。
mod_manager/collection_mechanic = メカニクス
mod_manager/collection_group = グループ
mod_manager/collection_mods = MOD
mod_manager/collection_in_group = グループ内
mod_manager/collection_remove_mod = メカニクスから外す
mod_manager/collection_turn_on = オンにする
mod_manager/collection_turn_off = オフにする
mod_manager/collection_unsaved = 保存していない変更
# ─── Mods & collections hub ───
mod_manager/hub_tab_mods = MOD
mod_manager/hub_tab_collections = コレクション
mod_manager/hub_back_to_menu = ← メニュー
mod_manager/hub_back_to_gallery = ← ゲーム
mod_manager/hub_back_to_mods = ← MOD
mod_manager/hub_in_one_collection = {0} 個のコレクションに含まれる
mod_manager/hub_in_many_collections = {0} 個のコレクションに含まれる
# Gallery of ready games
mod_manager/gallery_crumb = 完成済みのゲーム
mod_manager/gallery_title = ゲーム
mod_manager/gallery_hint = 完成済みの MOD セット:ボタンひとつで全部インストール
mod_manager/gallery_search_placeholder = コレクションを検索...
mod_manager/gallery_new_game = + 自分のゲームを作る
mod_manager/gallery_new_game_blank = + 自分のゲームを一から作る
mod_manager/gallery_empty_hint = MOD に JSON ファイル入りの collections/ フォルダーを追加すると、コレクションがここに表示されます。
mod_manager/gallery_edit_scheme = 構成を編集
mod_manager/gallery_ready = プレイできます
mod_manager/gallery_ready_done = 準備完了 ✓
mod_manager/gallery_missing_n = {0} 個不足
mod_manager/gallery_problems_n = 問題:{0}
mod_manager/gallery_enable_badge_n = {0} 個を有効化
mod_manager/gallery_install_btn_n = インストール({0})
mod_manager/gallery_enable_btn_n = 有効化({0})
mod_manager/gallery_draft = 下書き
mod_manager/gallery_continue = 組み立てを続ける
# Bottom build bar (scheme editor)
mod_manager/build_caption = 組み立て
mod_manager/build_stat_install = インストール
mod_manager/build_stat_enable = 有効化
mod_manager/build_stat_problems = 問題
mod_manager/build_hint_problems = コレクションを適用するには問題を解決してください
mod_manager/build_open_manager = MOD マネージャーを開く
mod_manager/collection_crumb_scheme = 構成
mod_manager/collection_find_in_catalog = カタログで探す
mod_manager/collection_duplicate = 複製
mod_manager/collection_revert = 組み込み版に戻す
mod_manager/collection_saves = セーブ:{0}
mod_manager/collection_next_launch = 変更は次にゲームを起動したときに反映されます。
mod_manager/collection_json_apply = 適用
mod_manager/collection_json_invalid = 不正な JSON:{0}
mod_manager/collection_json_id_changed = コレクションの id は変えられません——新しいゲームには「複製」を使ってください。
mod_manager/collection_copy_name = {0}(コピー)
mod_manager/collection_copy_saves = 元のゲームにはセーブがあります({0})。新しいゲームにコピーしますか?
mod_manager/collection_copy_saves_ok = コピー
mod_manager/collection_revert_confirm = 変更を破棄して、ゲームの組み込み版に戻しますか? セーブは残ります。
mod_manager/collection_revert_done = 組み込み版に戻しました。
mod_manager/collection_delete = コレクションを削除
mod_manager/collection_delete_confirm = コレクション「{0}」を削除します。MOD はシステムにインストールされたまま、セーブ({1})もディスクに残ります。
mod_manager/collection_delete_done = コレクションを削除しました。MOD はシステムに残っています。
mod_manager/collection_nested_readonly = このブロックはさらに深く入れ子になっています——変更するには単独で開いてください。
mod_manager/collection_forked = ブロック「{0}」をこのゲーム用にコピーしました——他のゲームは変わりません。
mod_manager/help_open = コンテキストヒント(F1)
mod_manager/help_active = コントロールにポインターを合わせる · 左クリックで固定 · Escで終了
mod_manager/collection_order_legend = 依存先 → それを必要とするMOD。カードの位置やグループの境界は読み込み順を決めません。
mod_manager/enable_tooltip = このMODを使用するゲームで読み込みます。
mod_manager/disable_tooltip = このMODを読み込みません。ファイルとセーブは残ります。
mod_manager/hint_mods_tab_title = MOD
mod_manager/hint_mods_tab_body = カタログとインストール済みMODです。ファイル、バージョン、ゲームでの読み込みを管理します。
mod_manager/hint_collections_tab_title = コレクション
mod_manager/hint_collections_tab_body = 独立したゲームとして実行できる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/filters_reset_tooltip = すべてのフィルターと検索ボックスを既定の状態に戻します。
mod_manager/collection_json_tooltip = 構成を生のJSONとして開きます。表示・編集して元に適用できます。
mod_manager/collection_reset_tooltip = 保存していない構成の変更をすべて破棄します。
mod_manager/collection_game_tooltip = 編集中の構成を別のコレクションに切り替えます。
mod_manager/collection_plan_tooltip = プランを開きます:適用で何をインストールし、何を有効にするか。
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の用途です。他のゲームでは異なる役割になり得ます。
# Bundled mod documentation
mod_manager/documentation = ドキュメント
mod_manager/docs_auto = 自動 · UIの言語
mod_manager/docs_default = 既定
mod_manager/docs_source = インストール済みバージョンの説明 {0} · {1}
mod_manager/docs_installed_notice = 別のダウンロードバージョンが選択されています。以下はインストール済みModのガイドです。
mod_manager/docs_image_unavailable = 画像を表示できません
mod_manager/hint_docs_language_title = ドキュメントの言語
mod_manager/hint_docs_language_body = 自動はUIの言語に従います。翻訳がない場合は default.md、英語、最初の文書の順に表示します。手動でも選択できます。
+330
View File
@@ -0,0 +1,330 @@
# mod_manager — mod manager localization
mod_mod_manager_name = 모드 관리자
mod_mod_manager_desc = 모드와 컬렉션 화면: 무엇이 설치되어 있고 각 모드가 하는 일, 온라인 카탈로그에서의 설치와 업데이트, 그리고 각 게임이 어떤 모드로 이루어져 있는지.
mod_manager/menu_title = 모드와 컬렉션
mod_manager/title = 모드 관리자
mod_manager/back = 닫기
mod_manager/search_placeholder = 모드 검색...
mod_manager/sort_name = 이름순
mod_manager/sort_author = 작성자순
mod_manager/sort_status = 상태순
mod_manager/refresh = 카탈로그 새로 고침
mod_manager/loading = 카탈로그 불러오는 중...
mod_manager/results = 모드: {0}
mod_manager/filters = 필터
mod_manager/reset_filters = 초기화
mod_manager/filter_status = 상태
mod_manager/filter_categories = 분류
mod_manager/group_by_category = 분류별로 묶기
mod_manager/status_installed = 설치됨
mod_manager/status_not_installed = 설치 안 됨
mod_manager/status_update = 업데이트 있음
mod_manager/status_downgrade = 이전 버전 있음
mod_manager/status_modified = 로컬에서 수정됨
mod_manager/status_disabled = 비활성화됨
mod_manager/status_conflict = 카탈로그 충돌
mod_manager/conflict_title = 경고
mod_manager/conflict_warning = 같은 버전과 제목의 같은 모드가 서로 다른 카탈로그에서 다른 내용으로 배포되고 있습니다. 한쪽이 위조일 수 있습니다: 믿을 수 있는 출처에서만 설치하세요.
mod_manager/source_catalog = 카탈로그: {0}
mod_manager/source_local = 로컬 모드
mod_manager/category_story = 스토리
mod_manager/category_characters = 캐릭터
mod_manager/category_gameplay = 게임플레이
mod_manager/category_visual = 비주얼
mod_manager/category_audio = 오디오
mod_manager/category_ui = UI
mod_manager/category_locale = 언어
mod_manager/category_library = 라이브러리
mod_manager/category_engine = 엔진
mod_manager/category_overhaul = 대규모 개편
mod_manager/category_tools = 도구
mod_manager/category_editor = 에디터
mod_manager/category_debug = 디버그
mod_manager/category_other = 기타
mod_manager/action_install = 설치
mod_manager/action_update = 업데이트
mod_manager/action_downgrade = 다운그레이드
mod_manager/action_reinstall = 다시 설치
mod_manager/action_delete = 삭제
mod_manager/action_enable = 켜기
mod_manager/action_disable = 끄기
mod_manager/restart_required = 변경 사항은 게임을 다시 시작한 뒤에 적용됩니다
mod_manager/progress_installing = "{0}" 설치 중...
mod_manager/install_failed = 설치에 실패했습니다. 연결을 확인하고 다시 시도하세요.
mod_manager/delete_title = 모드 삭제
mod_manager/delete_text = 모드 "{0}"을(를) 삭제할까요? 되돌릴 수 없으며 다시 시작한 뒤에 적용됩니다.
mod_manager/delete_confirm = 삭제
mod_manager/delete_failed = 모드를 삭제하지 못했습니다. 로그를 확인하세요.
mod_manager/cancel = 취소
mod_manager/installed_version = 설치됨: {0}
mod_manager/not_installed_hint = 설치 안 됨
mod_manager/latest = 최신
mod_manager/installed_marker = 설치됨
mod_manager/no_results = 아무것도 찾지 못했습니다
mod_manager/description = 설명
mod_manager/depends = 의존 대상
mod_manager/used_by = 사용처
mod_manager/conflicts = 충돌
mod_manager/changelog = 변경 기록
mod_manager/version = 버전:
mod_manager/self_hint = 시스템 모드: 삭제하거나 끌 수 없습니다
mod_manager/collections_menu_title = 컬렉션
mod_manager/collections_title = 컬렉션
mod_manager/collections_empty = 컬렉션을 찾지 못했습니다
mod_manager/collection_result = 모드: {0}/{1}
mod_manager/collection_mechanics = 메카닉
mod_manager/collection_node = 모드
mod_manager/collection_plan = 설치 계획
mod_manager/collection_apply = 컬렉션 적용
mod_manager/collection_plan_ready = 모두 준비되었습니다
mod_manager/collection_install_count = 설치할 것: {0}
mod_manager/collection_enable_count = 켤 것: {0}
mod_manager/collection_missing = 카탈로그에 없음: {0}
mod_manager/collection_mismatch = 버전 불일치: {0}
mod_manager/collection_optional = 선택
mod_manager/collection_externals = 의존성
mod_manager/collection_external = 의존성
mod_manager/collection_node_hint = 그래프에서 모드를 선택하세요
mod_manager/collection_requires = 필요: {0}
mod_manager/collection_origin_builtin = 기본 제공
mod_manager/collection_origin_user = 로컬
mod_manager/collection_origin_catalog = 카탈로그
mod_manager/collection_progress = "{0}" 적용 중...
mod_manager/collection_apply_failed = 컬렉션을 적용하지 못했습니다. 로그를 확인하세요.
mod_manager/collection_state_installed = 설치됨
mod_manager/collection_state_disabled = 비활성화됨
mod_manager/collection_state_catalog = 카탈로그에 있음
mod_manager/collection_state_missing = 찾을 수 없음
mod_manager/collection_state_mismatch = 버전 불일치
# Scheme editor (collections pyramid)
mod_manager/collection_add_mechanic = + 메카닉
mod_manager/collection_new_game = + 게임
mod_manager/collection_new_game_name = 새 게임
mod_manager/collection_new_game_desc = 메카닉으로 게임을 조립하세요: 블록과 모드를 추가합니다.
mod_manager/collection_new_mechanic_name = 새 메카닉
mod_manager/collection_group_ready = 준비됨
mod_manager/collection_group_empty = 비어 있음
mod_manager/collection_group_off = 꺼짐
mod_manager/collection_links = 연결
mod_manager/collection_problems = 문제
mod_manager/collection_tab_inspector = 인스펙터
mod_manager/collection_tab_problems = 검사
mod_manager/collection_tab_plan = 계획
mod_manager/collection_plan_install = 설치할 것
mod_manager/collection_plan_enable = 켤 것
mod_manager/collection_save = 저장
mod_manager/collection_reset = 초기화
mod_manager/collection_json = JSON
mod_manager/collection_save_ok = user://collections.json에 저장했습니다
mod_manager/collection_no_changes = 저장할 것이 없습니다
mod_manager/collection_reset_confirm = 저장하지 않은 변경을 버릴까요?
mod_manager/collection_rename_title = 이름과 설명
mod_manager/collection_field_name = 이름
mod_manager/collection_field_note = 설명
mod_manager/collection_add_mod = 모드 추가
mod_manager/collection_empty_mechanic = 빈 메카닉
mod_manager/collection_empty_mechanic_desc = 처음부터 윤곽을 만듭니다: 이름, 설명, 모드를 직접 추가하세요.
mod_manager/collection_pick_mechanic = 메카닉 추가
mod_manager/collection_pick_replace = 메카닉 교체
mod_manager/collection_choose = 추가
mod_manager/collection_replace = 교체
mod_manager/collection_remove = 제거
mod_manager/collection_rename = 이름 바꾸기
mod_manager/collection_create = 만들기
mod_manager/collection_where = 추가할 곳
mod_manager/collection_new_extra = + 새 "추가" 메카닉
mod_manager/collection_extra_name = 추가
mod_manager/collection_extra_note = 직접 추가한 모드.
mod_manager/collection_search_mechanic = 메카닉 검색...
mod_manager/collection_search_mod = 모드 검색...
mod_manager/collection_problems_none = 문제 없음 — 구성이 일관됩니다.
mod_manager/collection_problem_missing = 블록 "{0}"을(를) 찾을 수 없음
mod_manager/collection_problem_cycle = 순환: "{0}" -> "{1}"
mod_manager/collection_problem_role = 슬롯 "{0}"이(가) 두 번 쓰였습니다
mod_manager/collection_problem_require = "{0}": 요구 사항 "{1}"을(를) 충족하지 않음
mod_manager/collection_show = 보기
mod_manager/collection_hint = 윤곽이나 모드를 선택하세요. 틀은 머리글을 끌어 옮기고, 모서리로 크기를 바꾸며, 모드는 자유롭게 옮길 수 있습니다. 빈 캔버스를 왼쪽 버튼으로 끌면 화면이 이동합니다.
mod_manager/collection_mechanic = 메카닉
mod_manager/collection_group = 그룹
mod_manager/collection_mods = 모드
mod_manager/collection_in_group = 그룹 안
mod_manager/collection_remove_mod = 메카닉에서 제거
mod_manager/collection_turn_on = 켜기
mod_manager/collection_turn_off = 끄기
mod_manager/collection_unsaved = 저장하지 않은 변경
# ─── Mods & collections hub ───
mod_manager/hub_tab_mods = 모드
mod_manager/hub_tab_collections = 컬렉션
mod_manager/hub_back_to_menu = ← 메뉴
mod_manager/hub_back_to_gallery = ← 게임
mod_manager/hub_back_to_mods = ← 모드
mod_manager/hub_in_one_collection = 컬렉션 {0}개에 포함
mod_manager/hub_in_many_collections = 컬렉션 {0}개에 포함
# Gallery of ready games
mod_manager/gallery_crumb = 완성된 게임
mod_manager/gallery_title = 게임
mod_manager/gallery_hint = 완성된 모드 세트: 버튼 하나로 모두 설치
mod_manager/gallery_search_placeholder = 컬렉션 검색...
mod_manager/gallery_new_game = + 나만의 게임 만들기
mod_manager/gallery_new_game_blank = + 처음부터 나만의 게임 만들기
mod_manager/gallery_empty_hint = 내 모드에 JSON 파일이 든 collections/ 폴더를 추가하면 컬렉션이 여기에 나타납니다.
mod_manager/gallery_edit_scheme = 구성 편집
mod_manager/gallery_ready = 플레이 준비 완료
mod_manager/gallery_ready_done = 준비됨 ✓
mod_manager/gallery_missing_n = {0}개 부족
mod_manager/gallery_problems_n = 문제: {0}
mod_manager/gallery_enable_badge_n = {0}개 켜기
mod_manager/gallery_install_btn_n = 설치 ({0})
mod_manager/gallery_enable_btn_n = 켜기 ({0})
mod_manager/gallery_draft = 초안
mod_manager/gallery_continue = 계속 조립하기
# Bottom build bar (scheme editor)
mod_manager/build_caption = 조립
mod_manager/build_stat_install = 설치
mod_manager/build_stat_enable = 켜기
mod_manager/build_stat_problems = 문제
mod_manager/build_hint_problems = 컬렉션을 적용하려면 문제를 해결하세요
mod_manager/build_open_manager = 모드 관리자 열기
mod_manager/collection_crumb_scheme = 구성
mod_manager/collection_find_in_catalog = 카탈로그에서 찾기
mod_manager/collection_duplicate = 복제
mod_manager/collection_revert = 기본 제공 버전으로 되돌리기
mod_manager/collection_saves = 저장 파일: {0}
mod_manager/collection_next_launch = 변경 사항은 다음에 게임을 시작할 때 적용됩니다.
mod_manager/collection_json_apply = 적용
mod_manager/collection_json_invalid = 잘못된 JSON: {0}
mod_manager/collection_json_id_changed = 컬렉션 id는 바꿀 수 없습니다 — 새 게임은 "복제"를 쓰세요.
mod_manager/collection_copy_name = {0} (사본)
mod_manager/collection_copy_saves = 원래 게임에 저장 파일이 있습니다({0}). 새 게임으로 복사할까요?
mod_manager/collection_copy_saves_ok = 복사
mod_manager/collection_revert_confirm = 변경 사항을 버리고 게임의 기본 제공 버전으로 되돌릴까요? 저장 파일은 남습니다.
mod_manager/collection_revert_done = 기본 제공 버전으로 되돌렸습니다.
mod_manager/collection_delete = 컬렉션 삭제
mod_manager/collection_delete_confirm = 컬렉션 "{0}"이(가) 삭제됩니다. 모드는 시스템에 설치된 채로, 저장 파일({1})은 디스크에 남습니다.
mod_manager/collection_delete_done = 컬렉션을 삭제했습니다. 모드는 시스템에 남아 있습니다.
mod_manager/collection_nested_readonly = 이 블록은 더 깊이 중첩되어 있습니다 — 바꾸려면 따로 여세요.
mod_manager/collection_forked = 블록 "{0}"을(를) 이 게임용으로 복사했습니다 — 다른 게임은 그대로입니다.
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_mods_title = 모드
mod_manager/help_mods_body = 이 장치에 설치된 모드 목록입니다. 헤더의 검색 상자로 이름을 찾고, 새로 고침 버튼으로 카탈로그를 다시 읽습니다. 방금 설치한 모드도 새로 고치면 바로 목록에 나타납니다. 모드 카드를 클릭하면 설명, 버전, 동작이 있는 페이지가 열립니다. 각 카드의 스위치는 그 모드를 사용하는 게임에서 모드를 켜거나 끕니다. 꺼진 모드는 파일을 유지하지만 불러오지 않습니다. 꺼진 모드의 장면과 콘텐츠는 다시 켤 때까지 실행 중인 게임에서 사라지지만, 저장 파일은 언제나 유지됩니다.
mod_manager/help_mod_page_title = 모드 페이지
mod_manager/help_mod_page_body = 이 페이지는 하나의 모드를 자세히 보여 줍니다. 설명, 제작자, 버전, 의존성 목록입니다. 여기서 현재 게임에 대한 모드를 켜거나 끌 수 있고, 모드가 장면을 제공하면 그 장면을 바로 열 수도 있습니다. 설명 아래 링크는 그 모드가 속한 컬렉션으로 이동합니다. 뒤로 버튼과 Escape는 모드 목록으로 돌아갑니다. 모드의 켜기·끄기 상태는 게임과 함께 저장되어 다시 시작 후에도 유지되므로 매번 다시 정할 필요가 없습니다. 같은 파일을 가진 모드가 둘이면 어느 쪽이 이기는지도 페이지가 알려 줍니다.
mod_manager/help_collections_title = 컬렉션
mod_manager/help_collections_body = 컬렉션 탭에는 게임 컬렉션이 표시됩니다. 별개의 게임처럼 즐기는 모드 묶음입니다. 각 카드에는 표지, 제목, 컬렉션에 포함된 모드가 나옵니다. 카드를 클릭해 컬렉션을 열고, 구성 버튼으로 구성 편집기를 엽니다. 컬렉션을 설치하면 필요한 모드를 함께 가져옵니다. 컬렉션을 제거해도 저장 파일은 지워지지 않습니다. 게임 데이터는 잠들어 기다렸다가 컬렉션이 돌아오면 함께 돌아옵니다.
mod_manager/help_scheme_title = 구성 편집기
mod_manager/help_scheme_body = 구성 편집기는 컬렉션의 구성을 바꿉니다. 어떤 모드가 게임에 참여하고 어떤 순서로 불러오는지 여기서 정합니다. 체크 상자로 모드를 추가하거나 빼고, 행을 끌어서 불러오기 순서를 바꿉니다. 같은 파일을 가진 모드는 나중에 불러오는 쪽이 이깁니다. 저장하기 전에 구성을 검사하고 빠진 의존성을 경고합니다. 변경 사항은 다음 게임 시작에 적용됩니다. 저장 파일은 그대로 유지되고, 없는 모드의 데이터는 모드가 돌아올 때까지 잠듭니다.
mod_manager/sort_tooltip = 목록 정렬: 이름, 제작자, 설치 상태 기준.
mod_manager/filters_reset_tooltip = 모든 필터와 검색 상자를 기본값으로 되돌립니다.
mod_manager/collection_json_tooltip = 구성을 원본 JSON로 엽니다. 보고, 편집하고, 다시 적용할 수 있습니다.
mod_manager/collection_reset_tooltip = 저장하지 않은 구성 변경을 모두 버립니다.
mod_manager/collection_game_tooltip = 편집 중인 구성을 다른 컬렉션으로 바꿉니다.
mod_manager/collection_plan_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 = 의존 모드 → 이를 필요로 하는 모드. 카드 위치와 그룹 경계는 로드 순서를 정하지 않습니다. 그룹은 이 게임에서 모드의 용도를 설명합니다. 다른 게임에서는 역할이 다를 수 있습니다.
# Bundled mod documentation
mod_manager/documentation = 문서
mod_manager/docs_auto = 자동 · 인터페이스 언어
mod_manager/docs_default = 기본값
mod_manager/docs_source = 설치된 버전의 문서 {0} · {1}
mod_manager/docs_installed_notice = 다른 다운로드 버전이 선택되었습니다. 아래 안내는 설치된 모드에 해당합니다.
mod_manager/docs_image_unavailable = 이미지를 표시할 수 없음
mod_manager/hint_docs_language_title = 문서 언어
mod_manager/hint_docs_language_body = 자동은 인터페이스 언어를 따릅니다. 번역이 없으면 default.md, 영어, 첫 문서 순으로 표시합니다. 직접 언어를 선택할 수도 있습니다.
+101 -1
View File
@@ -1,7 +1,7 @@
# mod_manager — локализация менеджера модов
mod_mod_manager_name = Менеджер модов
mod_mod_manager_desc = Менеджер модов в стиле Nexus: каталог, фильтры по категориям, установка, обновление и удаление.
mod_mod_manager_desc = Экран «Моды и коллекции»: что установлено, что делает каждый мод, установка и обновление из онлайн-каталога и то, из каких модов собрана каждая готовая игра.
mod_manager/menu_title = Моды и коллекции
mod_manager/title = Менеджер модов
@@ -219,3 +219,103 @@ mod_manager/collection_delete_confirm = Коллекция «{0}» будет у
mod_manager/collection_delete_done = Коллекция удалена. Моды остались в системе.
mod_manager/collection_nested_readonly = Этот блок вложен глубже — откройте его отдельно, чтобы изменить.
mod_manager/collection_forked = Блок «{0}» скопирован для этой игры — другие игры не изменятся.
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/sort_tooltip = Сортировка списка: по имени, автору или состоянию установки.
mod_manager/filters_reset_tooltip = Вернуть все фильтры и поле поиска к значениям по умолчанию.
mod_manager/collection_json_tooltip = Открыть схему как чистый JSON: посмотреть, отредактировать и применить обратно.
mod_manager/collection_reset_tooltip = Отменить все несохранённые изменения схемы.
mod_manager/collection_game_tooltip = Переключить редактируемую схему на другую коллекцию.
mod_manager/collection_plan_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 = Зависимость → мод, которому она нужна. Положение карточки и границы групп не задают порядок загрузки. Группа задаёт назначение мода в этой игре; в другой игре его роль может отличаться.
# Bundled mod documentation
mod_manager/documentation = Документация
mod_manager/docs_auto = Авто · язык интерфейса
mod_manager/docs_default = По умолчанию
mod_manager/docs_source = Документация установленной версии {0} · {1}
mod_manager/docs_installed_notice = Выбрана другая версия для загрузки. Ниже показано руководство установленного мода.
mod_manager/docs_image_unavailable = Изображение недоступно
mod_manager/hint_docs_language_title = Язык документации
mod_manager/hint_docs_language_body = Авто использует язык интерфейса. Если перевода нет: default.md → английский → первый доступный документ. Можно выбрать язык вручную.
+321
View File
@@ -0,0 +1,321 @@
# mod_manager — mod manager localization
mod_mod_manager_name = Trình quản lý mod
mod_mod_manager_desc = Màn hình Mod và bộ sưu tập: cái nào đã cài, mỗi mod làm gì, cài đặt và cập nhật từ danh mục trực tuyến, và mỗi game làm sẵn được ghép từ những mod nào.
mod_manager/menu_title = Mod và bộ sưu tập
mod_manager/title = Trình quản lý mod
mod_manager/back = Đóng
mod_manager/search_placeholder = Tìm mod...
mod_manager/sort_name = Theo tên
mod_manager/sort_author = Theo tác giả
mod_manager/sort_status = Theo trạng thái
mod_manager/refresh = Làm mới danh mục
mod_manager/loading = Đang tải danh mục...
mod_manager/results = Mod: {0}
mod_manager/filters = Bộ lọc
mod_manager/reset_filters = Đặt lại
mod_manager/filter_status = Trạng thái
mod_manager/filter_categories = Thể loại
mod_manager/group_by_category = Nhóm theo thể loại
mod_manager/status_installed = Đã cài
mod_manager/status_not_installed = Chưa cài
mod_manager/status_update = Có bản cập nhật
mod_manager/status_downgrade = Có phiên bản cũ hơn
mod_manager/status_modified = Đã sửa cục bộ
mod_manager/status_disabled = Đã tắt
mod_manager/status_conflict = Xung đột danh mục
mod_manager/conflict_title = Cảnh báo
mod_manager/conflict_warning = Cùng một mod với cùng phiên bản và tiêu đề được các danh mục khác nhau phát hành với nội dung khác nhau. Một bản có thể là giả: chỉ cài từ nguồn đáng tin cậy.
mod_manager/source_catalog = Danh mục: {0}
mod_manager/source_local = Mod cục bộ
mod_manager/category_story = Cốt truyện
mod_manager/category_characters = Nhân vật
mod_manager/category_gameplay = Lối chơi
mod_manager/category_visual = Hình ảnh
mod_manager/category_audio = Âm thanh
mod_manager/category_ui = Giao diện
mod_manager/category_locale = Ngôn ngữ
mod_manager/category_library = Thư viện
mod_manager/category_engine = Engine
mod_manager/category_overhaul = Đại tu
mod_manager/category_tools = Công cụ
mod_manager/category_editor = Trình sửa
mod_manager/category_debug = Gỡ lỗi
mod_manager/category_other = Khác
mod_manager/action_install = Cài đặt
mod_manager/action_update = Cập nhật
mod_manager/action_downgrade = Hạ phiên bản
mod_manager/action_reinstall = Cài lại
mod_manager/action_delete = Xóa
mod_manager/action_enable = Bật
mod_manager/action_disable = Tắt
mod_manager/restart_required = Thay đổi có hiệu lực sau khi khởi động lại game
mod_manager/progress_installing = Đang cài "{0}"...
mod_manager/install_failed = Cài đặt thất bại. Hãy kiểm tra kết nối và thử lại.
mod_manager/delete_title = Xóa mod
mod_manager/delete_text = Xóa mod "{0}"? Không thể hoàn tác và có hiệu lực sau khi khởi động lại.
mod_manager/delete_confirm = Xóa
mod_manager/delete_failed = Không xóa được mod. Xem nhật ký.
mod_manager/cancel = Hủy
mod_manager/installed_version = Đã cài: {0}
mod_manager/not_installed_hint = Chưa cài
mod_manager/latest = mới nhất
mod_manager/installed_marker = đã cài
mod_manager/no_results = Không tìm thấy gì
mod_manager/description = Mô tả
mod_manager/depends = Phụ thuộc vào
mod_manager/used_by = Được dùng bởi
mod_manager/conflicts = Xung đột
mod_manager/changelog = Nhật ký thay đổi
mod_manager/version = Phiên bản:
mod_manager/self_hint = Mod hệ thống: không thể xóa hay tắt
mod_manager/collections_menu_title = Bộ sưu tập
mod_manager/collections_title = Bộ sưu tập
mod_manager/collections_empty = Không tìm thấy bộ sưu tập nào
mod_manager/collection_result = Mod: {0}/{1}
mod_manager/collection_mechanics = Cơ chế
mod_manager/collection_node = Mod
mod_manager/collection_plan = Kế hoạch cài đặt
mod_manager/collection_apply = Áp dụng bộ sưu tập
mod_manager/collection_plan_ready = Mọi thứ đã sẵn sàng
mod_manager/collection_install_count = Cần cài: {0}
mod_manager/collection_enable_count = Cần bật: {0}
mod_manager/collection_missing = Không có trong danh mục: {0}
mod_manager/collection_mismatch = Không khớp phiên bản: {0}
mod_manager/collection_optional = tùy chọn
mod_manager/collection_externals = Phụ thuộc
mod_manager/collection_external = phụ thuộc
mod_manager/collection_node_hint = Chọn một mod trên đồ thị
mod_manager/collection_requires = Cần: {0}
mod_manager/collection_origin_builtin = tích hợp
mod_manager/collection_origin_user = cục bộ
mod_manager/collection_origin_catalog = danh mục
mod_manager/collection_progress = Đang áp dụng "{0}"...
mod_manager/collection_apply_failed = Không áp dụng được bộ sưu tập. Xem nhật ký.
mod_manager/collection_state_installed = Đã cài
mod_manager/collection_state_disabled = Đã tắt
mod_manager/collection_state_catalog = Có trong danh mục
mod_manager/collection_state_missing = Không tìm thấy
mod_manager/collection_state_mismatch = Không khớp phiên bản
# Scheme editor (collections pyramid)
mod_manager/collection_add_mechanic = + Cơ chế
mod_manager/collection_new_game = + Game
mod_manager/collection_new_game_name = Game mới
mod_manager/collection_new_game_desc = Lắp ráp game từ các cơ chế: thêm khối và mod.
mod_manager/collection_new_mechanic_name = Cơ chế mới
mod_manager/collection_group_ready = sẵn sàng
mod_manager/collection_group_empty = trống
mod_manager/collection_group_off = tắt
mod_manager/collection_links = Liên kết
mod_manager/collection_problems = Vấn đề
mod_manager/collection_tab_inspector = Thanh thuộc tính
mod_manager/collection_tab_problems = Kiểm tra
mod_manager/collection_tab_plan = Kế hoạch
mod_manager/collection_plan_install = cần cài
mod_manager/collection_plan_enable = cần bật
mod_manager/collection_save = Lưu
mod_manager/collection_reset = Đặt lại
mod_manager/collection_json = JSON
mod_manager/collection_save_ok = Đã lưu vào user://collections.json
mod_manager/collection_no_changes = Không có gì để lưu
mod_manager/collection_reset_confirm = Bỏ các thay đổi chưa lưu?
mod_manager/collection_rename_title = Tên và mô tả
mod_manager/collection_field_name = Tên
mod_manager/collection_field_note = Mô tả
mod_manager/collection_add_mod = Thêm mod
mod_manager/collection_empty_mechanic = Cơ chế trống
mod_manager/collection_empty_mechanic_desc = Tạo một khung từ đầu: tự thêm tên, mô tả và mod.
mod_manager/collection_pick_mechanic = Thêm cơ chế
mod_manager/collection_pick_replace = Thay cơ chế
mod_manager/collection_choose = Thêm
mod_manager/collection_replace = Thay
mod_manager/collection_remove = Gỡ
mod_manager/collection_rename = Đổi tên
mod_manager/collection_create = Tạo
mod_manager/collection_where = Thêm vào
mod_manager/collection_new_extra = + cơ chế "Bổ sung" mới
mod_manager/collection_extra_name = Bổ sung
mod_manager/collection_extra_note = Các mod được thêm thủ công.
mod_manager/collection_search_mechanic = Tìm cơ chế...
mod_manager/collection_search_mod = Tìm mod...
mod_manager/collection_problems_none = Không có vấn đề — bản lắp ráp nhất quán.
mod_manager/collection_problem_missing = Không tìm thấy khối "{0}"
mod_manager/collection_problem_cycle = Vòng lặp: "{0}" -> "{1}"
mod_manager/collection_problem_role = Ô "{0}" bị nhận hai lần
mod_manager/collection_problem_require = "{0}": chưa đáp ứng yêu cầu "{1}"
mod_manager/collection_show = Hiển thị
mod_manager/collection_hint = Chọn một khung hoặc một mod. Kéo khung bằng tiêu đề, đổi kích thước ở góc, di chuyển mod tự do. Kéo vùng trống bằng chuột trái để cuộn.
mod_manager/collection_mechanic = cơ chế
mod_manager/collection_group = nhóm
mod_manager/collection_mods = mod
mod_manager/collection_in_group = Trong nhóm
mod_manager/collection_remove_mod = Gỡ khỏi cơ chế
mod_manager/collection_turn_on = Bật
mod_manager/collection_turn_off = Tắt
mod_manager/collection_unsaved = có thay đổi chưa lưu
# ─── Mods & collections hub ───
mod_manager/hub_tab_mods = Mod
mod_manager/hub_tab_collections = Bộ sưu tập
mod_manager/hub_back_to_menu = ← Menu
mod_manager/hub_back_to_gallery = ← Game
mod_manager/hub_back_to_mods = ← Mod
mod_manager/hub_in_one_collection = trong {0} bộ sưu tập
mod_manager/hub_in_many_collections = trong {0} bộ sưu tập
# Gallery of ready games
mod_manager/gallery_crumb = Game làm sẵn
mod_manager/gallery_title = Game
mod_manager/gallery_hint = bộ mod làm sẵn: một nút cài tất cả
mod_manager/gallery_search_placeholder = Tìm bộ sưu tập...
mod_manager/gallery_new_game = + Tự làm game của bạn
mod_manager/gallery_new_game_blank = + Tự làm game của bạn từ đầu
mod_manager/gallery_empty_hint = Thêm thư mục collections/ chứa các tệp JSON vào mod của bạn — các bộ sưu tập sẽ xuất hiện ở đây.
mod_manager/gallery_edit_scheme = Sửa sơ đồ
mod_manager/gallery_ready = Sẵn sàng để chơi
mod_manager/gallery_ready_done = Sẵn sàng ✓
mod_manager/gallery_missing_n = Thiếu {0}
mod_manager/gallery_problems_n = Vấn đề: {0}
mod_manager/gallery_enable_badge_n = Bật {0}
mod_manager/gallery_install_btn_n = Cài ({0})
mod_manager/gallery_enable_btn_n = Bật ({0})
mod_manager/gallery_draft = Bản nháp
mod_manager/gallery_continue = Tiếp tục lắp ráp
# Bottom build bar (scheme editor)
mod_manager/build_caption = Lắp ráp
mod_manager/build_stat_install = Cài
mod_manager/build_stat_enable = Bật
mod_manager/build_stat_problems = Vấn đề
mod_manager/build_hint_problems = Hãy sửa các vấn đề để áp dụng bộ sưu tập
mod_manager/build_open_manager = Mở trình quản lý mod
mod_manager/collection_crumb_scheme = Sơ đồ
mod_manager/collection_find_in_catalog = Tìm trong danh mục
mod_manager/collection_duplicate = Nhân bản
mod_manager/collection_revert = Khôi phục bản tích hợp
mod_manager/collection_saves = Bản lưu: {0}
mod_manager/collection_next_launch = Thay đổi được áp dụng vào lần chạy game tiếp theo.
mod_manager/collection_json_apply = Áp dụng
mod_manager/collection_json_invalid = JSON không hợp lệ: {0}
mod_manager/collection_json_id_changed = Không thể đổi id bộ sưu tập — dùng Nhân bản để tạo game mới.
mod_manager/collection_copy_name = {0} (bản sao)
mod_manager/collection_copy_saves = Game gốc có bản lưu ({0}). Sao chép chúng sang game mới?
mod_manager/collection_copy_saves_ok = Sao chép
mod_manager/collection_revert_confirm = Bỏ thay đổi và khôi phục phiên bản tích hợp của game? Bản lưu vẫn được giữ.
mod_manager/collection_revert_done = Đã khôi phục phiên bản tích hợp.
mod_manager/collection_delete = Xóa bộ sưu tập
mod_manager/collection_delete_confirm = Bộ sưu tập «{0}» sẽ bị xóa. Các mod của nó vẫn được cài trong hệ thống, và các bản lưu ({1}) vẫn ở trên đĩ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_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/help_open = Gợi ý theo ngữ cảnh (F1)
mod_manager/help_active = Di chuột lên điều khiển · Nhấp trái để ghim · Esc để thoát
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/enable_tooltip = Nạp mod này trong các game có sử dụng nó.
mod_manager/disable_tooltip = Không nạp mod này. Tệp và bản lưu vẫn được giữ.
mod_manager/hint_mods_tab_title = Mod
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/hint_collections_tab_title = Bộ sưu tập
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/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/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_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_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.
# 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.
# Bundled mod documentation
mod_manager/documentation = Tài liệu
mod_manager/docs_auto = Tự động · ngôn ngữ giao diện
mod_manager/docs_default = Mặc định
mod_manager/docs_source = Tài liệu phiên bản đã cài {0} · {1}
mod_manager/docs_installed_notice = Đang chọn phiên bản tải xuống khác. Hướng dẫn bên dưới dành cho mod đã cài.
mod_manager/docs_image_unavailable = Không có hình ảnh
mod_manager/hint_docs_language_title = Ngôn ngữ tài liệu
mod_manager/hint_docs_language_body = Tự động dùng ngôn ngữ giao diện. Nếu thiếu bản dịch: default.md, tiếng Anh, rồi tài liệu đầu tiên. Bạn cũng có thể chọn ngôn ngữ thủ công.
+321
View File
@@ -0,0 +1,321 @@
# mod_manager — mod manager localization
mod_mod_manager_name = 模组管理器
mod_mod_manager_desc = 模组与合集屏幕:装了什么、每个模组做什么、从在线目录安装和更新,以及每款现成游戏由哪些模组构成。
mod_manager/menu_title = 模组与合集
mod_manager/title = 模组管理器
mod_manager/back = 关闭
mod_manager/search_placeholder = 搜索模组...
mod_manager/sort_name = 按名称
mod_manager/sort_author = 按作者
mod_manager/sort_status = 按状态
mod_manager/refresh = 刷新目录
mod_manager/loading = 正在加载目录...
mod_manager/results = 模组:{0}
mod_manager/filters = 筛选
mod_manager/reset_filters = 重置
mod_manager/filter_status = 状态
mod_manager/filter_categories = 分类
mod_manager/group_by_category = 按分类分组
mod_manager/status_installed = 已安装
mod_manager/status_not_installed = 未安装
mod_manager/status_update = 有可用更新
mod_manager/status_downgrade = 有旧版本可用
mod_manager/status_modified = 本地已修改
mod_manager/status_disabled = 已停用
mod_manager/status_conflict = 目录冲突
mod_manager/conflict_title = 警告
mod_manager/conflict_warning = 同一个模组以相同的版本和标题由不同目录发布,但内容不同。其中一个可能是仿冒的:只从可信来源安装。
mod_manager/source_catalog = 目录:{0}
mod_manager/source_local = 本地模组
mod_manager/category_story = 剧情
mod_manager/category_characters = 角色
mod_manager/category_gameplay = 玩法
mod_manager/category_visual = 视觉
mod_manager/category_audio = 音频
mod_manager/category_ui = 界面
mod_manager/category_locale = 语言
mod_manager/category_library = 库
mod_manager/category_engine = 引擎
mod_manager/category_overhaul = 大型改动
mod_manager/category_tools = 工具
mod_manager/category_editor = 编辑器
mod_manager/category_debug = 调试
mod_manager/category_other = 其他
mod_manager/action_install = 安装
mod_manager/action_update = 更新
mod_manager/action_downgrade = 降级
mod_manager/action_reinstall = 重新安装
mod_manager/action_delete = 删除
mod_manager/action_enable = 启用
mod_manager/action_disable = 停用
mod_manager/restart_required = 重启游戏后更改才会生效
mod_manager/progress_installing = 正在安装"{0}"...
mod_manager/install_failed = 安装失败。请检查网络后重试。
mod_manager/delete_title = 删除模组
mod_manager/delete_text = 删除模组"{0}"?此操作不可撤销,重启后生效。
mod_manager/delete_confirm = 删除
mod_manager/delete_failed = 无法删除模组。请查看日志。
mod_manager/cancel = 取消
mod_manager/installed_version = 已安装:{0}
mod_manager/not_installed_hint = 未安装
mod_manager/latest = 最新
mod_manager/installed_marker = 已安装
mod_manager/no_results = 什么都没找到
mod_manager/description = 描述
mod_manager/depends = 依赖
mod_manager/used_by = 被使用于
mod_manager/conflicts = 冲突
mod_manager/changelog = 更新日志
mod_manager/version = 版本:
mod_manager/self_hint = 系统模组:不能删除或停用
mod_manager/collections_menu_title = 合集
mod_manager/collections_title = 合集
mod_manager/collections_empty = 没有找到合集
mod_manager/collection_result = 模组:{0}/{1}
mod_manager/collection_mechanics = 机制
mod_manager/collection_node = 模组
mod_manager/collection_plan = 安装计划
mod_manager/collection_apply = 应用合集
mod_manager/collection_plan_ready = 一切就绪
mod_manager/collection_install_count = 待安装:{0}
mod_manager/collection_enable_count = 待启用:{0}
mod_manager/collection_missing = 目录中缺少:{0}
mod_manager/collection_mismatch = 版本不匹配:{0}
mod_manager/collection_optional = 可选
mod_manager/collection_externals = 依赖
mod_manager/collection_external = 依赖
mod_manager/collection_node_hint = 在图上选择一个模组
mod_manager/collection_requires = 需要:{0}
mod_manager/collection_origin_builtin = 内置
mod_manager/collection_origin_user = 本地
mod_manager/collection_origin_catalog = 目录
mod_manager/collection_progress = 正在应用"{0}"...
mod_manager/collection_apply_failed = 无法应用合集。请查看日志。
mod_manager/collection_state_installed = 已安装
mod_manager/collection_state_disabled = 已停用
mod_manager/collection_state_catalog = 目录中可用
mod_manager/collection_state_missing = 未找到
mod_manager/collection_state_mismatch = 版本不匹配
# Scheme editor (collections pyramid)
mod_manager/collection_add_mechanic = + 机制
mod_manager/collection_new_game = + 游戏
mod_manager/collection_new_game_name = 新游戏
mod_manager/collection_new_game_desc = 用机制组装一个游戏:添加模块和模组。
mod_manager/collection_new_mechanic_name = 新机制
mod_manager/collection_group_ready = 就绪
mod_manager/collection_group_empty = 空
mod_manager/collection_group_off = 关闭
mod_manager/collection_links = 链接
mod_manager/collection_problems = 问题
mod_manager/collection_tab_inspector = 检查器
mod_manager/collection_tab_problems = 检查
mod_manager/collection_tab_plan = 计划
mod_manager/collection_plan_install = 待安装
mod_manager/collection_plan_enable = 待启用
mod_manager/collection_save = 保存
mod_manager/collection_reset = 重置
mod_manager/collection_json = JSON
mod_manager/collection_save_ok = 已保存到 user://collections.json
mod_manager/collection_no_changes = 没有需要保存的内容
mod_manager/collection_reset_confirm = 放弃未保存的更改?
mod_manager/collection_rename_title = 名称和描述
mod_manager/collection_field_name = 名称
mod_manager/collection_field_note = 描述
mod_manager/collection_add_mod = 添加模组
mod_manager/collection_empty_mechanic = 空机制
mod_manager/collection_empty_mechanic_desc = 从零创建一个框架:手动添加名称、描述和模组。
mod_manager/collection_pick_mechanic = 添加机制
mod_manager/collection_pick_replace = 替换机制
mod_manager/collection_choose = 添加
mod_manager/collection_replace = 替换
mod_manager/collection_remove = 移除
mod_manager/collection_rename = 重命名
mod_manager/collection_create = 创建
mod_manager/collection_where = 添加到
mod_manager/collection_new_extra = + 新的"额外"机制
mod_manager/collection_extra_name = 额外
mod_manager/collection_extra_note = 手动添加的模组。
mod_manager/collection_search_mechanic = 搜索机制...
mod_manager/collection_search_mod = 搜索模组...
mod_manager/collection_problems_none = 没有问题——组装是一致的。
mod_manager/collection_problem_missing = 找不到模块"{0}"
mod_manager/collection_problem_cycle = 循环:"{0}" -> "{1}"
mod_manager/collection_problem_role = 槽位"{0}"被占用了两次
mod_manager/collection_problem_require = "{0}":要求"{1}"未满足
mod_manager/collection_show = 显示
mod_manager/collection_hint = 选择一个框架或模组。拖动标题移动框架,拖动角落调整大小,模组可以自由移动。用左键拖动空白画布来平移。
mod_manager/collection_mechanic = 机制
mod_manager/collection_group = 组
mod_manager/collection_mods = 模组
mod_manager/collection_in_group = 所在组
mod_manager/collection_remove_mod = 从机制中移除
mod_manager/collection_turn_on = 启用
mod_manager/collection_turn_off = 停用
mod_manager/collection_unsaved = 有未保存的更改
# ─── Mods & collections hub ───
mod_manager/hub_tab_mods = 模组
mod_manager/hub_tab_collections = 合集
mod_manager/hub_back_to_menu = ← 菜单
mod_manager/hub_back_to_gallery = ← 游戏
mod_manager/hub_back_to_mods = ← 模组
mod_manager/hub_in_one_collection = 在 {0} 个合集中
mod_manager/hub_in_many_collections = 在 {0} 个合集中
# Gallery of ready games
mod_manager/gallery_crumb = 现成的游戏
mod_manager/gallery_title = 游戏
mod_manager/gallery_hint = 现成的模组组合:一个按钮全部安装
mod_manager/gallery_search_placeholder = 搜索合集...
mod_manager/gallery_new_game = + 打造你自己的游戏
mod_manager/gallery_new_game_blank = + 从零打造你自己的游戏
mod_manager/gallery_empty_hint = 在你的模组里添加一个装有 JSON 文件的 collections/ 文件夹——合集就会出现在这里。
mod_manager/gallery_edit_scheme = 编辑结构
mod_manager/gallery_ready = 可以开始游戏
mod_manager/gallery_ready_done = 就绪 ✓
mod_manager/gallery_missing_n = 缺少 {0}
mod_manager/gallery_problems_n = 问题:{0}
mod_manager/gallery_enable_badge_n = 启用 {0}
mod_manager/gallery_install_btn_n = 安装({0})
mod_manager/gallery_enable_btn_n = 启用({0})
mod_manager/gallery_draft = 草稿
mod_manager/gallery_continue = 继续组装
# Bottom build bar (scheme editor)
mod_manager/build_caption = 组装
mod_manager/build_stat_install = 安装
mod_manager/build_stat_enable = 启用
mod_manager/build_stat_problems = 问题
mod_manager/build_hint_problems = 解决问题后才能应用合集
mod_manager/build_open_manager = 打开模组管理器
mod_manager/collection_crumb_scheme = 结构
mod_manager/collection_find_in_catalog = 在目录中查找
mod_manager/collection_duplicate = 复制
mod_manager/collection_revert = 恢复内置版本
mod_manager/collection_saves = 存档:{0}
mod_manager/collection_next_launch = 更改会在下次启动游戏时生效。
mod_manager/collection_json_apply = 应用
mod_manager/collection_json_invalid = 无效的 JSON:{0}
mod_manager/collection_json_id_changed = 合集 id 不能更改——要新建游戏请使用"复制"。
mod_manager/collection_copy_name = {0}(副本)
mod_manager/collection_copy_saves = 原游戏有存档({0})。要复制到新游戏吗?
mod_manager/collection_copy_saves_ok = 复制
mod_manager/collection_revert_confirm = 放弃你的更改并恢复游戏的内置版本?存档会保留。
mod_manager/collection_revert_done = 已恢复内置版本。
mod_manager/collection_delete = 删除合集
mod_manager/collection_delete_confirm = 合集"{0}"将被删除。它的模组仍安装在系统中,它的存档({1})仍保留在磁盘上。
mod_manager/collection_delete_done = 合集已删除。它的模组仍在系统中。
mod_manager/collection_nested_readonly = 这个模块嵌套得更深——请单独打开它来修改。
mod_manager/collection_forked = 模块"{0}"已为本游戏复制——其他游戏保持不变。
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/sort_tooltip = 对列表排序:按名称、作者或安装状态。
mod_manager/filters_reset_tooltip = 把所有筛选条件和搜索框恢复为默认值。
mod_manager/collection_json_tooltip = 以原始 JSON 打开结构:查看、编辑后再应用回去。
mod_manager/collection_reset_tooltip = 放弃所有未保存的结构修改。
mod_manager/collection_game_tooltip = 把正在编辑的结构切换到另一个合集。
mod_manager/collection_plan_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 = 依赖项 → 使用它的模组。卡片位置和分组边界不决定加载顺序。 分组说明模组在本游戏中的用途;在其他游戏中,它可以有不同的作用。
# Bundled mod documentation
mod_manager/documentation = 文档
mod_manager/docs_auto = 自动 · 界面语言
mod_manager/docs_default = 默认
mod_manager/docs_source = 已安装版本的文档 {0} · {1}
mod_manager/docs_installed_notice = 当前选择了其他下载版本。下方指南适用于已安装的模组。
mod_manager/docs_image_unavailable = 图片不可用
mod_manager/hint_docs_language_title = 文档语言
mod_manager/hint_docs_language_body = 自动使用界面语言。缺少翻译时依次使用 default.md、英语、首个可用文档。也可手动选择语言。
+7 -1
View File
@@ -3,11 +3,12 @@
"name": "Mod Manager",
"version": "1.0.0",
"author": "ct",
"description": "Nexus-style mod manager: browse the catalog, filter by category and status, install, update and remove mods.",
"description": "The Mods & Collections screen: what is installed, what each mod does, installing and updating from the online catalog, and which mods make up each ready-made game.",
"category": "tools, ui",
"sources": [
"src/**/*.cs"
],
"test_sources": ["tests/*.cs"],
"depends": ["ui_base>=1"],
"main_menu": [
{
@@ -15,5 +16,10 @@
"scene_path": "scenes/mods_hub_screen.tscn",
"button": { "icon": "mod_manager", "locale": "mod_manager/menu_title", "index": 100 }
}
],
"test_suites": [
{ "id": "help", "activate": ["--itest=mod_help"] },
{ "id": "documentation", "activate": ["--itest=mod_documentation"] },
{ "id": "collection_settings", "activate": ["--itest=collection_settings"] }
]
}
+30
View File
@@ -1,6 +1,7 @@
[gd_scene format=3]
[ext_resource type="Script" path="src/UI/ModDetailView.cs" id="1_detail"]
[ext_resource type="Script" path="src/UI/ModMarkdownView.cs" id="2_markdown"]
[node name="ModDetailView" type="VBoxContainer"]
theme_override_constants/separation = 8
@@ -124,6 +125,35 @@ theme_override_font_sizes/font_size = 12
layout_mode = 2
autowrap_mode = 3
[node name="DocumentationBox" type="VBoxContainer" parent="Scroll/Content"]
visible = false
layout_mode = 2
theme_override_constants/separation = 10
[node name="Header" type="HBoxContainer" parent="Scroll/Content/DocumentationBox"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Caption" type="Label" parent="Scroll/Content/DocumentationBox/Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Languages" type="OptionButton" parent="Scroll/Content/DocumentationBox/Header"]
layout_mode = 2
[node name="Source" type="Label" parent="Scroll/Content/DocumentationBox"]
layout_mode = 2
autowrap_mode = 3
theme_override_font_sizes/font_size = 12
[node name="Body" type="RichTextLabel" parent="Scroll/Content/DocumentationBox"]
layout_mode = 2
size_flags_horizontal = 3
fit_content = true
scroll_active = false
selection_enabled = true
script = ExtResource("2_markdown")
[node name="ChangelogBox" type="VBoxContainer" parent="Scroll/Content"]
layout_mode = 2
theme_override_constants/separation = 4
+76
View File
@@ -0,0 +1,76 @@
[gd_scene format=3]
[ext_resource type="Script" path="src/UI/ModHelpPanel.cs" id="1_help"]
[node name="ModHelpPanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_help")
[node name="Highlight" type="Panel" parent="."]
visible = false
layout_mode = 0
mouse_filter = 2
[node name="Card" type="PanelContainer" parent="."]
visible = false
custom_minimum_size = Vector2(320, 0)
layout_mode = 0
mouse_filter = 0
[node name="Margin" type="MarginContainer" parent="Card"]
layout_mode = 2
theme_override_constants/margin_left = 12
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 12
theme_override_constants/margin_bottom = 10
[node name="Column" type="VBoxContainer" parent="Card/Margin"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="Title" type="Label" parent="Card/Margin/Column"]
layout_mode = 2
theme_override_font_sizes/font_size = 13
autowrap_mode = 3
[node name="Body" type="RichTextLabel" parent="Card/Margin/Column"]
custom_minimum_size = Vector2(296, 0)
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/normal_font_size = 12
bbcode_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
+124 -5
View File
@@ -2,6 +2,7 @@
[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/CollectionMiniMap.cs" id="3_map"]
[node name="ModCollectionsScreen" type="Control"]
layout_mode = 3
@@ -70,10 +71,9 @@ layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="EdgesToggle" type="CheckBox" parent="Margin/Root/Header"]
[node name="EdgesToggle" type="OptionButton" parent="Margin/Root/Header"]
layout_mode = 2
size_flags_vertical = 4
button_pressed = true
[node name="Problems" type="Button" parent="Margin/Root/Header"]
layout_mode = 2
@@ -112,25 +112,104 @@ size_flags_vertical = 4
layout_mode = 2
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="GameSettings" type="Button" parent="Margin/Root/Tools"]
layout_mode = 2
[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"]
layout_mode = 2
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
size_flags_horizontal = 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
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
size_flags_horizontal = 3
size_flags_vertical = 3
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"]
custom_minimum_size = Vector2(380, 0)
custom_minimum_size = Vector2(330, 0)
layout_mode = 2
[node name="DetailScroll" type="ScrollContainer" parent="Margin/Root/Body/Details"]
@@ -174,6 +253,46 @@ autowrap_mode = 3
layout_mode = 2
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"]
layout_mode = 2
theme_override_constants/separation = 6
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<!-- Rebuild the bundled parser with: dotnet build source_code/MarkdownDependencies.csproj.
Its BSD license is shipped alongside dll/Markdig.dll. No kernel dependency is needed. -->
<PackageReference Include="Markdig" Version="0.37.0" GeneratePathProperty="true" />
</ItemGroup>
<Target Name="CopyParser" AfterTargets="Build">
<Copy SourceFiles="$(PkgMarkdig)/lib/net6.0/Markdig.dll" DestinationFolder="../dll" />
</Target>
</Project>
+149
View File
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Mods.ModManager
{
/// <summary>
/// Lexical colouring for fenced documentation examples. Follows the site's
/// explicit-language approach and custom scenario/filestree/jsonc rules,
/// returning literal text spans rather than HTML or BBCode. Unknown languages,
/// oversized examples and timeouts retain their complete uncoloured source.
/// </summary>
public static class ModCodeSyntax
{
private const string Strings = "(?<string>\"\"\"[\\s\\S]*?\"\"\"|'''[\\s\\S]*?'''|(?:\\$@|@\\$|@)\"(?:\"\"|[^\"])*\"|\\$?\"(?:\\\\.|[^\"\\\\\\r\\n])*\"|'(?:\\\\.|[^'\\\\\\r\\n])*')";
private const string Tokens = @"(?<number>\b(?:0[xX][0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)[fFdDmM]?\b)|(?<variable>\$\{[^}\r\n]+\}|\$[A-Za-z_]\w*)|(?<name>[A-Za-z_]\w*)|(?<operator>[{}\[\](),:;=+*/<>!&|.\-?])";
private static readonly Regex CStyle = Create(@"(?<comment>//[^\r\n]*|/\*[\s\S]*?\*/)|" + Strings + "|" + Tokens);
private static readonly Regex Json = Create(Strings + "|" + Tokens);
private static readonly Regex HashStyle = Create(@"(?<comment>\#[^\r\n]*)|" + Strings + "|" + Tokens);
private static readonly Regex Scenario = Create(@"(?<meta>^---[ \t]*$)|(?<comment>\#[^\r\n]*|--[^\r\n]*)|" + Strings + @"|(?<label>:[A-Za-z_]\w*)|(?<operator>->)|" + Tokens);
private static readonly Regex Glsl = Create(@"(?<meta>^[ \t]*\#[^\r\n]*)|(?<comment>//[^\r\n]*|/\*[\s\S]*?\*/)|" + Strings + "|" + Tokens);
private static readonly Regex FileTree = Create(@"(?<treecomment>[#←][^\r\n]*)|(?<comment>[│├└][─ \t]*)|(?<directory>[\p{L}\p{N}_.{}-]+/|/)");
private static readonly Regex Properties = Create(@"(?<comment>^[ \t]*[#!;][^\r\n]*)|(?<meta>^[ \t]*\[[^\]\r\n]+\])|(?<attr>^[ \t]*[^=\r\n]+(?==))|(?<string>(?<==)[^\r\n]*)");
private static readonly Dictionary<string, HashSet<string>> Keywords = new(StringComparer.Ordinal)
{
["csharp"] = Words("abstract as async await base bool break byte case catch char checked class const continue decimal default delegate do double dynamic else enum event explicit extern false finally fixed float for foreach get global goto if implicit in init int interface internal is lock long namespace new null object operator out override params partial private protected public readonly record ref required return sbyte sealed set short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unmanaged unsafe ushort using value var virtual void volatile when where while with yield"),
["gdscript"] = Words("and as assert await break breakpoint class class_name const continue elif else enum extends false for func if in is match not null or pass preload return self signal static super true var void while yield"),
["scenario"] = Words("scene priority condition is_one_run save_allowed light_use_time background foreground background_color background_show_hide text p_text title select option switch_scene end action music music_play music_pause sound light_set delay effect animation interactive_add interactive_remove interactive_clear interactive_set show_dialog hide_dialog set flag empty goto"),
["glsl"] = Words("attribute bool break bvec2 bvec3 bvec4 case centroid coherent const continue default discard do double else flat float for highp if in inout int invariant ivec2 ivec3 ivec4 layout lowp mat2 mat3 mat4 mediump out precision readonly restrict return sampler2D samplerCube smooth struct switch uniform uint uvec2 uvec3 uvec4 varying vec2 vec3 vec4 void volatile while writeonly shader_type render_mode spatial canvas_item particles sky fog"),
["bash"] = Words("alias case cd do done echo elif else esac exit export fi for function if in local printf read readonly return set source then unset until while"),
["python"] = Words("and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield"),
};
/// <summary>Reads the first fence-info word and resolves the aliases used in mod documentation.</summary>
public static string Language(string info)
{
if (string.IsNullOrWhiteSpace(info))
return "";
var tag = info.Trim();
var end = tag.IndexOfAny(new[] { ' ', '\t', '\r', '\n' });
if (end >= 0)
tag = tag.Substring(0, end);
return tag.ToLowerInvariant() switch
{
"cs" or "c#" => "csharp",
"gd" => "gdscript",
"sh" or "shell" => "bash",
"py" => "python",
"yml" => "yaml",
"cfg" or "toml" => "ini",
"tscn" or "tres" => "ini",
"gdshader" or "shader" => "glsl",
var language => language,
};
}
/// <summary>
/// Returns spans whose concatenation is exactly source, including newlines,
/// tabs and trailing spaces. Kind names follow the site's syntax palette.
/// </summary>
public static IReadOnlyList<(string Text, string Kind)> Tokenize(string info, string source)
{
source ??= "";
var language = Language(info);
var grammar = Grammar(language);
if (grammar == null || source.Length > 65536)
return Plain(source);
var result = new List<(string Text, string Kind)>();
try
{
var offset = 0;
foreach (Match match in grammar.Matches(source))
{
if (match.Index > offset)
result.Add((source.Substring(offset, match.Index - offset), ""));
var kind = Kind(match, source, language);
result.Add((match.Value, kind));
offset = match.Index + match.Length;
if (result.Count > 10000)
return Plain(source);
}
if (offset < source.Length)
result.Add((source.Substring(offset), ""));
}
catch (RegexMatchTimeoutException)
{
return Plain(source);
}
return result;
}
private static IReadOnlyList<(string Text, string Kind)> Plain(string source)
=> new[] { (source, "") };
private static Regex Create(string pattern)
=> new Regex(pattern, RegexOptions.Multiline | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(100));
private static HashSet<string> Words(string words)
=> new HashSet<string>(words.Split(' '), StringComparer.Ordinal);
private static Regex Grammar(string language) => language switch
{
"json" => Json,
"jsonc" or "csharp" => CStyle,
"scenario" => Scenario,
"glsl" => Glsl,
"gdscript" or "bash" or "python" or "yaml" => HashStyle,
"filestree" => FileTree,
"ini" or "properties" => Properties,
_ => null,
};
private static string Kind(Match token, string source, string language)
{
foreach (var kind in new[] { "comment", "meta", "label", "directory", "treecomment", "attr" })
if (token.Groups[kind].Success)
return kind;
var next = token.Index + token.Length;
while (next < source.Length && (source[next] == ' ' || source[next] == '\t'))
next++;
var following = next < source.Length ? source[next] : '\0';
if (token.Groups["string"].Success)
return (language == "json" || language == "jsonc" || language == "yaml") && following == ':' ? "attr" : "string";
if (token.Groups["number"].Success)
return "number";
if (token.Groups["operator"].Success)
return "operator";
if (token.Groups["variable"].Success)
return language == "bash" ? "variable" : "";
var word = token.Value;
if (word == "true" || word == "false" || word == "null" || word == "True" || word == "False" || word == "None")
return "literal";
if (language == "scenario" && following == '=' || language == "yaml" && following == ':')
return "attr";
if (Keywords.TryGetValue(language, out var keywords) && keywords.Contains(word))
return "keyword";
if (language == "glsl" && word.StartsWith("gl_", StringComparison.Ordinal))
return "built_in";
if (language == "csharp" || language == "gdscript" || language == "python" || language == "glsl")
{
if (following == '(')
return "function";
if (char.IsUpper(word[0]))
return "type";
}
return "";
}
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Mods.ModManager
{
/// <summary>A readable, nonempty UTF-8 document bundled with an installed mod.</summary>
public sealed class ModDocument
{
public string Language { get; }
public string Path { get; }
public string Text { get; }
public ModDocument(string language, string path, string text)
{
Language = language;
Path = path;
Text = text;
}
}
}
+143
View File
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Cthangover.Core.Localization;
using Cthangover.Core.Mods;
using Cthangover.Core.Utils;
namespace Mods.ModManager
{
/// <summary>
/// Reads documentation independently of mod activation and storage format.
/// Only language-named files and default.md directly under docs participate
/// in fallback; README/CHANGELOG and nested assets never become translations.
/// </summary>
public static class ModDocumentation
{
/// <summary>Finds valid documents in stable path order; broken files do not hide other translations.</summary>
public static IReadOnlyList<ModDocument> Discover(IModFileProvider provider)
{
var result = new List<ModDocument>();
if (provider == null)
return result;
try
{
foreach (var path in provider.ListFiles("docs").Order(StringComparer.Ordinal))
{
if (!path.EndsWith(".md", StringComparison.Ordinal) || path.EndsWith("/"))
continue;
var language = System.IO.Path.GetFileNameWithoutExtension(path);
if (!IsLanguage(language))
continue;
var bytes = ReadResource(provider, path, 512 * 1024);
if (bytes == null)
continue;
try
{
var text = new UTF8Encoding(false, true).GetString(bytes).TrimStart('\uFEFF');
if (!string.IsNullOrWhiteSpace(text))
result.Add(new ModDocument(language, path, text));
}
catch (DecoderFallbackException)
{
GameLogger.Log("MODS", $"Documentation '{provider.Mod}/{path}' is not UTF-8", LogLevel.Warning);
}
if (result.Count >= 32)
break;
}
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is InvalidDataException || ex is ObjectDisposedException)
{
GameLogger.Log("MODS", $"Cannot list documentation for '{provider.Mod}': {ex.Message}", LogLevel.Warning);
}
return result;
}
/// <summary>Requested tag, primary language, default.md, English, then stable first document.</summary>
public static ModDocument Select(IReadOnlyList<ModDocument> documents, string language)
{
if (documents == null)
return null;
var tag = language?.Replace('_', '-');
foreach (var candidate in new[] { tag, LanguageCode.Primary(tag), "default", "en" })
foreach (var document in documents)
if (string.Equals(document.Language, candidate, StringComparison.OrdinalIgnoreCase))
return document;
return documents.FirstOrDefault();
}
/// <summary>Resolves local media relative to a document, rejecting schemes, absolute paths and escapes from docs.</summary>
public static string ResolveResource(string documentPath, string reference)
{
if (string.IsNullOrWhiteSpace(reference) || reference.StartsWith('/') || reference.StartsWith('\\')
|| reference.Contains(':') || reference.Contains('?') || reference.Contains('#'))
return null;
var path = ModPath.ResolveRelative(reference, documentPath);
return path.StartsWith("docs/", StringComparison.Ordinal) ? path : null;
}
/// <summary>
/// Bounded stream reads work for compressed entries too. Folder symlinks
/// are rejected along the resource path before delegating to the provider.
/// </summary>
public static byte[] ReadResource(IModFileProvider provider, string path, int limit)
{
if (provider == null || string.IsNullOrEmpty(path) || !path.StartsWith("docs/", StringComparison.Ordinal) || ModPath.Normalize(path) != path)
return null;
try
{
if (provider.RootPath != null && Directory.Exists(provider.RootPath))
{
var current = provider.RootPath;
foreach (var segment in path.Split('/'))
{
current = System.IO.Path.Combine(current, segment);
if ((File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0)
return null;
}
}
using var stream = provider.OpenStream(path);
if (stream == null)
return null;
using var output = new MemoryStream();
var buffer = new byte[16384];
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
{
if (output.Length + read > limit)
return null;
output.Write(buffer, 0, read);
}
return output.ToArray();
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is InvalidDataException || ex is ObjectDisposedException)
{
GameLogger.Log("MODS", $"Cannot read documentation resource '{provider.Mod}/{path}': {ex.Message}", LogLevel.Warning);
return null;
}
}
private static bool IsLanguage(string code)
{
if (code == "default")
return true;
var parts = code.Split('-');
if (parts[0].Length < 2 || parts[0].Length > 3 || parts.Length > 3)
return false;
foreach (var c in parts[0])
if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z'))
return false;
foreach (var part in parts)
{
if (part.Length == 0 || part.Length > 8)
return false;
foreach (var c in part)
if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9'))
return false;
}
return true;
}
}
}
+407 -26
View File
@@ -11,6 +11,7 @@ using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
using Cthangover.Mods.Interface.Tool;
namespace Mods.ModManager
{
/// <summary>
@@ -39,12 +40,13 @@ namespace Mods.ModManager
private Button _newGame;
private Button _apply;
private OptionButton _gameSelect;
private CheckBox _edgesToggle;
private OptionButton _edgesToggle;
private Button _pillProblems;
private Button _pillPlan;
private Label _modsCount;
private Label _dirty;
private Button _json;
private Button _gameSettings;
private Button _reset;
private Button _save;
private Label _bannerLabel;
@@ -86,6 +88,17 @@ namespace Mods.ModManager
private Button _revert;
private Button _delete;
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>
private readonly Dictionary<string, string> _duplicatedFrom = new(StringComparer.OrdinalIgnoreCase);
@@ -134,6 +147,7 @@ namespace Mods.ModManager
public void OpenCollection(string collectionId)
{
_currentId = collectionId;
_fitAfterBuild = true;
_selectedGroupKey = null;
_selectedModId = null;
// The gallery re-reads the store on every refresh; re-read it here
@@ -163,6 +177,254 @@ namespace Mods.ModManager
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()
{
_close = GetNode<Button>("Margin/Root/Header/Close");
@@ -170,17 +432,21 @@ namespace Mods.ModManager
_addMechanic = GetNode<Button>("Margin/Root/Header/AddMechanic");
_newGame = GetNode<Button>("Margin/Root/Header/NewGame");
_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");
_pillPlan = GetNode<Button>("Margin/Root/Header/Plan");
_modsCount = GetNode<Label>("Margin/Root/Header/ModsCount");
_dirty = GetNode<Label>("Margin/Root/Header/Dirty");
_json = GetNode<Button>("Margin/Root/Header/Json");
_gameSettings = GetNode<Button>("Margin/Root/Tools/GameSettings");
_gameSettings.Text = TranslationServer.Translate("mod_manager/game_settings_title");
_gameSettings.TooltipText = TranslationServer.Translate("mod_manager/game_settings_hint");
ModHelpPanel.Attach(_gameSettings, "game_settings");
_reset = GetNode<Button>("Margin/Root/Header/Reset");
_save = GetNode<Button>("Margin/Root/Header/Save");
_bannerPanel = GetNode<PanelContainer>("Margin/Root/Banner");
_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/";
_detailTitle = GetNode<Label>(detail + "DetailTitle");
@@ -199,6 +465,9 @@ namespace Mods.ModManager
_newGame.Text = TranslationServer.Translate("mod_manager/collection_new_game");
_pillProblems.Text = TranslationServer.Translate("mod_manager/collection_problems");
_pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan");
_gameSelect.TooltipText = TranslationServer.Translate("mod_manager/collection_game_tooltip");
_pillProblems.TooltipText = TranslationServer.Translate("mod_manager/collection_problems_tooltip");
_pillPlan.TooltipText = TranslationServer.Translate("mod_manager/collection_plan_tooltip");
_dirty.Text = TranslationServer.Translate("mod_manager/collection_unsaved");
_json.Text = TranslationServer.Translate("mod_manager/collection_json");
_reset.Text = TranslationServer.Translate("mod_manager/collection_reset");
@@ -206,8 +475,28 @@ namespace Mods.ModManager
_tabInspector.Text = TranslationServer.Translate("mod_manager/collection_tab_inspector");
_tabProblems.Text = TranslationServer.Translate("mod_manager/collection_tab_problems");
_tabPlan.Text = TranslationServer.Translate("mod_manager/collection_tab_plan");
_json.TooltipText = TranslationServer.Translate("mod_manager/collection_json_tooltip");
_reset.TooltipText = TranslationServer.Translate("mod_manager/collection_reset_tooltip");
_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"));
_edgesToggle.AddItem(TranslationServer.Translate("mod_manager/collection_links_all"));
_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();
_savesInfo = new Label { VerticalAlignment = VerticalAlignment.Center };
@@ -215,6 +504,7 @@ namespace Mods.ModManager
_duplicate = new Button { Text = TranslationServer.Translate("mod_manager/collection_duplicate") };
_revert = new Button { Text = TranslationServer.Translate("mod_manager/collection_revert") };
_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 })
{
header.AddChild(control);
@@ -237,6 +527,7 @@ namespace Mods.ModManager
_bannerLabel.AddThemeColorOverride("font_color", UiTokens.Warning);
_bannerPanel.Visible = false;
BindGraphiteControls();
BuildCrumbs();
BuildBuildBar();
if (Embedded)
@@ -256,7 +547,7 @@ namespace Mods.ModManager
_crumbCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"));
_crumbCollections.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
_crumbCollections.Pressed += () => GalleryRequested?.Invoke();
_crumbCollections.Pressed += OnGalleryPressed;
_crumbCollections.TooltipText = TranslationServer.Translate("mod_manager/gallery_crumb");
_crumbs.AddChild(_crumbCollections);
_crumbs.AddChild(UiKit.Label("›", UiTokens.FontBodySmall, UiTokens.Faint));
@@ -266,10 +557,19 @@ namespace Mods.ModManager
_crumbs.AddChild(UiKit.Label(TranslationServer.Translate("mod_manager/collection_crumb_scheme"), UiTokens.FontBodySmall, UiTokens.Body));
_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);
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>
@@ -302,11 +602,13 @@ namespace Mods.ModManager
row.AddChild(_buildHint);
row.AddChild(UiKit.Spacer());
_buildManager = UiKit.Button(TranslationServer.Translate("mod_manager/build_open_manager"));
_buildManager.Pressed += () => OpenManagerRequested?.Invoke(null);
_buildManager = UiKit.Button(TranslationServer.Translate("mod_manager/collection_tab_problems"));
ModHelpPanel.Attach(_buildManager, "collection_plan");
_buildManager.Pressed += OnBuildManagerPressed;
row.AddChild(_buildManager);
_buildApply = UiKit.Button(TranslationServer.Translate("mod_manager/collection_apply"), ButtonKind.Primary);
ModHelpPanel.Attach(_buildApply, "collection_apply");
_buildApply.Pressed += OnApplyPressed;
row.AddChild(_buildApply);
}
@@ -328,6 +630,7 @@ namespace Mods.ModManager
_close.Visible = false;
_refresh.Visible = false;
_newGame.Visible = false;
_pillPlan.Visible = false;
}
private void SubscribeEvents()
@@ -336,10 +639,11 @@ namespace Mods.ModManager
_refresh.Pressed += OnRefreshPressed;
_addMechanic.Pressed += OnAddMechanicPressed;
_newGame.Pressed += OnNewGamePressed;
_edgesToggle.Toggled += _canvas.SetEdgesVisible;
_edgesToggle.ItemSelected += _canvas.SetEdgeMode;
_pillProblems.Pressed += () => SelectTab("problems");
_pillPlan.Pressed += () => SelectTab("plan");
_json.Pressed += OpenJsonDialog;
_gameSettings.Pressed += OpenGameSettings;
_reset.Pressed += ResetDrafts;
_save.Pressed += OpenSaveDialog;
_gameSelect.ItemSelected += OnGameSelected;
@@ -352,6 +656,7 @@ namespace Mods.ModManager
_canvas.ModSelected += OnModSelected;
_canvas.GroupCommand += OnGroupCommand;
_canvas.LayoutChanged += OnLayoutChanged;
_canvas.ViewChanged += OnCanvasViewChanged;
ModDownloader.OnProgress += OnInstallProgress;
CatalogService.OnRefreshed += OnCatalogRefreshed;
@@ -361,6 +666,8 @@ namespace Mods.ModManager
{
ModDownloader.OnProgress -= OnInstallProgress;
CatalogService.OnRefreshed -= OnCatalogRefreshed;
if (_fitQueued)
GetTree().ProcessFrame -= OnCanvasFirstFrame;
}
/* ═══════════════ Data flow ═══════════════ */
@@ -379,12 +686,37 @@ namespace Mods.ModManager
private void Rebuild()
{
_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);
_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.SetSelection(_selectedGroupKey, _selectedModId);
_canvas.SetEdgesVisible(_edgesToggle.ButtonPressed);
_canvas.SetEdgeMode(_edgesToggle.Selected);
if (_fitAfterBuild && !_fitQueued)
{
_fitQueued = true;
GetTree().ProcessFrame += OnCanvasFirstFrame;
}
UpdateHeader();
UpdateInspector();
@@ -513,7 +845,7 @@ namespace Mods.ModManager
var current = EffectiveCollection(_currentId);
var isGame = current?.Kind == ModCollectionKind.Game;
_savesInfo.Visible = isGame;
_savesInfo.Visible = isGame && !Embedded;
if (isGame)
_savesInfo.Text = string.Format(TranslationServer.Translate("mod_manager/collection_saves"), SaveService.ListSaves(_currentId).Count);
_duplicate.Disabled = current == null;
@@ -542,6 +874,11 @@ namespace Mods.ModManager
_pillPlan.Text = TranslationServer.Translate("mod_manager/collection_plan");
return;
}
if (errors > 0)
{
_pillPlan.Text = $"{TranslationServer.Translate("mod_manager/collection_problems")}: {errors}";
return;
}
var parts = new List<string>();
if (_plan.Install.Count > 0)
@@ -554,7 +891,7 @@ namespace Mods.ModManager
}
private int CountErrors()
=> _graph?.Problems?.Count ?? 0;
=> (_graph?.Problems?.Count ?? 0) + (_plan?.Missing.Count ?? 0) + (_plan?.Mismatches.Count ?? 0);
private void OnGameSelected(long index)
{
@@ -563,6 +900,7 @@ namespace Mods.ModManager
return;
_currentId = id;
_fitAfterBuild = true;
_selectedGroupKey = null;
_selectedModId = null;
Rebuild();
@@ -570,6 +908,7 @@ namespace Mods.ModManager
private void OnNewGamePressed()
{
_fitAfterBuild = true;
var id = UniqueGameId();
var draft = new ModCollection
{
@@ -736,6 +1075,8 @@ namespace Mods.ModManager
_tabPlan.ButtonPressed = _tab == "plan";
ClearChildren(_detailExtra);
_versionEditor.Visible = false;
_versionReference = null;
_apply.Visible = !Embedded && _tab == "plan";
if (_tab == "problems")
@@ -784,6 +1125,7 @@ namespace Mods.ModManager
_detailMeta.Text = string.Join(" · ", metaParts);
_detailDescription.Text = string.IsNullOrEmpty(group.Description) ? "—" : group.Description;
AddInfoLine(TranslationServer.Translate("mod_manager/collection_role_hint"));
var collection = EffectiveCollection(group.CollectionId);
if (collection != null)
@@ -819,18 +1161,23 @@ namespace Mods.ModManager
{
var node = _graph?.Nodes?.FirstOrDefault(n => string.Equals(n.ModId, modId, StringComparison.OrdinalIgnoreCase));
_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);
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);
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)
{
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"),
() => RemoveModFromGroup(group, modId), true);
@@ -871,13 +1218,16 @@ namespace Mods.ModManager
_detailMeta.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));
UpdateApplyButton();
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)
{
var row = UiKit.Column(UiTokens.Space1);
@@ -958,7 +1308,7 @@ namespace Mods.ModManager
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_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();
}
@@ -996,7 +1346,7 @@ namespace Mods.ModManager
var canApply = !_installing
&& _plan != null
&& !_plan.IsEmpty
&& _plan.Missing.Count == 0
&& !_plan.HasProblems
&& CountErrors() == 0;
_apply.Disabled = !canApply;
if (_buildApply != null)
@@ -1031,7 +1381,7 @@ namespace Mods.ModManager
_buildHint.Text = TranslationServer.Translate(blocked
? "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);
}
@@ -1479,6 +1829,30 @@ namespace Mods.ModManager
Rebuild();
}
private void OpenGameSettings()
{
var collection = EffectiveCollection(_currentId);
if (collection == null)
return;
var discovered = ModRegistry.Instance.AllDiscoveredMods;
var ordered = ModRegistry.Instance.GetOrderedModIds(false).Where(discovered.ContainsKey).Select(id => discovered[id]);
var available = Resolve().Nodes.Where(n => n.State == ModCollectionNodeState.Installed).Select(n => n.ModId);
var catalog = new CollectionSettingsCatalog(ordered, available, TranslationServer.GetLocale());
var dialog = new CollectionSettingsDialog();
AddChild(dialog);
dialog.Initialize(catalog, collection.Settings, ApplyGameSettings);
dialog.PopupCenteredClamped(new Vector2I(760, 660), 0.9f);
}
private void ApplyGameSettings(Dictionary<string, string> settings)
{
var current = EffectiveCollection(_currentId)?.Settings;
if ((current?.Count ?? 0) == settings.Count && settings.All(p => current != null && current.TryGetValue(p.Key, out var value) && value == p.Value))
return;
Draft(_currentId).Settings = settings.Count == 0 ? null : settings;
Rebuild();
}
private void OpenSaveDialog()
{
var draft = EffectiveCollection(_currentId);
@@ -1588,12 +1962,17 @@ namespace Mods.ModManager
if (collection == null)
return;
var view = new TextEdit
var view = new CodeEdit
{
Text = JsonSerializer.Serialize(collection, JsonOptionsProvider.WriteIndentedOptions),
Editable = true,
GuttersDrawLineNumbers = true,
SyntaxHighlighter = SourceSyntax.CreateJsonHighlighter(),
IndentUseSpaces = true,
IndentSize = 2,
CustomMinimumSize = new Vector2(620, 420),
};
view.AddThemeFontOverride("font", ToolWindow.GetMonospaceFont());
var dialog = new ConfirmationDialog
{
@@ -1780,6 +2159,8 @@ namespace Mods.ModManager
}
include.Ref = forkId;
if (string.Equals(_selectedGroupKey, group.Key, StringComparison.OrdinalIgnoreCase))
_selectedGroupKey = $"i:{group.ParentCollectionId}:{forkId}";
return Draft(forkId);
}
@@ -1831,7 +2212,7 @@ namespace Mods.ModManager
private async void OnApplyPressed()
{
if (_installing || _plan == null || _plan.IsEmpty
|| _plan.Missing.Count > 0 || CountErrors() > 0)
|| _plan.HasProblems || CountErrors() > 0)
return;
var plan = _plan;
+63 -3
View File
@@ -35,6 +35,7 @@ namespace Mods.ModManager
private const string TabCollections = "collections";
private const string ModsScene = "scenes/mod_manager_screen.tscn";
private const string EditorScene = "scenes/mod_collections_screen.tscn";
private const string HelpScene = "scenes/components/mod_help_panel.tscn";
/// <summary>Active tab, remembered for the whole game session (mockup: "state is remembered").</summary>
private static string _activeTab = TabMods;
@@ -53,6 +54,8 @@ namespace Mods.ModManager
private ModManagerScreen _mods;
private CollectionsGallery _gallery;
private ModCollectionsScreen _editor;
private ModHelpPanel _help;
private Button _helpButton;
private bool _editorOpen;
/// <summary>Mods installed this session, shared by the gallery and the editor so both stay honest before a restart.</summary>
@@ -66,6 +69,7 @@ namespace Mods.ModManager
BuildHeader();
BuildBanner();
BuildBody();
BuildHelp();
ShowTab(_activeTab);
}
@@ -99,12 +103,14 @@ namespace Mods.ModManager
_tabMods.ToggleMode = true;
_tabMods.ButtonGroup = group;
_tabMods.Pressed += () => ShowTab(TabMods);
ModHelpPanel.Attach(_tabMods, "mods_tab");
_segment.AddChild(_tabMods);
_tabCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"), ButtonKind.Tab);
_tabCollections.ToggleMode = true;
_tabCollections.ButtonGroup = group;
_tabCollections.Pressed += () => ShowTab(TabCollections);
ModHelpPanel.Attach(_tabCollections, "collections_tab");
_segment.AddChild(_tabCollections);
header.AddChild(UiKit.Spacer());
@@ -116,11 +122,47 @@ namespace Mods.ModManager
SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
};
_gallerySearch.TextChanged += OnGallerySearchChanged;
ModHelpPanel.Attach(_gallerySearch, "collection_search");
header.AddChild(_gallerySearch);
_galleryRefresh = UiKit.Button(TranslationServer.Translate("mod_manager/refresh"));
_galleryRefresh.Pressed += OnGalleryRefreshPressed;
ModHelpPanel.Attach(_galleryRefresh, "refresh");
header.AddChild(_galleryRefresh);
_helpButton = UiKit.IconButton("?", TranslationServer.Translate("mod_manager/help_open"));
_helpButton.Name = "HelpButton";
_helpButton.ToggleMode = true;
_helpButton.Pressed += OnHelpPressed;
header.AddChild(_helpButton);
}
/// <summary>Builds the non-modal inspection layer above every hub level.</summary>
private void BuildHelp()
{
_help = TscnScenes.LoadAndBuild(HelpScene) as ModHelpPanel;
if (_help == null)
{
GameLogger.Log("MOD_HUB", $"Failed to build '{HelpScene}'", LogLevel.Error);
return;
}
AddChild(_help);
_help.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
_help.EnsureConstructed();
_help.Visible = false;
}
private void OnHelpPressed()
{
if (_help == null)
return;
if (_help.Visible)
_help.Close();
else
_help.Open();
_helpButton.ButtonPressed = _help.Visible;
}
private void BuildBanner()
@@ -216,7 +258,6 @@ namespace Mods.ModManager
if (_editorOpen && _editor != null)
{
_back.Text = TranslationServer.Translate("mod_manager/hub_back_to_gallery");
_back.TooltipText = _back.Text;
_title.Text = _editor.CurrentCollectionTitle ?? TranslationServer.Translate("mod_manager/collections_title");
_segment.Visible = false;
_gallerySearch.Visible = false;
@@ -225,7 +266,6 @@ namespace Mods.ModManager
}
_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");
_segment.Visible = true;
_tabMods.ButtonPressed = _activeTab == TabMods;
@@ -357,12 +397,32 @@ namespace Mods.ModManager
/* ═══════════════ 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)
{
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"))
return;
if (_help != null && _help.Visible)
{
_help.Close();
_helpButton.ButtonPressed = false;
GetViewport().SetInputAsHandled();
return;
}
if (_editorOpen)
{
ShowGallery();
+4
View File
@@ -133,6 +133,10 @@ namespace Mods.ModManager
_sort.AddItem(TranslationServer.Translate("mod_manager/sort_author"));
_sort.AddItem(TranslationServer.Translate("mod_manager/sort_status"));
_sort.Selected = (int)_filter.Sort;
_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(
UiTokens.Alpha(UiTokens.Warning, 0.15f),
@@ -0,0 +1,25 @@
namespace Mods.ModManager
{
using System.Collections.Generic;
using System.Text.Json.Serialization;
/// <summary>A mod-owned collection parameter, readable before that mod's assembly is loaded.</summary>
public class CollectionSettingDefinition
{
public string Key { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Group { get; set; }
/// <summary>choice, resource or character. Resource/character sources name a mod-relative folder.</summary>
public string Type { get; set; } = "choice";
public string Source { get; set; }
public bool AllowCustom { get; set; }
/// <summary>Explanation of the runtime fallback, not a value to persist.</summary>
public string Automatic { get; set; }
/// <summary>Optional anchored validation expression for custom input.</summary>
public string Pattern { get; set; }
public List<CollectionSettingOption> Options { get; set; } = new();
[JsonIgnore] public string ModId { get; set; }
[JsonIgnore] public bool Available { get; set; }
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Mods.ModManager
{
using System.Text.Json.Serialization;
using Cthangover.Core.Mods;
/// <summary>An explicit mod contribution or an indexed resource; availability belongs to the edited collection.</summary>
public class CollectionSettingOption
{
public string Key { get; set; }
public string Value { get; set; }
public string Name { get; set; }
[JsonIgnore] public string ModId { get; set; }
[JsonIgnore] public bool Available { get; set; }
[JsonIgnore] public IModFileProvider Provider { get; set; }
[JsonIgnore] public string PreviewPath { get; set; }
}
}
+223
View File
@@ -0,0 +1,223 @@
namespace Mods.ModManager
{
using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.Theming;
using Godot;
/// <summary>One draft field. Invalid saved values are retained until the player explicitly replaces them.</summary>
public partial class CollectionSettingRow : VBoxContainer
{
private CollectionSettingsCatalog catalog;
private CollectionSettingDefinition definition;
private IReadOnlyList<CollectionSettingOption> choices;
private OptionButton select;
private LineEdit custom;
private Label feedback;
private TextureRect preview;
private ImageTexture texture;
private string original;
private string value;
private bool changing;
private bool customMode;
private int customIndex = -1;
public string Key => definition.Key;
public string Value => customMode ? custom.Text.Trim() : value;
public bool Valid => Value == original || Value == null || (definition.AllowCustom && catalog.IsValidCustom(definition, Value))
|| choices.Any(o => o.Available && o.Value == Value);
/// <summary>Builds controls from the owner's descriptor; opening a form never creates a collection override.</summary>
public void Initialize(CollectionSettingsCatalog source, CollectionSettingDefinition field, string saved)
{
catalog = source;
definition = field;
original = value = saved;
Name = field.Key;
choices = catalog.Choices(field);
customMode = saved != null && field.AllowCustom && !choices.Any(o => o.Value == saved) && catalog.IsValidCustom(field, saved);
AddThemeConstantOverride("separation", UiTokens.Space2);
var title = Wrapped(catalog.Translate(field.Name), UiTokens.InkBright);
title.TooltipText = field.Key + " · " + field.ModId;
title.MouseFilter = MouseFilterEnum.Pass;
AddChild(title);
AddChild(Wrapped(catalog.Translate(field.Description), UiTokens.Muted));
var controls = UiKit.Row(UiTokens.Space2);
select = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill, FitToLongestItem = false, TooltipText = catalog.Translate(field.Description) };
select.AddItem(Tr("mod_manager/game_settings_automatic"));
// Renderer fallback is valid only when exactly one implementation is present.
select.SetItemDisabled(0, field.Type == "choice" && !field.AllowCustom && string.IsNullOrEmpty(field.Automatic) && choices.Count(o => o.Available) != 1);
foreach (var option in choices)
{
select.AddItem(OptionTitle(option));
select.SetItemDisabled(select.ItemCount - 1, !option.Available);
}
if (saved != null && !customMode && !choices.Any(o => o.Value == saved))
{
select.AddItem(saved + " · " + Tr("mod_manager/game_settings_unavailable"));
select.SetItemDisabled(select.ItemCount - 1, true);
}
if (field.AllowCustom)
{
customIndex = select.ItemCount;
select.AddItem(Tr("mod_manager/game_settings_custom"));
}
controls.AddChild(select);
if (field.Type is "resource" or "character")
{
var browse = UiKit.Button(Tr("mod_manager/game_settings_browse"));
browse.TooltipText = Tr("mod_manager/game_settings_search_hint");
browse.Pressed += OpenPicker;
controls.AddChild(browse);
}
AddChild(controls);
custom = new LineEdit { Text = saved ?? "", PlaceholderText = Tr("mod_manager/game_settings_custom"), TooltipText = field.Key, Visible = false };
custom.TextChanged += OnCustomChanged;
AddChild(custom);
if (field.Type == "resource")
{
preview = new TextureRect { CustomMinimumSize = new Vector2(0, 112), ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize, StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered, MouseFilter = MouseFilterEnum.Ignore };
AddChild(preview);
}
feedback = Wrapped("", UiTokens.Warning);
AddChild(feedback);
select.ItemSelected += OnSelected;
Refresh();
}
public override void _ExitTree() => ReleaseTexture();
private static string Tr(string suffix) => TranslationServer.Translate(suffix);
private static Label Wrapped(string text, Color color)
=> new Label { Text = text, AutowrapMode = TextServer.AutowrapMode.WordSmart, Modulate = color, SizeFlagsHorizontal = SizeFlags.ExpandFill };
private string OptionTitle(CollectionSettingOption option)
=> catalog.Translate(option.Name ?? option.Value) + (option.Name == option.Value ? "" : " (" + option.Value + ")")
+ (option.Available ? "" : " · " + Tr("mod_manager/game_settings_unavailable"));
private void OnSelected(long index)
{
if (changing)
return;
customMode = index == customIndex;
if (customMode)
value = custom.Text.Trim();
else if (index == 0)
value = null;
else if (index <= choices.Count)
value = choices[(int)index - 1].Value;
Refresh();
}
private void OnCustomChanged(string text)
{
if (changing)
return;
value = text.Trim();
UpdateFeedback();
}
private void Refresh()
{
changing = true;
var index = choices.ToList().FindIndex(o => o.Value == value);
var typed = customMode;
select.Select(typed ? customIndex : value == null ? 0 : index >= 0 ? index + 1 : choices.Count + 1);
custom.Visible = select.Selected == customIndex;
if (custom.Visible && custom.Text != value)
custom.Text = value ?? "";
changing = false;
UpdateFeedback();
UpdatePreview(index >= 0 ? choices[index] : null);
}
private void UpdateFeedback()
{
if (!definition.Available)
feedback.Text = Tr("mod_manager/game_settings_owner_missing");
else if (!Valid)
feedback.Text = Tr("mod_manager/game_settings_invalid");
else if (value == null)
feedback.Text = string.IsNullOrEmpty(definition.Automatic) ? Tr("mod_manager/game_settings_single_choice") : catalog.Translate(definition.Automatic);
else if (!choices.Any(o => o.Available && o.Value == value) && !(definition.AllowCustom && catalog.IsValidCustom(definition, value)))
feedback.Text = Tr("mod_manager/game_settings_missing");
else
feedback.Text = "";
}
private void ReleaseTexture()
{
if (preview != null)
preview.Texture = null;
texture?.Dispose();
texture = null;
}
private void UpdatePreview(CollectionSettingOption option)
{
if (preview == null)
return;
ReleaseTexture();
preview.Visible = option?.PreviewPath != null;
if (!preview.Visible)
return;
var data = option.Provider.ReadFileBinary(option.PreviewPath);
if (data == null)
return;
using var image = new Image();
var extension = System.IO.Path.GetExtension(option.PreviewPath).ToLowerInvariant();
var error = extension == ".webp" ? image.LoadWebpFromBuffer(data) : extension is ".jpg" or ".jpeg" ? image.LoadJpgFromBuffer(data) : image.LoadPngFromBuffer(data);
if (error == Error.Ok)
preview.Texture = texture = ImageTexture.CreateFromImage(image);
}
private void OpenPicker()
{
var dialog = new ConfirmationDialog { Title = catalog.Translate(definition.Name), OkButtonText = Tr("mod_manager/game_settings_choose"), CancelButtonText = TranslationServer.Translate("mod_manager/cancel") };
var column = UiKit.Column(UiTokens.Space2);
var search = new LineEdit { PlaceholderText = Tr("mod_manager/game_settings_search_hint"), TooltipText = Tr("mod_manager/game_settings_search_hint") };
var list = new ItemList { CustomMinimumSize = new Vector2(0, 80), SizeFlagsVertical = SizeFlags.ExpandFill, TooltipText = catalog.Translate(definition.Description) };
column.AddChild(search);
column.AddChild(list);
dialog.AddChild(column);
var filtered = new List<CollectionSettingOption>();
search.TextChanged += text => FillPicker(list, filtered, text);
dialog.Confirmed += () => ApplyPicker(dialog, list, filtered);
dialog.Canceled += dialog.QueueFree;
list.ItemActivated += _ => ApplyPicker(dialog, list, filtered);
AddChild(dialog);
FillPicker(list, filtered, "");
dialog.PopupCenteredClamped(new Vector2I(540, 440), 0.9f);
search.GrabFocus();
}
private void FillPicker(ItemList list, List<CollectionSettingOption> filtered, string search)
{
list.Clear();
filtered.Clear();
foreach (var option in choices.Where(o => OptionTitle(o).Contains(search, StringComparison.OrdinalIgnoreCase)))
{
filtered.Add(option);
var index = list.AddItem(OptionTitle(option));
list.SetItemDisabled(index, !option.Available);
list.SetItemTooltip(index, option.Value + " · " + option.ModId);
if (option.Value == value)
list.Select(index);
}
}
private void ApplyPicker(ConfirmationDialog dialog, ItemList list, List<CollectionSettingOption> filtered)
{
var selected = list.GetSelectedItems();
if (selected.Length > 0 && filtered[selected[0]].Available)
{
customMode = false;
value = filtered[selected[0]].Value;
Refresh();
}
dialog.QueueFree();
}
}
}
+182
View File
@@ -0,0 +1,182 @@
namespace Mods.ModManager
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using Cthangover.Core.Localization;
using Cthangover.Core.Mods;
using Cthangover.Core.Utils;
/// <summary>
/// Reads declarations, translations and resource choices from discovered folder/ZIP mods without loading
/// assemblies. Callers supply dependency order and the edited collection's available mod ids, never the
/// currently running game's registries. No static cache retains providers across reloads.
/// </summary>
public class CollectionSettingsCatalog
{
private static readonly JsonSerializerOptions Json = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true };
private readonly Dictionary<string, string> translations = new(StringComparer.Ordinal);
private readonly Dictionary<string, CollectionSettingDefinition> definitions = new(StringComparer.Ordinal);
private readonly List<CollectionSettingOption> contributions = new();
private readonly List<IModInfo> mods;
private readonly HashSet<string> available;
public IReadOnlyCollection<CollectionSettingDefinition> Definitions => definitions.Values;
public List<string> Errors { get; } = new();
/// <summary>Later mods override resource ids and translations, matching the supplied dependency order.</summary>
public CollectionSettingsCatalog(IEnumerable<IModInfo> orderedMods, IEnumerable<string> availableModIds, string language)
{
mods = orderedMods.ToList();
available = new HashSet<string>(availableModIds, StringComparer.OrdinalIgnoreCase);
// Read inactive owners first, so their fields still explain saved values after removal.
foreach (var mod in mods.OrderBy(m => available.Contains(m.Id)))
ReadDeclarations(mod);
foreach (var mod in mods)
ReadTranslations(mod.FileProvider, "en");
foreach (var mod in mods)
ReadTranslations(mod.FileProvider, LanguageCode.Primary(language));
}
/// <summary>Mod-local locale files are read even in frontend-only mode, with an English fallback.</summary>
public string Translate(string key) => key != null && translations.TryGetValue(key.TrimStart('@'), out var text) ? text : key ?? string.Empty;
/// <summary>Choices from declarations plus the selected collection's resources; unavailable contributions stay visible.</summary>
public IReadOnlyList<CollectionSettingOption> Choices(CollectionSettingDefinition definition)
{
var result = new Dictionary<string, CollectionSettingOption>(StringComparer.Ordinal);
foreach (var option in definition.Options ?? new List<CollectionSettingOption>())
{
option.ModId = definition.ModId;
option.Available = definition.Available;
if (!string.IsNullOrEmpty(option.Value))
result[option.Value] = option;
}
foreach (var option in contributions.Where(o => o.Key == definition.Key).OrderBy(o => o.Available))
result[option.Value] = option;
if (definition.Type is "resource" or "character" && !string.IsNullOrEmpty(definition.Source))
foreach (var mod in mods.Where(m => available.Contains(m.Id)))
ReadChoices(mod, definition, result);
return result.Values.OrderBy(o => o.Value, StringComparer.Ordinal).ToList();
}
/// <summary>Unknown/unavailable saved values may be retained; newly typed values must obey the owner's syntax.</summary>
public bool IsValidCustom(CollectionSettingDefinition definition, string value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
if (string.IsNullOrEmpty(definition.Pattern))
return true;
try { return Regex.IsMatch(value, definition.Pattern, RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(100)); }
catch (ArgumentException) { return false; }
catch (RegexMatchTimeoutException) { return false; }
}
private void ReadDeclarations(IModInfo mod)
{
try
{
var text = mod.FileProvider?.ReadFileText("collection_settings.json");
if (string.IsNullOrWhiteSpace(text))
return;
var file = JsonSerializer.Deserialize<CollectionSettingsFile>(text, Json);
foreach (var definition in file?.Settings ?? new List<CollectionSettingDefinition>())
{
if (definition == null || string.IsNullOrWhiteSpace(definition.Key))
continue;
definition.ModId = mod.Id;
definition.Available = available.Contains(mod.Id);
definitions[definition.Key] = definition;
}
foreach (var option in file?.Options ?? new List<CollectionSettingOption>())
{
if (option == null || string.IsNullOrEmpty(option.Key) || string.IsNullOrEmpty(option.Value))
continue;
option.ModId = mod.Id;
option.Available = available.Contains(mod.Id);
contributions.Add(option);
}
}
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
{
Errors.Add(mod.Id);
GameLogger.Log("MOD_COLLECTIONS", $"Invalid collection settings in '{mod.Id}': {ex.Message}", LogLevel.Warning);
}
}
private void ReadTranslations(IModFileProvider provider, string language)
{
if (provider == null || string.IsNullOrEmpty(language))
return;
foreach (var path in provider.ListFiles("locale").Order(StringComparer.Ordinal))
{
var name = Path.GetFileName(path);
if (name != language + ".properties" && !name.EndsWith("_" + language + ".properties", StringComparison.Ordinal))
continue;
foreach (var line in (provider.ReadFileText(path) ?? "").Split('\n'))
{
var trimmed = line.Trim();
var split = trimmed.IndexOf('=');
if (!trimmed.StartsWith('#') && split > 0)
translations[trimmed[..split].Trim()] = trimmed[(split + 1)..].Trim();
}
}
}
private void ReadChoices(IModInfo mod, CollectionSettingDefinition definition, Dictionary<string, CollectionSettingOption> result)
{
foreach (var path in Walk(mod.FileProvider, definition.Source))
{
var extension = Path.GetExtension(path).ToLowerInvariant();
if (definition.Type == "resource" && extension is ".png" or ".jpg" or ".jpeg" or ".webp")
{
var value = path[(definition.Source.Length + 1)..^extension.Length];
result[value] = new CollectionSettingOption { Value = value, Name = value, ModId = mod.Id, Available = true, Provider = mod.FileProvider, PreviewPath = path };
}
else if (definition.Type == "character" && extension == ".json")
ReadCharacters(mod, path, result);
}
}
private void ReadCharacters(IModInfo mod, string path, Dictionary<string, CollectionSettingOption> result)
{
try
{
using var document = JsonDocument.Parse(mod.FileProvider.ReadFileText(path));
var root = document.RootElement;
if (!root.TryGetProperty("Kind", out var kind) || kind.GetString() != "character"
|| !root.TryGetProperty("Items", out var items) || items.ValueKind != JsonValueKind.Array)
return;
foreach (var item in items.EnumerateArray())
{
if (!item.TryGetProperty("Id", out var id) || string.IsNullOrEmpty(id.GetString()))
continue;
var value = id.GetString();
var name = item.TryGetProperty("Name", out var title) ? title.GetString() : value;
result[value] = new CollectionSettingOption { Value = value, Name = name, ModId = mod.Id, Available = true };
}
}
catch (JsonException ex)
{
GameLogger.Log("MOD_COLLECTIONS", $"Cannot index '{mod.Id}/{path}': {ex.Message}", LogLevel.Warning);
}
}
private static IEnumerable<string> Walk(IModFileProvider provider, string folder)
{
foreach (var path in provider.ListFiles(folder).Order(StringComparer.Ordinal))
{
if (path.EndsWith('/'))
{
foreach (var child in Walk(provider, path.TrimEnd('/')))
yield return child;
}
else
yield return path;
}
}
}
}
+130
View File
@@ -0,0 +1,130 @@
namespace Mods.ModManager
{
using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.Theming;
using Godot;
/// <summary>Edits a detached copy; Apply replaces the collection draft, Cancel leaves it untouched.</summary>
public partial class CollectionSettingsDialog : ConfirmationDialog
{
private readonly List<CollectionSettingRow> rows = new();
private readonly List<HBoxContainer> extras = new();
private CollectionSettingsCatalog catalog;
private Action<Dictionary<string, string>> apply;
private VBoxContainer extraFields;
private Label error;
/// <summary>Builds a scrollable form from selected mods and existing keys, including undeclared third-party settings.</summary>
public void Initialize(CollectionSettingsCatalog source, Dictionary<string, string> settings, Action<Dictionary<string, string>> onApply)
{
catalog = source;
apply = onApply;
Name = "CollectionSettingsDialog";
Title = Tr("mod_manager/game_settings_title");
OkButtonText = TranslationServer.Translate("mod_manager/collection_json_apply");
CancelButtonText = TranslationServer.Translate("mod_manager/cancel");
DialogHideOnOk = false;
Exclusive = true;
// Only the native action bar stays outside the scroll. A large content minimum
// defeats PopupCenteredClamped: Godot grows the window back past the viewport.
Unresizable = false;
var scroll = new ScrollContainer { Name = "FieldsScroll", CustomMinimumSize = new Vector2(0, 80), SizeFlagsVertical = Control.SizeFlags.ExpandFill, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled, VerticalScrollMode = ScrollContainer.ScrollMode.Auto, FollowFocus = true };
var fields = UiKit.Column(UiTokens.Space4);
fields.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
scroll.AddChild(fields);
fields.AddChild(Wrapped(Tr("mod_manager/game_settings_hint")));
string group = null;
foreach (var definition in catalog.Definitions.Where(d => d.Available || settings?.ContainsKey(d.Key) == true).OrderBy(d => d.Group).ThenBy(d => d.Key))
{
if (group != definition.Group)
{
group = definition.Group;
fields.AddChild(Wrapped(catalog.Translate(group)));
}
var row = new CollectionSettingRow();
fields.AddChild(row);
row.Initialize(catalog, definition, settings != null && settings.TryGetValue(definition.Key, out var saved) ? saved : null);
rows.Add(row);
}
var advanced = UiKit.Button(Tr("mod_manager/game_settings_advanced"));
advanced.ToggleMode = true;
advanced.TooltipText = Tr("mod_manager/game_settings_advanced_hint");
fields.AddChild(advanced);
extraFields = UiKit.Column(UiTokens.Space2);
extraFields.Visible = false;
fields.AddChild(extraFields);
advanced.Toggled += visible => extraFields.Visible = visible;
var known = new HashSet<string>(rows.Select(r => r.Key), StringComparer.Ordinal);
foreach (var pair in settings ?? new Dictionary<string, string>())
if (!known.Contains(pair.Key))
AddExtra(pair.Key, pair.Value);
var add = UiKit.Button(Tr("mod_manager/game_settings_add"));
add.TooltipText = Tr("mod_manager/game_settings_advanced_hint");
add.Pressed += () => AddExtra("", "");
extraFields.AddChild(add);
error = Wrapped(catalog.Errors.Count == 0 ? "" : Tr("mod_manager/game_settings_metadata_error") + " " + string.Join(", ", catalog.Errors));
error.Modulate = UiTokens.Warning;
fields.AddChild(error);
AddChild(scroll);
Confirmed += Apply;
Canceled += QueueFree;
}
private static string Tr(string suffix) => TranslationServer.Translate(suffix);
private static Label Wrapped(string text) => new() { Text = text, AutowrapMode = TextServer.AutowrapMode.WordSmart, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
private void AddExtra(string key, string value)
{
var row = UiKit.Row(UiTokens.Space2);
var keyEdit = new LineEdit { Name = "Key", Text = key, PlaceholderText = Tr("mod_manager/game_settings_key"), TooltipText = Tr("mod_manager/game_settings_key"), SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
var valueEdit = new LineEdit { Name = "Value", Text = value ?? "", PlaceholderText = Tr("mod_manager/game_settings_value"), TooltipText = Tr("mod_manager/game_settings_value"), SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
var remove = UiKit.Button(Tr("mod_manager/game_settings_remove"));
remove.TooltipText = Tr("mod_manager/game_settings_remove_hint");
row.AddChild(keyEdit);
row.AddChild(valueEdit);
row.AddChild(remove);
remove.Pressed += () => RemoveExtra(row);
extraFields.AddChild(row);
extras.Add(row);
}
private void RemoveExtra(HBoxContainer row)
{
extras.Remove(row);
row.QueueFree();
}
private void Apply()
{
var invalid = rows.FirstOrDefault(r => !r.Valid);
if (invalid != null)
{
error.Text = invalid.Key + ": " + Tr("mod_manager/game_settings_invalid");
return;
}
var result = new Dictionary<string, string>(StringComparer.Ordinal);
var reserved = new HashSet<string>(rows.Select(r => r.Key), StringComparer.Ordinal);
foreach (var row in rows)
if (row.Value != null)
result[row.Key] = row.Value;
foreach (var row in extras)
{
var key = row.GetNode<LineEdit>("Key").Text.Trim();
var value = row.GetNode<LineEdit>("Value").Text;
if (key.Length == 0 && value.Length == 0)
continue;
if (key.Length == 0 || reserved.Contains(key) || !result.TryAdd(key, value))
{
error.Text = Tr("mod_manager/game_settings_duplicate");
return;
}
}
apply(result);
QueueFree();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace Mods.ModManager
{
using System.Collections.Generic;
/// <summary>collection_settings.json: owners declare fields, implementation mods contribute choices.</summary>
public class CollectionSettingsFile
{
public List<CollectionSettingDefinition> Settings { get; set; } = new();
public List<CollectionSettingOption> Options { get; set; } = new();
}
}
+6 -4
View File
@@ -9,10 +9,9 @@ namespace Mods.ModManager
/// live global rectangles, so dragging a chip or a whole group keeps the
/// lines attached without recomputing them.
/// <para/>
/// <see cref="Cross"/> is the "dangerous layout" flag: the dependency's two
/// mods live in different mechanics, which means the player separated what
/// belongs together. Cross edges are painted amber (red when the selection
/// touches them) so the mistake is visible across the whole canvas.
/// A dependency may cross group boundaries normally: groups describe its
/// purpose in one game, not compatibility. Only <see cref="Problem"/> carries
/// an actual availability/version failure and receives warning styling.
/// </summary>
public class CollectionEdge
{
@@ -25,6 +24,9 @@ namespace Mods.ModManager
/// <summary>True when the two mods belong to different groups.</summary>
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>
public bool Active { get; set; }
}
+23 -6
View File
@@ -60,7 +60,7 @@ namespace Mods.ModManager
if (_canvas == null || !GodotObject.IsInstanceValid(_canvas))
return;
var origin = _canvas.GetGlobalRect().Position;
var inverse = _canvas.GetGlobalTransform().AffineInverse();
foreach (var edge in _edges)
{
@@ -69,13 +69,16 @@ namespace Mods.ModManager
if (!_chips.TryGetValue(edge.ToModId, out var to) || !GodotObject.IsInstanceValid(to))
continue;
var a = from.GetGlobalRect().GetCenter() - origin;
var b = to.GetGlobalRect().GetCenter() - origin;
var fromRect = from.GetGlobalRect();
var toRect = to.GetGlobalRect();
var a = inverse * BorderPoint(fromRect, toRect.GetCenter());
var b = inverse * BorderPoint(toRect, fromRect.GetCenter());
var points = BuildCurve(a, b);
var color = edge.Cross ? (edge.Active ? UiTokens.Danger : UiTokens.Warning) : (edge.Active ? UiTokens.Accent : UiTokens.LineStrong);
var width = edge.Cross ? (edge.Active ? 3f : 2f) : (edge.Active ? 2.5f : 1.5f);
var color = edge.Problem ? UiTokens.DangerText
: 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++)
DrawDashedLine(points[i], points[i + 1], color, width, 5f);
@@ -85,9 +88,23 @@ namespace Mods.ModManager
{
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)
{
var dx = b.X - a.X;
+2 -2
View File
@@ -158,7 +158,7 @@ namespace Mods.ModManager
MouseFilter = MouseFilterEnum.Ignore;
_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.AddThemeStyleboxOverride("panel", _style);
_panel.GuiInput += OnPanelGuiInput;
@@ -217,7 +217,7 @@ namespace Mods.ModManager
_desc = UiKit.Label(string.Empty, UiTokens.FontSmall, UiTokens.Muted);
_desc.AutowrapMode = TextServer.AutowrapMode.WordSmart;
_desc.MaxLinesVisible = 3;
_desc.MaxLinesVisible = 1;
_content.AddChild(_desc);
_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>
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>
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>
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>
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
/// status without rebuilding the node.
/// </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();
ModId = modId;
GroupKey = groupKey;
_stateColor = stateColor;
_role.Text = string.IsNullOrEmpty(role) ? string.Empty : role.ToUpperInvariant();
_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;
_style.BorderColor = stateColor;
_style.BorderColor = stateColor == UiTokens.Danger || stateColor == UiTokens.DangerText || stateColor == UiTokens.Warning
? stateColor : UiTokens.LineCard;
_panel.TooltipText = tooltip ?? string.Empty;
}
@@ -75,7 +81,10 @@ namespace Mods.ModManager
public void SetSelected(bool selected)
{
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()
@@ -136,7 +145,7 @@ namespace Mods.ModManager
{
_dragging = true;
_moved = false;
Selected?.Invoke(ModId);
Selected?.Invoke(this);
}
else
{
@@ -168,7 +177,7 @@ namespace Mods.ModManager
_moved = true;
Position = clamped;
Moved?.Invoke(ModId, clamped);
Moved?.Invoke(this, clamped);
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>
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>
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, CollectionModChip> _chips = 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 Vector2 _panStartMouse;
@@ -94,7 +207,7 @@ namespace Mods.ModManager
_layout.Groups ??= new Dictionary<string, ModCollectionLayoutRect>();
_layout.Mods ??= new Dictionary<string, ModCollectionLayoutPoint>();
foreach (var child in GetChildren())
foreach (var child in _content.GetChildren())
{
if (child != _edges)
child.QueueFree();
@@ -103,6 +216,8 @@ namespace Mods.ModManager
_cards.Clear();
_chips.Clear();
_chipGroups.Clear();
_instances.Clear();
_visibleFrames.Clear();
if (graph != null && graph.Groups.Count > 0)
{
@@ -110,11 +225,12 @@ namespace Mods.ModManager
BuildCards();
}
_edges.SetEdges(null, _chips, this);
MoveChild(_edges, GetChildCount() - 1);
_edges.SetEdges(null, _chips, _content);
_content.MoveChild(_edges, _content.GetChildCount() - 1);
_edges.Visible = ShowEdges;
UpdateCanvasSize();
RebuildEdges();
SetSearch(_search);
}
/// <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)
pair.Value.SetSelected(string.Equals(pair.Key, groupKey, StringComparison.OrdinalIgnoreCase));
foreach (var pair in _chips)
pair.Value.SetSelected(string.Equals(pair.Key, modId, StringComparison.OrdinalIgnoreCase));
foreach (var pair in _chipGroups)
{
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();
}
@@ -145,15 +273,42 @@ namespace Mods.ModManager
/// <summary>Finds the group key owning a rendered mod chip, for inspector navigation.</summary>
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()
{
MouseFilter = MouseFilterEnum.Stop;
GuiInput += OnCanvasGuiInput;
_content = new Control { MouseFilter = MouseFilterEnum.Ignore };
AddChild(_content);
_content.Connect("draw", Callable.From((Action)DrawGrid));
_edges = new CollectionEdgeLayer();
AddChild(_edges);
_content.AddChild(_edges);
_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()
@@ -162,7 +317,7 @@ namespace Mods.ModManager
{
var rect = _layout.Groups[group.Key];
var card = new CollectionGroupCard();
AddChild(card);
_content.AddChild(card);
var summary = Summarize(group);
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.DragFinished += OnDragFinished;
_cards[group.Key] = card;
_visibleFrames[group.Key] = rect;
foreach (var mod in group.Mods)
{
@@ -183,11 +339,14 @@ namespace Mods.ModManager
card.ModsLayer.AddChild(chip);
var node = FindNode(mod.ModId);
var requirement = ModDependency.Parse(mod.Requirement);
chip.Init(
group.Key,
mod.ModId,
mod.Role,
node?.Title ?? mod.ModId,
VersionOf(node),
requirement.IsValid && requirement.VersionRaw != null ? requirement.Operator + requirement.VersionRaw : null,
StateLabel(node),
StateColor(node),
BuildModTooltip(mod, node));
@@ -195,6 +354,7 @@ namespace Mods.ModManager
chip.Selected += OnChipSelected;
chip.Moved += OnChipMoved;
chip.DragFinished += OnDragFinished;
_instances[key] = chip;
if (!_chips.ContainsKey(mod.ModId))
{
@@ -396,11 +556,11 @@ namespace Mods.ModManager
GroupSelected?.Invoke(groupKey);
}
private void OnChipSelected(string modId)
private void OnChipSelected(CollectionModChip chip)
{
SelectedModId = modId;
SelectedGroupKey = null;
ModSelected?.Invoke(modId);
SelectedModId = chip.ModId;
SelectedGroupKey = chip.GroupKey;
ModSelected?.Invoke(chip.ModId);
}
private void OnCardCommand(string groupKey, string command)
@@ -427,12 +587,9 @@ namespace Mods.ModManager
RebuildEdges();
}
private void OnChipMoved(string modId, Vector2 position)
private void OnChipMoved(CollectionModChip chip, Vector2 position)
{
if (!_chipGroups.TryGetValue(modId, out var groupKey))
return;
var key = groupKey + "|" + modId;
var key = chip.GroupKey + "|" + chip.ModId;
if (!_layout.Mods.TryGetValue(key, out var point) || point == null)
{
point = new ModCollectionLayoutPoint();
@@ -472,11 +629,15 @@ namespace Mods.ModManager
var size = new Vector2(
Mathf.Max(maxX + GapX + 160f, MinCanvasWidth),
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.
// _edges carries FullRect anchors (CollectionEdgeLayer.Construct),
// so it follows this resize automatically; assigning its Size
// 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()
@@ -491,18 +652,60 @@ namespace Mods.ModManager
var fromGroup = GetGroupKeyOfMod(edge.FromModId);
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
{
FromModId = edge.FromModId,
ToModId = edge.ToModId,
Cross = !string.Equals(fromGroup, toGroup, StringComparison.OrdinalIgnoreCase),
Active = string.Equals(edge.FromModId, SelectedModId, StringComparison.OrdinalIgnoreCase)
|| string.Equals(edge.ToModId, SelectedModId, StringComparison.OrdinalIgnoreCase),
Active = active,
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>
@@ -516,6 +719,17 @@ namespace Mods.ModManager
/// </summary>
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 (button.Pressed)
+4
View File
@@ -151,6 +151,7 @@ namespace Mods.ModManager
row.AddChild(UiKit.Spacer());
var newGame = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_new_game"), ButtonKind.Primary);
ModHelpPanel.Attach(newGame, "collection_new");
newGame.Pressed += () => NewGameRequested?.Invoke();
row.AddChild(newGame);
_host.AddChild(row);
@@ -401,6 +402,7 @@ namespace Mods.ModManager
button.Disabled = true;
}
ModHelpPanel.Attach(button, "collection_apply");
if (action != null)
button.Pressed += action;
return button;
@@ -409,6 +411,7 @@ namespace Mods.ModManager
private Button BuildEditButton(ModCollectionGameSummary summary)
{
var button = UiKit.Button(TranslationServer.Translate("mod_manager/gallery_edit_scheme"));
ModHelpPanel.Attach(button, "collection_edit");
button.Disabled = _installingId != null;
button.Pressed += () => EditSchemeRequested?.Invoke(summary.Collection.Id);
return button;
@@ -417,6 +420,7 @@ namespace Mods.ModManager
private Button BuildDeleteButton(ModCollectionGameSummary summary)
{
var button = UiKit.Button(TranslationServer.Translate("mod_manager/collection_delete"), ButtonKind.Danger);
ModHelpPanel.Attach(button, "collection_delete");
button.Disabled = _installingId != null;
button.Pressed += () => CollectionDeletion.Confirm(this, summary.Collection, () =>
{
+29 -8
View File
@@ -21,6 +21,7 @@ namespace Mods.ModManager
public partial class ModCard : PanelContainer
{
private ModCardData _data;
private Texture2D _ownedPreview;
private TextureRect _image;
private Label _title;
private Label _author;
@@ -72,14 +73,22 @@ namespace Mods.ModManager
_author.AddThemeColorOverride("font_color", UiTokens.Muted);
_source.AddThemeColorOverride("font_color", UiTokens.Muted);
// The source tooltip needs hover, while clicks on it must still open the card.
_source.MouseFilter = Control.MouseFilterEnum.Pass;
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;
_action.Pressed += OnActionPressed;
_enabled.Toggled += OnEnabledPressed;
GuiInput += OnGuiInput;
BuildCollectionsLink();
ModHelpPanel.Attach(_collectionsLink, "mod_collections");
PopulateVersions();
RefreshState();
LoadPreview();
@@ -136,6 +145,7 @@ namespace Mods.ModManager
{
_versions.AddItem("v" + (_data.LocalVersion ?? "—"));
_versions.Disabled = true;
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
return;
}
@@ -161,9 +171,9 @@ namespace Mods.ModManager
}
_versions.Selected = selectedIndex;
// "v1.2.0 · latest · installed" does not fit the narrow combo: it ends in an
// ellipsis (clip_text in the scene), the full label is in the tooltip.
_versions.TooltipText = _versions.GetItemText(selectedIndex);
// The combo clips its long version label, so its tooltip explains
// the version markers instead of repeating the truncated caption.
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
}
/// <summary>
@@ -197,7 +207,6 @@ namespace Mods.ModManager
var text = ModCollectionLinks.BadgeText(_data.CollectionsCount);
_collectionsLink.Visible = text.Length > 0;
_collectionsLink.Text = text;
_collectionsLink.TooltipText = text;
}
private void UpdateActionButton()
@@ -228,18 +237,19 @@ namespace Mods.ModManager
_enabled.ButtonPressed = !_data.IsDisabled;
_updatingEnabled = false;
_enabled.TooltipText = TranslationServer.Translate(_data.IsDisabled
? "mod_manager/action_enable"
: "mod_manager/action_disable");
? "mod_manager/enable_tooltip"
: "mod_manager/disable_tooltip");
}
private void LoadPreview()
{
ReleasePreview();
if (_data.LocalInfo != null)
{
var local = ModPreviewLoader.LoadLocal(_data.LocalInfo);
if (local != null)
{
_image.Texture = local;
_image.Texture = _ownedPreview = local;
return;
}
}
@@ -248,6 +258,17 @@ namespace Mods.ModManager
ModPreviewLoader.LoadRemote(_data.Id, _data.Catalog.IconUrl, OnRemotePreviewLoaded);
}
/// <summary>Local previews are decoded for this view; remote previews belong to the shared icon cache.</summary>
public override void _ExitTree() => ReleasePreview();
private void ReleasePreview()
{
if (GodotObject.IsInstanceValid(_image))
_image.Texture = null;
_ownedPreview?.Dispose();
_ownedPreview = null;
}
private void OnRemotePreviewLoaded(Texture2D texture)
{
if (texture != null && GodotObject.IsInstanceValid(_image))
@@ -287,7 +308,7 @@ namespace Mods.ModManager
return;
_data.SelectedDownload = _data.Downloads[(int)index];
_versions.TooltipText = _versions.GetItemText((int)index);
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
_data.RecomputeStatus();
RefreshState();
VersionChanged?.Invoke(_data);
+84 -2
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Cthangover.Core.UI.Theming;
using Godot;
@@ -18,6 +19,7 @@ namespace Mods.ModManager
public partial class ModDetailView : VBoxContainer
{
private ModCardData _data;
private Texture2D _ownedPreview;
private bool _nodesReady;
private TextureRect _coverImage;
@@ -38,6 +40,12 @@ namespace Mods.ModManager
private Label _usedByText;
private VBoxContainer _conflictsBox;
private Label _conflictsText;
private VBoxContainer _documentationBox;
private OptionButton _documentationLanguages;
private Label _documentationSource;
private ModMarkdownView _documentationBody;
private IReadOnlyList<ModDocument> _documents = Array.Empty<ModDocument>();
private string _documentationLanguage;
/// <summary>Raised when the primary contextual action is pressed.</summary>
public event Action<ModCardData> ActionPressed;
@@ -57,10 +65,19 @@ namespace Mods.ModManager
/// <summary>Displays the given mod; call again with refreshed data after actions.</summary>
public void Open(ModCardData data)
{
if (_data?.MatchKey != data?.MatchKey)
_documentationLanguage = null;
_data = data;
_documents = ModDocumentation.Discover(data?.LocalInfo?.FileProvider);
Rebuild();
}
public override void _Notification(int what)
{
if (what == NotificationTranslationChanged && _nodesReady)
BuildDocumentation();
}
private void EnsureNodes()
{
if (_nodesReady)
@@ -85,6 +102,16 @@ namespace Mods.ModManager
_usedByText = GetNode<Label>("Scroll/Content/UsedByBox/Text");
_conflictsBox = GetNode<VBoxContainer>("Scroll/Content/ConflictsBox");
_conflictsText = GetNode<Label>("Scroll/Content/ConflictsBox/Text");
_documentationBox = GetNode<VBoxContainer>("Scroll/Content/DocumentationBox");
_documentationLanguages = GetNode<OptionButton>("Scroll/Content/DocumentationBox/Header/Languages");
_documentationSource = GetNode<Label>("Scroll/Content/DocumentationBox/Source");
_documentationBody = GetNode<ModMarkdownView>("Scroll/Content/DocumentationBox/Body");
_documentationLanguages.ItemSelected += OnDocumentationLanguageSelected;
_documentationLanguages.TooltipText = TranslationServer.Translate("mod_manager/hint_docs_language_body");
ModHelpPanel.Attach(_documentationLanguages, "docs_language");
_documentationSource.AddThemeColorOverride("font_color", UiTokens.Muted);
ModHelpPanel.Attach(_versions, "mod_version");
_versions.ItemSelected += OnVersionSelected;
@@ -141,6 +168,7 @@ namespace Mods.ModManager
BuildConflictBox();
_descriptionText.Text = NullIfEmpty(_data.Description) ?? "—";
BuildDocumentation();
_changelogText.Text = NullIfEmpty(_data.SelectedDownload?.Changelog) ?? "—";
SetOptional(_dependsBox, _dependsText, JoinOrEmpty(ModQueries.GetDependencies(_data)));
SetOptional(_usedByBox, _usedByText, JoinOrEmpty(ModQueries.GetDependents(_data.Id)));
@@ -149,6 +177,44 @@ namespace Mods.ModManager
LoadCover();
}
private void BuildDocumentation()
{
_documentationBox.Visible = _documents.Count > 0;
_documentationBody.Display(null, null);
if (_documents.Count == 0 || _data == null)
return;
SetCaption("Scroll/Content/DocumentationBox/Header/Caption", "mod_manager/documentation");
_documentationLanguages.TooltipText = TranslationServer.Translate("mod_manager/hint_docs_language_body");
_documentationLanguages.Clear();
_documentationLanguages.AddItem(TranslationServer.Translate("mod_manager/docs_auto"));
var selected = 0;
for (var i = 0; i < _documents.Count; i++)
{
var language = _documents[i].Language;
_documentationLanguages.AddItem(language == "default"
? TranslationServer.Translate("mod_manager/docs_default").ToString() : language.ToUpperInvariant());
if (language == _documentationLanguage)
selected = i + 1;
}
_documentationLanguages.Selected = selected;
var requested = selected == 0 ? TranslationServer.GetLocale() : _documentationLanguage;
var document = ModDocumentation.Select(_documents, requested);
_documentationSource.Text = string.Format(TranslationServer.Translate("mod_manager/docs_source"),
_data.LocalVersion ?? "—", document.Language.ToUpperInvariant());
if (!string.IsNullOrEmpty(_data.SelectedDownload?.Version) && _data.SelectedDownload.Version != _data.LocalVersion)
_documentationSource.Text += "\n" + TranslationServer.Translate("mod_manager/docs_installed_notice");
_documentationBody.Display(_data.LocalInfo?.FileProvider, document);
}
private void OnDocumentationLanguageSelected(long index)
{
if (index < 0 || index > _documents.Count)
return;
_documentationLanguage = index == 0 ? null : _documents[(int)index - 1].Language;
BuildDocumentation();
}
private void PopulateVersions()
{
_versions.Clear();
@@ -177,6 +243,7 @@ namespace Mods.ModManager
}
_versions.Selected = selectedIndex;
_versions.TooltipText = TranslationServer.Translate("mod_manager/hint_mod_version_body");
}
private void BuildActions()
@@ -195,6 +262,7 @@ namespace Mods.ModManager
CthangoverTheme.ApplyDanger(button);
else
CthangoverTheme.ApplyPrimary(button);
ModHelpPanel.Attach(button, "mod_action");
button.Pressed += OnPrimaryPressed;
_actionsRow.AddChild(button);
}
@@ -203,6 +271,7 @@ namespace Mods.ModManager
{
var delete = new Button { Text = ModActionNames.Translate(ModActionKind.Delete) };
CthangoverTheme.ApplyDanger(delete);
ModHelpPanel.Attach(delete, "mod_delete");
delete.Pressed += OnDeletePressed;
_actionsRow.AddChild(delete);
}
@@ -219,6 +288,7 @@ namespace Mods.ModManager
CthangoverTheme.ApplyPrimary(toggle);
else
CthangoverTheme.ApplyDanger(toggle);
ModHelpPanel.Attach(toggle, "mod_enabled");
toggle.Pressed += OnTogglePressed;
_actionsRow.AddChild(toggle);
}
@@ -241,12 +311,12 @@ namespace Mods.ModManager
var link = new Button
{
Text = collectionsText,
TooltipText = collectionsText,
FocusMode = FocusModeEnum.None,
};
link.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
link.AddThemeColorOverride("font_color", UiTokens.TypeTarget);
link.AddThemeColorOverride("font_hover_color", UiTokens.TypeTarget);
ModHelpPanel.Attach(link, "mod_collections");
link.Pressed += () => CollectionsLinkRequested?.Invoke();
_actionsRow.AddChild(link);
}
@@ -269,6 +339,7 @@ namespace Mods.ModManager
private void LoadCover()
{
ReleasePreview();
_coverImage.Texture = null;
if (_data.LocalInfo != null)
@@ -276,7 +347,7 @@ namespace Mods.ModManager
var local = ModPreviewLoader.LoadLocal(_data.LocalInfo);
if (local != null)
{
_coverImage.Texture = local;
_coverImage.Texture = _ownedPreview = local;
return;
}
}
@@ -285,6 +356,17 @@ namespace Mods.ModManager
ModPreviewLoader.LoadRemote(_data.Id, _data.Catalog.IconUrl, OnRemotePreviewLoaded);
}
/// <summary>Local previews are decoded for this view; remote previews belong to the shared icon cache.</summary>
public override void _ExitTree() => ReleasePreview();
private void ReleasePreview()
{
if (GodotObject.IsInstanceValid(_coverImage))
_coverImage.Texture = null;
_ownedPreview?.Dispose();
_ownedPreview = null;
}
private void OnRemotePreviewLoaded(Texture2D texture)
{
if (texture != null && GodotObject.IsInstanceValid(_coverImage))
+3
View File
@@ -52,10 +52,13 @@ namespace Mods.ModManager
_title.Text = TranslationServer.Translate("mod_manager/filters");
_reset.Text = TranslationServer.Translate("mod_manager/reset_filters");
_reset.TooltipText = TranslationServer.Translate("mod_manager/filters_reset_tooltip");
_statusCaption.Text = TranslationServer.Translate("mod_manager/filter_status");
_categoryCaption.Text = TranslationServer.Translate("mod_manager/filter_categories");
_groupToggle.Text = TranslationServer.Translate("mod_manager/group_by_category");
_groupToggle.ButtonPressed = _state.GroupByCategory;
ModHelpPanel.Attach(_reset, "filters_reset");
ModHelpPanel.Attach(_groupToggle, "group_categories");
_statusCaption.AddThemeColorOverride("font_color", UiTokens.Muted);
_categoryCaption.AddThemeColorOverride("font_color", UiTokens.Muted);
+230
View File
@@ -0,0 +1,230 @@
namespace Mods.ModManager
{
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.Theming;
using Godot;
/// <summary>
/// Non-modal, contextual help for the mods hub. Controls carry a locale suffix in
/// <see cref="HintKeyMetadata"/>; the layer finds the annotated ancestor under the
/// pointer or keyboard focus, outlines it and places a compact card next to it.
/// The root ignores mouse input, so the inspected control still performs its normal
/// action. A click pins the current card without swallowing that click; Escape or
/// the header button leaves help mode through <see cref="ModHubScreen"/>.
/// </summary>
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 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()
{
MouseFilter = MouseFilterEnum.Ignore;
_highlight = GetNode<Panel>("Highlight");
_card = GetNode<PanelContainer>("Card");
_title = GetNode<Label>("Card/Margin/Column/Title");
_body = GetNode<RichTextLabel>("Card/Margin/Column/Body");
_footer = GetNode<Label>("Footer/Margin/FooterText");
_highlight.AddThemeStyleboxOverride("panel", UiKit.Frame(
UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.Accent, 0.025f), UiTokens.AccentLineStrong));
_card.AddThemeStyleboxOverride("panel", UiKit.Frame(
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);
_footer.Text = TranslationServer.Translate("mod_manager/help_active");
_highlight.Visible = false;
_card.Visible = false;
}
/// <summary>Enables inspection without choosing a page or blocking the hub.</summary>
public void Open()
{
Visible = true;
IsPinned = false;
CurrentTarget = null;
_highlight.Visible = false;
_card.Visible = false;
UpdateHoveredTarget();
}
/// <summary>Leaves help mode and removes every help-only visual.</summary>
public void Close()
{
Visible = false;
IsPinned = false;
CurrentTarget = null;
if (GodotObject.IsInstanceValid(_highlight))
_highlight.Visible = false;
if (GodotObject.IsInstanceValid(_card))
_card.Visible = false;
}
/// <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)
{
Control annotated = FindTarget(target);
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));
}
}
}
+274
View File
@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using Cthangover.Core.Mods;
using Cthangover.Core.UI.Theming;
using Godot;
using Markdig;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Mods.ModManager
{
/// <summary>
/// CommonMark rendered through Godot's rich-text API, never as HTML or
/// interpolated BBCode. Images come from the same provider as the document;
/// percentage widths keep them inside the page when the window is resized.
/// </summary>
public partial class ModMarkdownView : RichTextLabel
{
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().DisableHtml().Build();
private readonly Dictionary<string, ImageTexture> textures = new(StringComparer.Ordinal);
private IModFileProvider provider;
private ModDocument document;
private bool connected;
private FontFile codeFont;
/// <summary>Replaces the document and releases images owned by the previous page.</summary>
public void Display(IModFileProvider source, ModDocument value)
{
Clear();
ReleaseTextures();
provider = source;
document = value;
if (!connected)
{
MetaClicked += OnLinkClicked;
connected = true;
AddThemeColorOverride("default_color", UiTokens.Body);
AddThemeFontSizeOverride("normal_font_size", UiTokens.FontBody);
}
if (value != null)
{
EnsureCodeFont();
RenderBlocks(Markdown.Parse(value.Text, Pipeline), 0);
}
}
public override void _ExitTree()
{
Clear();
ReleaseTextures();
provider = null;
document = null;
ReleaseCodeFont();
}
public override void _Notification(int what)
{
// Detached preview/test controls may be freed without entering a tree.
if (what == NotificationPredelete)
ReleaseCodeFont();
}
private void EnsureCodeFont()
{
if (codeFont != null || !ModRegistry.Instance.AllDiscoveredMods.TryGetValue("mod_manager", out var owner))
return;
var bytes = owner.FileProvider?.ReadFileBinary("fonts/DejaVuSansMono.ttf");
if (bytes == null)
return;
// Read bytes through the provider so the font also works in ZIP mods,
// without Godot import files or a system-installed monospace font.
codeFont = new FontFile { Data = bytes };
AddThemeFontOverride("mono_font", codeFont);
}
private void ReleaseCodeFont()
{
if (codeFont == null)
return;
RemoveThemeFontOverride("mono_font");
codeFont.Dispose();
codeFont = null;
}
private void ReleaseTextures()
{
foreach (var texture in textures.Values)
texture.Dispose();
textures.Clear();
}
private void RenderBlocks(ContainerBlock blocks, int depth)
{
if (depth > 32)
return;
foreach (var block in blocks)
{
switch (block)
{
case HeadingBlock heading:
PushFontSize(Math.Max(UiTokens.FontBody + 2, 30 - heading.Level * 2));
PushBold();
RenderInlines(heading.Inline, depth + 1);
Pop();
Pop();
break;
case ParagraphBlock paragraph:
RenderInlines(paragraph.Inline, depth + 1);
break;
case CodeBlock code:
PushMono();
PushBgcolor(UiTokens.Surface0);
foreach (var span in ModCodeSyntax.Tokenize(code is FencedCodeBlock fence ? fence.Info : null, code.Lines.ToString()))
{
if (span.Kind.Length > 0)
PushColor(CodeColor(span.Kind));
AddText(span.Text);
if (span.Kind.Length > 0)
Pop();
}
Pop();
Pop();
break;
case ListBlock list:
var number = int.TryParse(list.OrderedStart, out var start) ? start : 1;
foreach (var item in list)
{
PushIndent(depth + 1);
AddText(list.IsOrdered ? $"{number++}. " : "• ");
RenderBlocks((ContainerBlock)item, depth + 1);
Pop();
}
break;
case QuoteBlock quote:
PushIndent(1);
PushColor(UiTokens.Muted);
RenderBlocks(quote, depth + 1);
Pop();
Pop();
break;
case ThematicBreakBlock:
AddText("────────────────────────");
break;
case ContainerBlock container:
RenderBlocks(container, depth + 1);
break;
}
AddText("\n\n");
}
}
private void RenderInlines(ContainerInline container, int depth)
{
if (container == null || depth > 32)
return;
foreach (var inline in container)
{
switch (inline)
{
case LiteralInline literal:
AddText(literal.Content.ToString());
break;
case CodeInline code:
PushMono();
AddText(code.Content);
Pop();
break;
case LineBreakInline line:
AddText(line.IsHard ? "\n" : " ");
break;
case EmphasisInline emphasis:
if (emphasis.DelimiterCount >= 2)
PushBold();
else
PushItalics();
RenderInlines(emphasis, depth + 1);
Pop();
break;
case LinkInline image when image.IsImage:
RenderImage(image);
break;
case LinkInline link:
var allowed = IsWebLink(link.Url);
if (allowed)
{
PushColor(UiTokens.TypeTarget);
PushMeta(link.Url);
}
RenderInlines(link, depth + 1);
if (allowed)
{
Pop();
Pop();
}
break;
case AutolinkInline autoLink:
if (IsWebLink(autoLink.Url))
{
PushMeta(autoLink.Url);
AddText(autoLink.Url);
Pop();
}
else
AddText(autoLink.Url);
break;
case ContainerInline nested:
RenderInlines(nested, depth + 1);
break;
}
}
}
private void RenderImage(LinkInline image)
{
var alt = image.FirstChild is LiteralInline literal ? literal.Content.ToString() : image.Title ?? "";
var path = ModDocumentation.ResolveResource(document.Path, image.Url);
var texture = path == null ? null : LoadImage(path);
if (texture == null)
{
AddText($"[{alt}] ({TranslationServer.Translate("mod_manager/docs_image_unavailable")})");
return;
}
AddImage(texture, width: 100, height: 0, widthInPercent: true, altText: alt, tooltip: image.Title ?? alt);
}
private ImageTexture LoadImage(string path)
{
if (textures.TryGetValue(path, out var cached))
return cached;
// Do not allow a document to retain an unbounded number of textures.
if (textures.Count >= 16)
return null;
var extension = System.IO.Path.GetExtension(path).ToLowerInvariant();
if (extension != ".png" && extension != ".jpg" && extension != ".jpeg" && extension != ".webp")
return null;
var bytes = ModDocumentation.ReadResource(provider, path, 8 * 1024 * 1024);
if (bytes == null || bytes.Length == 0)
return null;
using var image = new Image();
var error = extension == ".png" ? image.LoadPngFromBuffer(bytes)
: extension == ".webp" ? image.LoadWebpFromBuffer(bytes) : image.LoadJpgFromBuffer(bytes);
if (error != Error.Ok || image.GetWidth() > 4096 || image.GetHeight() > 4096)
return null;
var texture = ImageTexture.CreateFromImage(image);
textures[path] = texture;
return texture;
}
private static bool IsWebLink(string value)
=> Uri.TryCreate(value, UriKind.Absolute, out var uri) && (uri.Scheme == "https" || uri.Scheme == "http");
// Same semantic palette as site/css/highlight-theme.css, independent of
// the application's theme so examples remain recognisable on both UIs.
private static Color CodeColor(string kind) => kind switch
{
"keyword" => new Color("ff7b72"),
"function" or "type" => new Color("d2a8ff"),
"string" => new Color("a5d6ff"),
"comment" => new Color("8b949e"),
"built_in" => new Color("ffa657"),
"label" => new Color("1f6feb"),
"directory" => new Color("e3b341"),
"treecomment" => new Color("3fb950"),
_ => new Color("79c0ff"),
};
private void OnLinkClicked(Variant value)
{
var url = value.AsString();
if (IsWebLink(url))
OS.ShellOpen(url);
}
}
}
@@ -0,0 +1,4 @@
{
"name": "Collection settings UI test",
"sources": ["src/*.cs"]
}
@@ -0,0 +1,202 @@
namespace Mods.ModManager.SettingsTest
{
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using Cthangover.Core.Bootstrap;
using Cthangover.Core.Localization;
using Cthangover.Core.Mods;
using Cthangover.Core.UI.Menu;
using Cthangover.Core.Utils;
using Cthangover.Mods.Interface.Menu;
using Godot;
/// <summary>Small-viewport scrolling, cancel, validation, automatic removal and persisted settings through real UI controls.</summary>
public partial class CollectionSettingsTestDriver : Node
{
private const string FixtureId = "collection_settings_ui_test";
private ModHubScreen hub;
private ModCollectionsScreen editor;
private CollectionSettingsDialog dialog;
private string fixturePath;
private string originalLocale;
private int step;
private int passed;
private double elapsed;
private double next;
private bool done;
public override void _Process(double delta)
{
if (done)
return;
elapsed += delta;
if (elapsed > 90)
{
Finish("timeout at step " + step);
return;
}
if (elapsed < next)
return;
next = elapsed + 0.35;
try { RunStep(); }
catch (Exception ex) { Finish("step " + step + ": " + ex); }
}
private void RunStep()
{
switch (step)
{
case 0:
if (!Boot.IsReady)
return;
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcher)
{
launcher.EmitSignal(BaseButton.SignalName.Pressed);
return;
}
if (GetTree().CurrentScene?.FindChild("play", true, false) is not Button)
return;
originalLocale = TranslationServer.GetLocale();
LocaleLoader.LoadLocale("ru");
MakeFixture();
var entry = MenuRegistry.GetButtonById("mods_hub");
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry.ScenePath, "mods_hub") as ModHubScreen;
Check(hub != null, "hub opens");
hub.OpenCollectionEditor(FixtureId);
editor = hub.FindChildren("*", "", true, false).OfType<ModCollectionsScreen>().Single();
break;
case 1:
OpenForm();
break;
case 2:
Check(dialog.FindChildren("*", "", true, false).OfType<CollectionSettingRow>().Count() == 5, "five fields read without loading game mods");
Check(dialog.Title == "Параметры игры", "localized dialog title");
Check(Row("battle.core").Value == "missing_core", "unavailable saved choice retained");
dialog.Size = new Vector2I(640, 420);
break;
case 3:
var scroll = dialog.GetNode<ScrollContainer>("FieldsScroll");
Check(dialog.Size.Y <= 420, "dialog minimum does not exceed small viewport");
Check(scroll.GetChild<Control>(0).Size.Y > scroll.Size.Y, "long form is contained in scroll viewport");
Check(dialog.GetOkButton().GetGlobalRect().End.Y <= dialog.Size.Y, "Apply remains inside window");
Check(dialog.GetCancelButton().GetGlobalRect().End.Y <= dialog.Size.Y, "Cancel remains inside window");
scroll.ScrollVertical = 100000;
break;
case 4:
Check(dialog.GetNode<ScrollContainer>("FieldsScroll").ScrollVertical > 0, "form scrolls to lower fields");
Custom(Row("checks.dice"), "bad dice");
dialog.EmitSignal(ConfirmationDialog.SignalName.Confirmed);
Check(!dialog.IsQueuedForDeletion(), "invalid dice prevents Apply");
Custom(Row("checks.dice"), "2d6");
Custom(Row("stats.player"), "created_during_play");
Select(Row("tile.renderer"), 0);
dialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
break;
case 5:
OpenForm();
Check(Row("checks.dice").Value == "d20", "Cancel leaves original settings");
Custom(Row("checks.dice"), "2d6");
Custom(Row("stats.player"), "created_during_play");
Select(Row("tile.renderer"), 0);
dialog.EmitSignal(ConfirmationDialog.SignalName.Confirmed);
break;
case 6:
editor.GetNode<Button>("Margin/Root/Header/Save").EmitSignal(BaseButton.SignalName.Pressed);
break;
case 7:
var save = editor.FindChildren("*", "", true, false).OfType<AcceptDialog>().Single(d => d.Visible);
save.EmitSignal(AcceptDialog.SignalName.Confirmed);
break;
case 8:
var saved = ModCollectionStore.LoadUser().Single(c => c.Id == FixtureId);
Check(saved.Settings["checks.dice"] == "2d6", "dice saved to collection package");
Check(saved.Settings["stats.player"] == "created_during_play", "custom character saved");
Check(saved.Settings["battle.core"] == "missing_core", "unavailable choice survives save");
Check(saved.Settings["custom.fixture"] == "keep", "undeclared setting survives save");
Check(!saved.Settings.ContainsKey("tile.renderer"), "Automatic deletes explicit key");
editor.OpenCollection(FixtureId);
OpenForm();
break;
case 9:
Check(Row("checks.dice").Value == "2d6", "saved values restored on reopening");
Check(Row("tile.renderer").Value == null, "automatic restored on reopening");
dialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
Finish(null);
return;
}
step++;
}
private void MakeFixture()
{
var path = Path.Combine(ModCollectionStore.UserPath, FixtureId);
Check(!Directory.Exists(path), "fixture cannot overwrite player data");
var source = ModCollectionStore.LoadBuiltIn().Single(c => c.Id == "rpg_plus");
var clone = JsonSerializer.Deserialize<ModCollection>(JsonSerializer.Serialize(source));
clone.Id = FixtureId;
clone.Name = "Collection settings test";
// A fixture must not inherit the game's localized display names.
clone.Locale = null;
clone.Settings["custom.fixture"] = "keep";
clone.Settings["battle.core"] = "missing_core";
Directory.CreateDirectory(path);
fixturePath = path;
File.WriteAllText(Path.Combine(path, "collection.json"), JsonSerializer.Serialize(clone));
}
private void OpenForm()
{
editor.GetNode<Button>("Margin/Root/Tools/GameSettings").EmitSignal(BaseButton.SignalName.Pressed);
dialog = editor.FindChildren("*", "", true, false).OfType<CollectionSettingsDialog>().Single(d => !d.IsQueuedForDeletion());
}
private CollectionSettingRow Row(string key) => dialog.FindChildren("*", "", true, false).OfType<CollectionSettingRow>().Single(r => r.Key == key);
private static void Custom(CollectionSettingRow row, string value)
{
var select = row.FindChildren("*", "", true, false).OfType<OptionButton>().First();
Select(row, select.ItemCount - 1);
row.FindChildren("*", "", true, false).OfType<LineEdit>().First().Text = value;
}
private static void Select(CollectionSettingRow row, int index)
{
var select = row.FindChildren("*", "", true, false).OfType<OptionButton>().First();
select.Select(index);
select.EmitSignal(OptionButton.SignalName.ItemSelected, (long)index);
}
private void Check(bool condition, string label)
{
if (!condition)
throw new InvalidOperationException(label);
passed++;
GameLogger.Log("TEST", "Collection settings: [OK] " + label);
}
private void Finish(string failure)
{
done = true;
hub?.QueueFree();
CleanupFixture();
if (originalLocale != null)
LocaleLoader.LoadLocale(originalLocale);
if (failure != null)
GameLogger.Log("TEST", "Collection settings: " + failure, LogLevel.Error);
GameLogger.Log("TEST", $"[TEST_RESULT] name=collection_settings passed={passed} failed={(failure == null ? 0 : 1)}");
GetTree().Quit(failure == null ? 0 : 1);
}
/// <summary>Also remove the owned fixture when the suite exits before Finish.</summary>
public override void _ExitTree() => CleanupFixture();
private void CleanupFixture()
{
if (fixturePath != null && Directory.Exists(fixturePath))
Directory.Delete(fixturePath, true);
fixturePath = null;
}
}
}
@@ -0,0 +1,18 @@
namespace Mods.ModManager.SettingsTest
{
using Cthangover.Core.Mods;
/// <summary>Explicit frontend-only UI test, leaving game assemblies unloaded.</summary>
public class CollectionSettingsTestInitializer : IModInitializer
{
private static bool queued;
public void OnModLoaded(string modId) { }
public void OnModResourcesReady()
{
if (queued)
return;
queued = true;
ModRoot.AddDeferred(new CollectionSettingsTestDriver());
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Mod documentation test",
"depends": ["menu_base>=1"],
"sources": ["src/*.cs"]
}
@@ -0,0 +1,302 @@
using System;
using System.IO;
using System.Linq;
using Cthangover.Core.Bootstrap;
using Cthangover.Core.Localization;
using Cthangover.Core.Mods;
using Cthangover.Core.UI.Menu;
using Cthangover.Core.Utils;
using Cthangover.Core.Settings;
using Cthangover.BaseMenu;
using Cthangover.Mods.Interface.Menu;
using Cthangover.Mods.Interface.View;
using Godot;
namespace Mods.ModManager.DocumentationTest
{
/// <summary>
/// Exercises the reference page, or reviews a real English page with
/// --doc-mod=id. --doc-settings additionally captures the real Sound tab;
/// the original settings file and language are restored even after failure.
/// </summary>
public partial class DocumentationTestDriver : Node
{
private double elapsed;
private double stepAt;
private int step;
private int passed;
private bool done;
private string originalLocale;
private ModHubScreen hub;
private ModManagerScreen manager;
private ModDetailView detail;
private readonly string reviewMod = ReviewMod();
private string reviewTitle;
private SettingsMenu settings;
private string settingsPath;
private byte[] settingsBackup;
private string settingsLanguage;
public override void _Process(double delta)
{
if (done)
return;
elapsed += delta;
if (elapsed > 60)
{
Finish("timeout at step " + step);
return;
}
if (elapsed - stepAt < 0.6)
return;
stepAt = elapsed;
try
{
if (string.IsNullOrEmpty(reviewMod))
RunStep();
else
RunReviewStep();
}
catch (Exception ex)
{
Finish(ex.ToString());
}
}
private void RunStep()
{
switch (step)
{
case 0:
if (!Boot.IsReady)
return;
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcherPlay)
{
launcherPlay.EmitSignal(BaseButton.SignalName.Pressed);
return;
}
if (GetTree().CurrentScene?.FindChild("play", true, false) is not Button)
return;
originalLocale = TranslationServer.GetLocale();
LocaleLoader.LoadLocale("ru");
var entry = MenuRegistry.GetButtonById("mods_hub");
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry.ScenePath, "mods_hub") as ModHubScreen;
Check(hub != null, "hub opens");
break;
case 1:
manager = hub.FindChildren("*", "", true, false).OfType<ModManagerScreen>().First();
manager.SetSearch("music_pack");
break;
case 2:
var card = hub.FindChildren("*", "", true, false).OfType<ModCard>().First(IsVisibleCard);
card.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
break;
case 3:
detail = hub.FindChildren("*", "", true, false).OfType<ModDetailView>().First(IsVisibleDetail);
Check(Body().GetParsedText().Contains("музыка для вашей игры"), "automatic Russian document");
Check(Body().GetContentHeight() > 600, "long document lays out");
var codeFont = Body().GetThemeFont("mono_font");
Check(codeFont.HasChar('└') && codeFont.HasChar('─') && codeFont.HasChar('├'), "bundled monospace font covers tree glyphs");
Check(ModDocumentation.Discover(ModRegistry.Instance.AllDiscoveredMods["music_pack"].FileProvider).Count == LanguageCode.Supported.Length, "every project language is bundled");
Shot("music_pack_ru");
SelectLanguage("EN");
break;
case 4:
Check(Body().GetParsedText().Contains("a soundtrack for your game"), "manual English document");
Check(!Body().GetParsedText().Contains(TranslationServer.Translate("mod_manager/docs_image_unavailable").ToString()), "bundled image resolves");
Shot("music_pack_en");
ScrollToText("// Add the dependency");
break;
case 5:
Shot("music_pack_code_en");
var languages = Languages();
languages.Select(0);
languages.EmitSignal(OptionButton.SignalName.ItemSelected, 0L);
LocaleLoader.LoadLocale("ja");
break;
case 6:
Check(Body().GetParsedText().Contains("あなたのゲームのためのサウンドトラック"), "auto language follows the interface language (Japanese)");
Check(detail.GetNode<Label>("Scroll/Content/DocumentationBox/Source").Text.Contains("JA"), "actual document language is visible");
LocaleLoader.LoadLocale("de");
break;
case 7:
Check(Body().GetParsedText().Contains("a soundtrack for your game"), "unbundled German falls back to English after locale change");
LocaleLoader.LoadLocale("en");
break;
case 8:
Check(detail.GetNode<Label>("Scroll/Content/DocumentationBox/Source").Text.Contains("EN"), "document language is visible for a bundled UI language");
// A controlled legacy record stays valid as documentation
// is gradually added to every real mod in the project.
detail.Open(new ModCardData { Id = "documentation_legacy_fixture", Title = "Legacy fixture", Description = "Manifest-only description" });
break;
case 9:
Check(!detail.GetNode<Control>("Scroll/Content/DocumentationBox").Visible, "legacy mod keeps manifest description");
Check(!string.IsNullOrWhiteSpace(detail.GetNode<Label>("Scroll/Content/DescriptionBox/Text").Text), "legacy description remains readable");
Finish(null);
break;
}
step++;
}
private static string ReviewMod()
{
foreach (var arg in OS.GetCmdlineUserArgs())
if (arg.StartsWith("--doc-mod=", StringComparison.Ordinal))
return arg.Substring("--doc-mod=".Length);
return null;
}
private void RunReviewStep()
{
switch (step)
{
case 0:
if (!Boot.IsReady)
return;
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcherPlay)
{
launcherPlay.EmitSignal(BaseButton.SignalName.Pressed);
return;
}
if (GetTree().CurrentScene?.FindChild("play", true, false) is not Button)
return;
originalLocale = TranslationServer.GetLocale();
settingsLanguage = GameData.Instance.Settings.Language;
settingsPath = ProjectSettings.GlobalizePath("user://settings.json");
settingsBackup = File.Exists(settingsPath) ? File.ReadAllBytes(settingsPath) : null;
settings = GetTree().CurrentScene.GetNode<SettingsMenu>("SettingsPanel");
settings.Open();
var language = settings.GetNode<OptionButton>("Window/Column/Body/Pages/General/LangRow/LangSelect");
var english = Array.IndexOf(LanguageCode.Supported, "en");
language.Select(english);
language.EmitSignal(OptionButton.SignalName.ItemSelected, (long)english);
settings.ShowTab(1);
break;
case 1:
Check(TranslationServer.GetLocale().StartsWith("en", StringComparison.Ordinal), "English UI selected through Settings");
if (OS.GetCmdlineUserArgs().Contains("--doc-settings") || OS.GetCmdlineUserArgs().Contains("--doc-settings-only"))
{
var audio = settings.GetNode<Control>("Window/Column/Body/Pages/Audio");
Check(audio.IsVisibleInTree(), "actual Sound tab visible");
Shot(reviewMod + "_sound_settings");
}
if (OS.GetCmdlineUserArgs().Contains("--doc-settings-only"))
{
Finish(null);
return;
}
settings.Visible = false;
var entry = MenuRegistry.GetButtonById("mods_hub");
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry.ScenePath, "mods_hub") as ModHubScreen;
Check(hub != null, "hub opens for documentation review");
break;
case 2:
manager = hub.FindChildren("*", "", true, false).OfType<ModManagerScreen>().First();
manager.SetSearch(reviewMod);
break;
case 3:
// The search also matches test suites such as mod_manager__documentation; open the reviewed mod itself.
reviewTitle = ModRegistry.Instance.AllDiscoveredMods[reviewMod].DisplayTitle ?? reviewMod;
var card = hub.FindChildren("*", "", true, false).OfType<ModCard>().First(IsReviewedCard);
card.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
break;
case 4:
detail = hub.FindChildren("*", "", true, false).OfType<ModDetailView>().First(IsVisibleDetail);
var provider = ModRegistry.Instance.AllDiscoveredMods[reviewMod].FileProvider;
var document = ModDocumentation.Select(ModDocumentation.Discover(provider), "en");
Check(document?.Path == "docs/en.md", "English entry point exists for " + reviewMod);
var heading = document.Text.Split('\n')[0].TrimStart('#', ' ', '\uFEFF').TrimEnd('\r');
var shown = Body().GetParsedText();
Check(shown.Contains(heading), "actual page displays the selected English document; heading '" + heading + "', shown '" + shown.Substring(0, Math.Min(160, shown.Length)) + "'");
Check(!Body().GetParsedText().Contains(TranslationServer.Translate("mod_manager/docs_image_unavailable").ToString()), "all page images resolve");
Check(Body().GetContentHeight() > 300, "page lays out for scrolling");
Shot(reviewMod + "_documentation_en");
LocaleLoader.LoadLocale("ru");
break;
case 5:
Check(detail.GetNode<Label>("Scroll/Content/DocumentationBox/Source").Text.Contains("EN"), "actual English fallback is identified");
Check(!string.IsNullOrWhiteSpace(Body().GetParsedText()), "fallback document remains readable");
Shot(reviewMod + "_documentation_fallback");
Finish(null);
break;
}
step++;
}
private ModMarkdownView Body() => detail.GetNode<ModMarkdownView>("Scroll/Content/DocumentationBox/Body");
private OptionButton Languages() => detail.GetNode<OptionButton>("Scroll/Content/DocumentationBox/Header/Languages");
private static bool IsVisibleCard(ModCard card) => card.IsVisibleInTree();
private bool IsReviewedCard(ModCard card) => IsVisibleCard(card) && card.GetNode<Label>("VBox/Body/Box/Title").Text == reviewTitle;
private static bool IsVisibleDetail(ModDetailView page) => page.IsVisibleInTree();
private static bool IsScreenshotArg(string arg) => arg.StartsWith("--shots=");
private void ScrollToText(string needle)
{
var text = Body().GetParsedText();
var offset = text.IndexOf(needle, StringComparison.Ordinal);
Check(offset >= 0, "code example remains literal and selectable");
var paragraph = 0;
for (var i = 0; i < offset; i++)
if (text[i] == '\n')
paragraph++;
var scroll = detail.GetNode<ScrollContainer>("Scroll");
var content = detail.GetNode<Control>("Scroll/Content");
scroll.ScrollVertical = (int)(Body().GlobalPosition.Y - content.GlobalPosition.Y + Body().GetParagraphOffset(paragraph)) - 32;
}
private void SelectLanguage(string language)
{
var languages = Languages();
for (var i = 0; i < languages.ItemCount; i++)
if (languages.GetItemText(i) == language)
{
languages.Select(i);
languages.EmitSignal(OptionButton.SignalName.ItemSelected, (long)i);
return;
}
throw new InvalidOperationException("Missing language " + language);
}
private void Check(bool ok, string label)
{
if (!ok)
throw new InvalidOperationException(label);
passed++;
GameLogger.Log("TEST", "Documentation: [OK] " + label);
}
private void Shot(string name)
{
var arg = OS.GetCmdlineUserArgs().FirstOrDefault(IsScreenshotArg);
if (arg == null || DisplayServer.GetName() == "headless")
return;
var directory = ProjectSettings.GlobalizePath(arg.Substring(8));
Directory.CreateDirectory(directory);
ViewportCapture.SavePng(GetViewport(), Path.Combine(directory, name + ".png"));
}
private async void Finish(string failure)
{
if (done)
return;
done = true;
hub?.QueueFree();
if (settingsPath != null)
{
GameData.Instance.Settings.Language = settingsLanguage;
if (settingsBackup == null)
File.Delete(settingsPath);
else
File.WriteAllBytes(settingsPath, settingsBackup);
}
if (originalLocale != null)
LocaleLoader.LoadLocale(originalLocale);
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
if (failure != null)
GameLogger.Log("TEST", "Documentation: " + failure, LogLevel.Error);
GameLogger.Log("TEST", $"[TEST_RESULT] name=mod_documentation passed={passed} failed={(failure == null ? 0 : 1)}");
GetTree().Quit(failure == null ? 0 : 1);
}
}
}
@@ -0,0 +1,18 @@
using Cthangover.Core.Mods;
namespace Mods.ModManager.DocumentationTest
{
/// <summary>Mounting this suite explicitly enables the documentation UI walk-through.</summary>
public class DocumentationTestInitializer : IModInitializer
{
private static bool queued;
public void OnModLoaded(string modId) { }
public void OnModResourcesReady()
{
if (queued)
return;
queued = true;
ModRoot.AddDeferred(new DocumentationTestDriver());
}
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "Mod Manager Help Test",
"description": "Integration test of the mod manager's help: opens the Mods & Collections hub on every level (mods grid, a mod's page, games gallery, scheme editor with a selected mechanic and mod, help panels) and checks that every visible button, toggle, list and input explains itself with a tooltip. Activated only by --itest=mod_help (with --frontend); --shots=<dir> saves every level.",
"sources": [
"src/*.cs"
]
}
@@ -0,0 +1,703 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cthangover.Core.Bootstrap;
using Cthangover.Core.UI.Menu;
using Cthangover.Core.Utils;
using Cthangover.Mods.Interface.Menu;
using Cthangover.Mods.Interface.Theming;
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.View;
using Godot;
using Mods.ModManager;
namespace Mods.ModManagerHelpTest
{
using System.Text.Json;
using Cthangover.Core.Mods;
/// <summary>
/// Exercises contextual help as a player uses it: pin a hint beside a mod search,
/// keep the hub interactive while help is open, inspect a mod page and the scheme
/// legend, invoke F1 from keyboard focus and leave with Escape without navigating
/// away. Also rejects tooltips that merely repeat their button captions and
/// exercises Graphite's scoped version edits, duplicate occurrences, zoom and
/// 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>
public partial class ModHelpTestDriverNode : Node
{
private const string HubId = "mods_hub";
private const string GameId = "novel_template";
private const float TimeoutSeconds = 90f;
private readonly List<string> failures = new();
private int passed;
private int step;
private double elapsed;
private double stepAt;
private bool done;
private ModHubScreen hub;
private ModCollectionsScreen collectionEditor;
private CollectionSchemeCanvas scheme;
private Vector2 firstOccurrencePosition;
private string layoutPath;
private byte[] layoutBackup;
public override void _Process(double delta)
{
if (done)
return;
elapsed += delta;
if (elapsed > TimeoutSeconds)
{
failures.Add($"timeout at step {step}");
Finish();
return;
}
if (elapsed - stepAt < 0.5)
return;
stepAt = elapsed;
try
{
RunStep();
}
catch (Exception ex)
{
failures.Add($"step {step}: {ex.GetType().Name}: {ex.Message}");
Finish();
}
}
private void RunStep()
{
switch (step)
{
case 0:
if (!Boot.IsReady)
return;
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcherPlay)
{
launcherPlay.EmitSignal(BaseButton.SignalName.Pressed);
return;
}
if (GetTree().CurrentScene?.FindChild("play", true, false) is not Button)
return;
var entry = MenuRegistry.GetButtonById(HubId);
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry?.ScenePath, HubId) as ModHubScreen;
Check("the hub opens", hub != null);
if (hub == null)
Finish();
break;
case 1:
{
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;
}
case 2:
{
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());
Check("the grid shows mod cards", card != null);
card?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
break;
}
case 3:
{
Check("the hub stays interactive while help is open", hub.FindChildren("*", "", true, false).OfType<ModDetailView>().Any(d => d.IsVisibleInTree()));
CheckLevel("mod_page");
CheckHelp("mod_version", Annotated("mod_version"));
PressTab("mod_manager/hub_tab_collections");
break;
}
case 4:
{
Check("collections open without closing help", Gallery() != null && Gallery().IsVisibleInTree() && Help().Visible);
CheckLevel("gallery");
EscapeHelp();
hub.OpenCollectionEditor(GameId);
break;
}
case 5:
{
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());
Check("the scheme shows mechanics", group != null);
Click(group);
break;
}
case 6:
{
CheckLevel("scheme_mechanic");
var chip = hub.FindChildren("*", "", true, false).OfType<CollectionModChip>().FirstOrDefault(c => c.IsVisibleInTree());
Check("the mechanic shows its mods", chip != null);
Click(chip);
break;
}
case 7:
{
CheckLevel("scheme_mod");
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;
}
case 8:
// Leave the F1 state visible for the deferred screenshot.
EscapeHelp();
Check("Escape leaves help without closing the editor", Annotated("load_order") != null);
hub.OpenCollectionEditor("cthangover");
break;
case 9:
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;
case 10:
SetGraphiteFixture();
break;
case 11:
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;
case 12:
SelectVersionPolicy("=");
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();
break;
}
step++;
}
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)
{
var panel = owner?.GetChildren().OfType<PanelContainer>().FirstOrDefault();
panel?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
panel?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = false });
}
private void PressTab(string key)
{
var text = TranslationServer.Translate(key).ToString();
var tab = Buttons(hub).FirstOrDefault(b => TextOf(b) == text && b.IsVisibleInTree() && b.ToggleMode);
tab ??= Buttons(hub).FirstOrDefault(b => TextOf(b) == text && b.IsVisibleInTree());
Check($"tab '{text}' shown", tab != null);
tab?.EmitSignal(BaseButton.SignalName.Pressed);
}
private void PressHelp()
{
var help = Buttons(hub).FirstOrDefault(b => b.Name == "HelpButton" && b.IsVisibleInTree());
Check("the header offers contextual help", help != null);
help?.EmitSignal(BaseButton.SignalName.Pressed);
}
private void EscapeHelp()
{
hub._UnhandledInput(new InputEventAction { Action = "ui_cancel", Pressed = true });
Check("Escape closes help", Help() != null && !Help().Visible);
}
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)
{
Shot(level);
var echoes = new List<string>();
var deaf = new List<string>();
foreach (var control in hub.FindChildren("*", "", true, false).OfType<Control>())
{
if (!control.IsVisibleInTree())
continue;
var tip = control.TooltipText;
if (control is Button button
&& !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)
deaf.Add(Describe(control));
}
Check($"{level}: no tooltip repeats its button" + (echoes.Count > 0 ? ": " + string.Join(", ", echoes) : string.Empty), echoes.Count == 0);
Check($"{level}: no tooltip ignores the mouse" + (deaf.Count > 0 ? ": " + string.Join(", ", deaf) : string.Empty), deaf.Count == 0);
}
private static string Describe(Control control)
{
var text = control switch
{
Button button => button.Text,
LineEdit edit => edit.PlaceholderText,
_ => string.Empty,
};
return $"{control.GetClass()} '{control.Name}'" + (string.IsNullOrEmpty(text) ? string.Empty : $" \"{text}\"");
}
private static IEnumerable<BaseButton> Buttons(Node root)
=> root == null ? Enumerable.Empty<BaseButton>() : root.FindChildren("*", "", true, false).OfType<BaseButton>();
private static string TextOf(BaseButton button)
=> button is Button clickable ? clickable.Text : string.Empty;
/// <summary>With <c>--shots=&lt;dir&gt;</c> saves the settled window as <paramref name="name"/>.png.</summary>
private async void Shot(string name)
{
string dir = null;
foreach (var arg in OS.GetCmdlineUserArgs())
{
if (arg.StartsWith("--shots="))
dir = arg.Substring("--shots=".Length);
}
if (string.IsNullOrEmpty(dir) || DisplayServer.GetName() == "headless")
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);
Directory.CreateDirectory(dir);
ViewportCapture.SavePng(GetViewport(), Path.Combine(dir, name + ".png"));
}
private void Check(string name, bool ok)
{
if (ok)
{
passed++;
GameLogger.Log("TEST", $"ModHelpTest: [OK] {name}");
}
else
{
failures.Add(name);
GameLogger.Log("TEST", $"ModHelpTest: [FAIL] {name}", LogLevel.Error);
}
}
private async void Finish()
{
if (done)
return;
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}");
foreach (var failure in failures)
GameLogger.Log("TEST", $"ModHelpTest: failure: {failure}", LogLevel.Error);
GetTree().Quit(failures.Count == 0 ? 0 : 1);
}
}
}
@@ -0,0 +1,25 @@
using System.Linq;
using Cthangover.Core.Mods;
using Cthangover.Core.Utils;
using Godot;
namespace Mods.ModManagerHelpTest
{
/// <summary>Queues <see cref="ModHelpTestDriverNode"/> only for <c>--itest=mod_help</c>.</summary>
public class ModHelpTestInitializer : IModInitializer
{
private static bool queued;
public void OnModLoaded(string modId) { }
public void OnModResourcesReady()
{
if (queued || !OS.GetCmdlineUserArgs().Contains("--itest=mod_help"))
return;
queued = true;
GameLogger.Log("TEST", "ModHelpTest: activating");
ModRoot.AddDeferred(new ModHelpTestDriverNode());
}
}
}
+79
View File
@@ -0,0 +1,79 @@
namespace Mods.ModManager.Tests
{
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json;
using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Providers;
using Cthangover.Core.Mods.Testing;
/// <summary>Declarations work before mod activation, including ZIP storage, translated resources and unavailable choices.</summary>
public class CollectionSettingsCatalogTests
{
[ModTest("collection_settings/roundtrip")]
static void Roundtrip(ModTestContext ctx)
{
var original = new CollectionSettingsFile
{
Settings = new() { new() { Key = "test.choice", Name = "field", Description = "hint", Group = "group", Type = "resource", Source = "charsets", AllowCustom = true, Automatic = "fallback", Pattern = "^d[0-9]+$", Options = new() { new() { Key = "test.choice", Value = "d20", Name = "twenty" } } } },
Options = new() { new() { Key = "test.choice", Value = "d6", Name = "six" } },
};
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
var restored = JsonSerializer.Deserialize<CollectionSettingsFile>(JsonSerializer.Serialize(original, options), options);
ModAssert.Equal(JsonSerializer.Serialize(original, options), JsonSerializer.Serialize(restored, options), "all declaration fields survive serialization");
ModAssert.Equal("d20", restored.Settings[0].Options[0].Value);
ModAssert.Equal("test.choice", restored.Options[0].Key);
}
[ModTest("collection_settings/offline_folder_zip")]
static void OfflineFolderZip(ModTestContext ctx)
{
var root = Path.Combine(ctx.SandboxDir, "collection_settings");
var zip = root + ".zip";
Directory.CreateDirectory(Path.Combine(root, "locale"));
Directory.CreateDirectory(Path.Combine(root, "charsets", "village"));
Directory.CreateDirectory(Path.Combine(root, "characters"));
File.WriteAllText(Path.Combine(root, "collection_settings.json"), "{\"settings\":[{\"key\":\"look\",\"name\":\"appearance\",\"type\":\"resource\",\"source\":\"charsets\"},{\"key\":\"who\",\"type\":\"character\",\"source\":\"characters\",\"allow_custom\":true},{\"key\":\"dice\",\"allow_custom\":true,\"pattern\":\"^(d20|2d6|none)$\"}],\"options\":[{\"key\":\"dice\",\"value\":\"d20\",\"name\":\"twenty\"}]}");
File.WriteAllText(Path.Combine(root, "locale", "settings_en.properties"), "appearance=Appearance\nhero=Hero\ntwenty=Twenty\n");
File.WriteAllText(Path.Combine(root, "locale", "settings_ru.properties"), "appearance=Внешность\nhero=Герой\n");
File.WriteAllBytes(Path.Combine(root, "charsets", "village", "hero.png"), new byte[] { 1 });
File.WriteAllText(Path.Combine(root, "characters", "heroes.json"), "{\"Kind\":\"character\",\"Items\":[{\"Id\":\"hero\",\"Name\":\"hero\"}]}");
try
{
ZipFile.CreateFromDirectory(root, zip);
using var folder = new FolderModFileProvider(root, "fixture");
using var archive = new ZipModFileProvider(zip, "fixture");
foreach (var provider in new IModFileProvider[] { folder, archive })
{
var mods = new IModInfo[] { new ModInfo { Id = "fixture", FileProvider = provider } };
var catalog = new CollectionSettingsCatalog(mods, new[] { "fixture" }, "ru_RU");
ModAssert.Equal(3, catalog.Definitions.Count, "declarations without loaded assemblies");
ModAssert.Equal("Внешность", catalog.Translate("appearance"), "regional locale");
ModAssert.Equal("Twenty", catalog.Translate("twenty"), "English fallback");
var look = catalog.Definitions.Single(d => d.Key == "look");
var sprite = catalog.Choices(look).Single();
ModAssert.Equal("village/hero", sprite.Value, "resource id keeps subfolder");
ModAssert.Equal("charsets/village/hero.png", sprite.PreviewPath);
var who = catalog.Definitions.Single(d => d.Key == "who");
ModAssert.Equal("Герой", catalog.Translate(catalog.Choices(who).Single().Name));
ModAssert.True(catalog.IsValidCustom(who, "created_at_runtime"), "runtime character ids allowed");
var dice = catalog.Definitions.Single(d => d.Key == "dice");
ModAssert.True(catalog.IsValidCustom(dice, "2d6"), "valid custom syntax");
ModAssert.False(catalog.IsValidCustom(dice, "typo"), "invalid custom syntax");
var inactive = new CollectionSettingsCatalog(mods, Array.Empty<string>(), "en");
var inactiveDice = inactive.Definitions.Single(d => d.Key == "dice");
ModAssert.False(inactiveDice.Available, "removed owner's saved keys remain describable");
ModAssert.False(inactive.Choices(inactiveDice).Single().Available, "choice from removed mod is unavailable");
ModAssert.Equal(0, inactive.Choices(inactive.Definitions.Single(d => d.Key == "look")).Count, "unselected resources excluded");
}
}
finally
{
File.Delete(zip);
Directory.Delete(root, true);
}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
using System.Collections.Generic;
using System.Text;
using Cthangover.Core.Mods.Testing;
namespace Mods.ModManager.Tests
{
/// <summary>Colouring preserves source literally and recognises each supported fence grammar.</summary>
public class ModCodeSyntaxTests
{
[ModTest("documentation/code_languages")]
static void Languages(ModTestContext ctx)
{
ModAssert.Equal("csharp", ModCodeSyntax.Language(" C# title=example "));
ModAssert.Equal("gdscript", ModCodeSyntax.Language("GD"));
ModAssert.Equal("bash", ModCodeSyntax.Language("shell"));
ModAssert.Equal("yaml", ModCodeSyntax.Language("yml"));
ModAssert.Equal("glsl", ModCodeSyntax.Language("gdshader"));
ModAssert.Equal("", ModCodeSyntax.Language(null));
ModAssert.Equal("unknown", ModCodeSyntax.Language("unknown"));
}
[ModTest("documentation/code_jsonc")]
static void Jsonc(ModTestContext ctx)
{
const string source = "{\r\n\t\"name\": \"// literal \\\"quoted\\\" Русский\", // comment\r\n \"enabled\": true, /* two\nlines */ \"count\": 22\n} ";
var spans = ModCodeSyntax.Tokenize("jsonc", source);
ModAssert.Equal(source, Join(spans), "all whitespace and Unicode preserved");
Expect(spans, "\"name\"", "attr");
Expect(spans, "\"// literal \\\"quoted\\\" Русский\"", "string");
Expect(spans, "// comment", "comment");
Expect(spans, "/* two\nlines */", "comment");
Expect(spans, "true", "literal");
Expect(spans, "22", "number");
}
[ModTest("documentation/code_site_languages")]
static void SiteLanguages(ModTestContext ctx)
{
Expect(ModCodeSyntax.Tokenize("csharp", "public class Example { var value = 42; }"), "public", "keyword");
Expect(ModCodeSyntax.Tokenize("gdscript", "func start():\n # comment\n return true"), "func", "keyword");
var scenario = ModCodeSyntax.Tokenize("scenario", "scene: intro\n---\ntext \"# literal\" key=intro\noption \"Go\" -> :next\n-- comment");
Expect(scenario, "text", "keyword");
Expect(scenario, "\"# literal\"", "string");
Expect(scenario, "key", "attr");
Expect(scenario, ":next", "label");
Expect(scenario, "---", "meta");
Expect(scenario, "-- comment", "comment");
var tree = ModCodeSyntax.Tokenize("filestree", "mods/\n└── music_pack/ # shared music\n └── manifest.json");
Expect(tree, "music_pack/", "directory");
Expect(tree, "# shared music", "treecomment");
Expect(ModCodeSyntax.Tokenize("glsl", "uniform vec4 color;\nvoid fragment() { COLOR = color; }"), "uniform", "keyword");
Expect(ModCodeSyntax.Tokenize("bash", "export GAME=demo # comment\necho $GAME"), "$GAME", "variable");
Expect(ModCodeSyntax.Tokenize("python", "def start():\n return True"), "def", "keyword");
Expect(ModCodeSyntax.Tokenize("yaml", "enabled: true\ncount: 22"), "enabled", "attr");
Expect(ModCodeSyntax.Tokenize("ini", "[section]\nname=value\n# comment"), "[section]", "meta");
Expect(ModCodeSyntax.Tokenize("properties", "mod/name=Музыка\n"), "Музыка", "string");
}
[ModTest("documentation/code_plain_fallback")]
static void PlainFallback(ModTestContext ctx)
{
const string literal = "[url=javascript:bad]literal[/url] <script> // # true\n\t ";
foreach (var language in new[] { "", "text", "unknown", "mermaid" })
{
var spans = ModCodeSyntax.Tokenize(language, literal);
ModAssert.Equal(literal, Join(spans), language + " preserves literal source");
foreach (var span in spans)
ModAssert.Equal("", span.Kind, language + " does not guess a language");
}
var large = new string('x', 65537);
var oversized = ModCodeSyntax.Tokenize("json", large);
ModAssert.Equal(large, Join(oversized), "oversized source remains readable");
ModAssert.Equal("", oversized[0].Kind, "oversized source skips colouring");
}
private static string Join(IReadOnlyList<(string Text, string Kind)> spans)
{
var result = new StringBuilder();
foreach (var span in spans)
result.Append(span.Text);
return result.ToString();
}
private static void Expect(IReadOnlyList<(string Text, string Kind)> spans, string text, string kind)
{
foreach (var span in spans)
if (span.Text == text && span.Kind == kind)
return;
ModAssert.True(false, "expected " + kind + " token: " + text);
}
}
}
+89
View File
@@ -0,0 +1,89 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Cthangover.Core.Mods.Providers;
using Cthangover.Core.Mods.Testing;
namespace Mods.ModManager.Tests
{
/// <summary>Storage and fallback contracts, including malformed files and compressed mods.</summary>
public class ModDocumentationTests
{
[ModTest("documentation/language_fallback")]
static void LanguageFallback(ModTestContext ctx)
{
var docs = new[]
{
new ModDocument("de", "docs/de.md", "Deutsch"),
new ModDocument("en", "docs/en.md", "English"),
new ModDocument("ru", "docs/ru.md", "Русский"),
new ModDocument("default", "docs/default.md", "Default"),
};
ModAssert.Equal("ru", ModDocumentation.Select(docs, "RU_ru").Language, "regional language");
ModAssert.Equal("default", ModDocumentation.Select(docs, "ja").Language, "author fallback");
ModAssert.Equal("en", ModDocumentation.Select(docs.Take(3).ToArray(), "ja").Language, "English fallback");
ModAssert.Equal("de", ModDocumentation.Select(docs.Take(1).ToArray(), "ja").Language, "first available");
ModAssert.Null(ModDocumentation.Select(Array.Empty<ModDocument>(), "ru"), "legacy mod");
}
[ModTest("documentation/folder_and_zip")]
static void FolderAndZip(ModTestContext ctx)
{
var root = Path.Combine(ctx.SandboxDir, "documentation");
var zip = Path.Combine(ctx.SandboxDir, "documentation.zip");
Directory.CreateDirectory(Path.Combine(root, "docs", "assets"));
File.WriteAllText(Path.Combine(root, "docs", "ru.md"), "# Русский\n![Example](assets/picture.png)");
File.WriteAllText(Path.Combine(root, "docs", "en.md"), "# English");
File.WriteAllText(Path.Combine(root, "docs", "README.md"), "Not a language");
File.WriteAllText(Path.Combine(root, "docs", "ja.md"), " \n ");
File.WriteAllBytes(Path.Combine(root, "docs", "ko.md"), new byte[] { 0xFF, 0xFF });
File.WriteAllBytes(Path.Combine(root, "docs", "assets", "picture.png"), new byte[] { 1, 2, 3 });
try
{
using var folder = new FolderModFileProvider(root, "documentation_fixture");
var local = ModDocumentation.Discover(folder);
ModAssert.Equal(2, local.Count, "only readable language files");
ModAssert.True(ModDocumentation.Select(local, "ru").Text.Contains("Русский"), "UTF-8 preserved");
ModAssert.Null(ModDocumentation.ReadResource(folder, "docs/en.md", 2), "size limit");
ModAssert.Null(ModDocumentation.ReadResource(folder, "docs/missing.md", 100), "missing file");
ZipFile.CreateFromDirectory(root, zip);
using var archive = new ZipModFileProvider(zip, "documentation_fixture");
var packed = ModDocumentation.Discover(archive);
ModAssert.Equal(local.Count, packed.Count, "ZIP discovery");
ModAssert.Equal(ModDocumentation.Select(local, "ru").Text, ModDocumentation.Select(packed, "ru").Text, "ZIP text");
ModAssert.Equal(3, ModDocumentation.ReadResource(archive, "docs/assets/picture.png", 100).Length, "ZIP image bytes");
}
finally
{
if (File.Exists(zip))
File.Delete(zip);
Directory.Delete(root, true);
}
}
[ModTest("documentation/resource_paths")]
static void ResourcePaths(ModTestContext ctx)
{
ModAssert.Equal("docs/assets/a.png", ModDocumentation.ResolveResource("docs/ru.md", "assets/a.png"));
ModAssert.Equal("docs/assets/a.png", ModDocumentation.ResolveResource("docs/ru/page.md", "../assets/a.png"));
foreach (var path in new[] { "../../secret.png", "/tmp/a.png", "C:\\a.png", "https://example.com/a.png", "file:///etc/passwd", "\\server\\a.png" })
ModAssert.Null(ModDocumentation.ResolveResource("docs/ru.md", path), path);
}
[ModTest("documentation/markdown_literals")]
static void MarkdownLiterals(ModTestContext ctx)
{
var view = new ModMarkdownView();
ctx.SandboxRoot.AddChild(view);
view.Display(null, new ModDocument("en", "docs/en.md", "# Heading\n\n**Bold** and `code`\n\n```json\n[url=javascript:bad]literal[/url]\n```\n\n<script>literal HTML</script>\n\n[Visible](javascript:bad)"));
var text = view.GetParsedText();
ModAssert.True(text.Contains("Heading") && text.Contains("Bold and code"), "Markdown text preserved");
ModAssert.True(text.Contains("[url=javascript:bad]literal[/url]"), "code never becomes BBCode");
ModAssert.True(text.Contains("<script>literal HTML</script>"), "HTML never executes");
ModAssert.True(text.Contains("Visible"), "unsupported link retains label");
view.Display(null, null);
ModAssert.Equal("", view.GetParsedText(), "clearing previous document");
}
}
}