diff --git a/AGENTS.md b/AGENTS.md index 149df59..9570758 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,8 @@ |---|---| | User setup | `README.md` | | Configuration contract | `docs/configuration.md` | -| Runtime design, build, testing, release | `docs/development.md` | +| Runtime design, build, testing | `docs/development.md` | +| Release process and packaging | `docs/release.md` | | Change history | `CHANGELOG.md` | | CI | `.github/workflows/ci.yml` | | Published releases | `.github/workflows/release.yml` | diff --git a/README.md b/README.md index 7ec88f2..56a9686 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A configurable **BepInEx 5 plugin for Unity Mono games using TextMeshPro**. Add missing Vietnamese glyphs or use a consistent replacement font for selected font groups. -This plugin does not translate text. It can work alongside XUnity.AutoTranslator or an existing Vietnamese translation. +This plugin does not translate text. It can work alongside [XUnity.AutoTranslator](https://github.com/bbepis/XUnity.AutoTranslator) or an existing Vietnamese translation. ## Compatibility @@ -24,7 +24,7 @@ This plugin does not translate text. It can work alongside XUnity.AutoTranslator ## Installation 1. Install [BepInEx 5 for Unity Mono](https://docs.bepinex.dev/articles/user_guide/installation/index.html). -2. Extract the release archive into `/BepInEx/`. The plugin DLL should be at: +2. Download the ZIP from the [latest release](https://github.com/zeldrisho/UnityVietnameseFontSync/releases/latest) and extract it into `/BepInEx/`. The plugin DLL should be at: ```text /BepInEx/plugins/zeldrisho.UnityVietnameseFontSync/UnityVietnameseFontSync.dll ``` @@ -32,36 +32,36 @@ This plugin does not translate text. It can work alongside XUnity.AutoTranslator ```text /BepInEx/config/zeldrisho.UnityVietnameseFontSync.cfg ``` -4. Close the game, configure a font file and matching rules, then start it again. - -There is no need for a separate BepInEx-only run before installing this plugin. If you previously used a UnityExplorer font script, restart with that script disabled and do not run it alongside the plugin. +4. Close the game and follow the setup steps below to discover font names and configure a matching rule. The initial configuration makes **no font changes**. No game assemblies or proprietary font files are bundled. -## Worked configuration example +## Configure and verify + +### 1. Discover the font asset name + +With the game closed, set `Diagnostics = true` under `[General]` in the generated config. Leave `Rules.Order` empty while discovering names. Start the game, visit the menus or screens you want to change, then inspect `Discovered TMP font:` entries in `BepInEx/LogOutput.log`. Match the TMP font asset name, not the text component name. + +### 2. Add an exact Fallback rule + +Close the game before editing. Create this directory and put a licensed Vietnamese-capable font file in it; fonts are not included in the release: -The generated config starts with no matching rules, so it makes no font changes. Close the game before editing the file, then restart it. Enable diagnostics to find the TMP font asset name in `BepInEx/LogOutput.log`; match the asset name, not the text component name. +```text +/BepInEx/plugins/zeldrisho.UnityVietnameseFontSync/Fonts/ +``` -This complete example synchronizes one fictional asset named `MainMenu SDF`, using a primary font plus a secondary font for missing characters. Replace the filenames and exact asset name with values available in your installation. Put both licensed font files in the plugin's `Fonts/` directory. **Synchronize replaces the matched font's characters in memory**; do not broaden the match to icon or unrelated fonts. +Use the following example, replacing `MyVietnameseFont.ttf` and the fictional asset name `MainMenu SDF` with your filename and discovered name. Other settings can retain their generated defaults. ```ini [General] Enabled = true Diagnostics = true -ScanInterval = 2 -RetryInterval = 30 [Fonts] Profiles = menu -CustomCharacters = [Font.menu] File = MyVietnameseFont.ttf -FallbackFiles = CoverageFont.ttf -SamplingSize = 36 -Padding = 4 -AtlasSize = 2048 -MultiAtlas = true [Rules] Order = menu-text @@ -71,16 +71,20 @@ Exclude = Icons, Symbols Profile = menu Match = Exact Pattern = MainMenu SDF -Mode = Synchronize +Mode = Fallback ``` -`FallbackFiles` supplies characters missing from the primary file, while keeping its style for available characters. Coverage must be complete across the ordered files; otherwise the profile is disabled. Leave this setting empty when the primary font covers the whole preset. +Fallback retains original glyphs and adds missing-character coverage. To replace incorrect existing glyphs or make accented and unaccented letters use the same replacement style, change only `Mode = Fallback` to `Mode = Synchronize`. + +**Synchronize clears the matched font's whole character table in memory.** Use narrow rules and check symbols, untranslated text, and layout. Both modes preserve existing fallback chains and leave unmatched fonts unchanged. Matching outlines, shadows, spacing, and line breaks is not guaranteed. Restart without the plugin to restore the game's original assets. + +### 3. Verify and turn off diagnostics -Keep `Synchronize` when replacing incorrect existing glyphs or making accented and unaccented letters use the same replacement style. Use `Mode = Fallback` instead if you only need missing-character coverage: it retains the original glyphs and cannot replace incorrect ones. Both modes preserve existing fallback chains and leave unmatched fonts unchanged. Restart without the plugin to restore the game's original assets. +Restart the game. Check the log for a loaded profile and an `Applied` rule, then visually check the relevant screens. Successful log messages do not guarantee correct rendering or layout. -See [configuration](docs/configuration.md) for multiple profiles, matching modes, exclusions, and atlas settings. +Once verified, close the game, set `Diagnostics = false`, and restart. This stops discovered-font-name logging. With at least one ready profile, scanning and font changes continue; otherwise, game-font enumeration is skipped while diagnostics are off. Normal status messages, warnings, and errors remain enabled; there is no separate plugin debug setting, and you do not need to disable BepInEx logging globally. Re-enable diagnostics when troubleshooting or discovering more fonts. -**Synchronization clears the matched font's character table in memory.** Use narrow rules and check symbols, untranslated text, and layout. Restart to recover. Matching outlines, shadows, spacing, and line breaks is not guaranteed. +See [configuration](docs/configuration.md) for secondary font files, multiple profiles, matching modes, exclusions, and atlas settings. ## With XUnity.AutoTranslator @@ -112,6 +116,10 @@ Keep translation enabled. This plugin neither edits XUnity configuration nor rew To disable, set `Enabled = false` and restart. To uninstall, close the game and remove the plugin DLL. +## Development + +See the [development guide](docs/development.md) for setup, build/test commands, runtime design, and validation limits, and the [release guide](docs/release.md) for packaging and publication. + ## License -[MIT](LICENSE). The license covers this plugin and its documentation, not third-party fonts or game assemblies. Do not redistribute proprietary system fonts or game assets. +[MIT](LICENSE). diff --git a/docs/configuration.md b/docs/configuration.md index 40a99db..620ab35 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -15,6 +15,8 @@ File: `BepInEx/config/zeldrisho.UnityVietnameseFontSync.cfg`. Edit while the gam | Rules | Order | empty | Comma-separated rule IDs in first-match order | | Rules | Exclude | empty | Case-insensitive asset-name fragments excluded before all rules | +Enable `Diagnostics` during setup or troubleshooting to discover asset names, then set it to `false` and restart after verifying the relevant screens. This stops discovered-font-name logging. With at least one ready profile, scanning and font changes continue; otherwise, game-font enumeration is skipped while diagnostics are off. Normal status messages, warnings, and errors remain enabled. There is no separate plugin debug setting; leave BepInEx logging available for troubleshooting. + IDs contain letters, digits, underscores, or hyphens. Lists trim whitespace and remove exact duplicates. IDs/profile references are case-sensitive; asset matching is case-insensitive. Unlisted sections are inactive. The preset covers printable ASCII and uppercase/lowercase precomposed Vietnamese. Add curly quotes, ellipsis, combining marks, or other required characters explicitly. Game text is not normalized or rewritten. Invalid surrogate sequences are rejected. @@ -34,9 +36,20 @@ Relative paths search `/Fonts/` first, then the OS Fonts d Profiles must successfully add the entire preset plus custom characters across `File` and `FallbackFiles` combined. Each fallback file is preloaded with only the characters still missing from earlier files. Later files are not loaded once coverage is complete. All generated files use the profile's atlas settings; the primary asset links to the generated fallback assets in order. No links back to game fonts are added. -Example: `File = PrimaryFont.ttf` with `FallbackFiles = CoverageFont.ttf` keeps the primary style wherever available and uses the second font for missing characters. A character supplied by another font may look different. This does not guarantee combining-mark shaping. +For a profile needing secondary-file coverage: + +```ini +[Fonts] +Profiles = menu + +[Font.menu] +File = PrimaryFont.ttf +FallbackFiles = CoverageFont.ttf +``` + +Put both licensed files in the plugin's `Fonts/` directory, or use valid absolute paths. This keeps the primary style wherever available and uses the second font for missing characters. A character supplied by another font may look different. Leave `FallbackFiles` empty when the primary file covers the whole preset. This does not guarantee combining-mark shaping; matching rules are still required to apply the profile. -If coverage remains incomplete, a needed file is missing, or TMP cannot report which characters failed, the entire profile is disabled and mapped game fonts are left alone. Missing Unicode values are logged where TMP supplies them. Files are loaded once at startup; restart to retry after fixing a file. +If coverage remains incomplete, a needed file is missing, or TMP cannot report which characters failed, the entire profile is disabled and mapped game fonts are left alone. Missing Unicode values are logged where TMP supplies them. Files are loaded once when TMP becomes available; restart to retry after fixing a file. ## Rules: `[Rule.]` diff --git a/docs/development.md b/docs/development.md index e18e037..d0d7036 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,7 @@ # Development +For CI, packaging, and publication instructions, see the [release guide](release.md). + ## Toolchain and layout - Stable .NET 10 SDK, Git, and Python 3 for release packaging/tests. @@ -31,9 +33,9 @@ python3 -m unittest discover -s scripts -p 'test_*.py' To apply formatting, omit `--verify-no-changes`. Output: `src/bin/Release/netstandard2.1/UnityVietnameseFontSync.dll`. Deploy only that DLL, manually, to the game's plugin directory. Build/test commands never deploy. -The project originated from official [BepInEx.Templates](https://github.com/BepInEx/BepInEx.Templates). Stable 1.4.0 generation failed under SDK 10.0.112; documented template 2.0.0-be.4 worked. That scaffolding exception does not require a bleeding-edge runtime. Restore/build does not require templates to remain installed. +Dependencies use exact versions plus lock files. UnityEngine.Modules 2022.3.0 is an intentional compile-only compatibility baseline, not a required game version; blindly selecting the newest Unity package could introduce newer runtime API dependencies. No Unity/TMP/game DLL ships with the plugin. -Dependencies use exact versions plus lock files. BepInEx packages use the latest stable versions available in the configured feed when checked. UnityEngine.Modules 2022.3.0 is an intentional compile-only compatibility baseline, not a required game version; blindly selecting the newest Unity package could introduce newer runtime API dependencies. No Unity/TMP/game DLL ships with the plugin. `Version`, display name, and GUID live in `src/UnityVietnameseFontSync.csproj`; template tooling generates constants for the plugin attribute. The initial stable version is `1.0.0`, matching the .NET SDK default and using three-part SemVer. The SDK derives the assembly/file versions (`1.0.0.0` initially); no separate version constants are maintained. +Edit `Version`, display name, and GUID in `src/UnityVietnameseFontSync.csproj`, not the generated plugin constants. Use three-part SemVer; the SDK derives assembly/file versions. Restore/build does not require BepInEx templates to be installed. ## Runtime design @@ -67,6 +69,10 @@ The working starting point was C# run inside UnityExplorer, not an installed ver A texture PNG alone is not a portable TMP font asset: glyph mappings, metrics, and other font data are also required. Runtime-generated fonts are not exported by this plugin. Restarting clears manual in-memory changes; disable startup scripts before testing the plugin independently. +### Plugin log evidence + +A local Unity Mono plugin run logged successful preflight for two profiles, including secondary-file coverage for a character missing from the primary file, followed by successful Synchronize application to four matching game font assets. Discovery continued beyond startup. This confirms profile creation and rule application for that run, not visual correctness, broad runtime compatibility, or performance. Keep local game mappings, paths, and font files out of committed examples and runtime configuration. + ### Automated and in-game checks Unit tests exercise configuration, matching, paths, Unicode scalars, adapter capability checks, optional parameters, and failed glyph-preflight reporting. Packaging tests check version/tag validation, changelog requirements, the exact four-file layout, documentation links, deterministic archive contents, and rejection of XML DTDs/entities before output is created. These are not native rendering tests. @@ -88,35 +94,3 @@ Before claiming runtime support, manually test: - Startup, scenes, async loads, inactive text activation, retries, and long-session atlas growth. - XUnity coexistence with its competing TMP font settings empty. - Discovery/refresh timings and memory in multiple games. No rendering/performance claims are verified yet. - -## CI and release - -Workflows use minimal permissions, timeouts, concurrency controls, and action SHA pins with stable version comments. Review dependency updates manually and retain lock files. Review the Unity compatibility baseline rather than blindly accepting engine upgrades. - -CI restores in locked mode, checks formatting, builds, runs tests, checks packaging logic, and uploads only the plugin DLL. Both CI and release packaging install the pinned, hash-verified dependency from `scripts/requirements.txt`. The packager uses `defusedxml` to reject DTDs, entities, and external references when reading project metadata; this build-time dependency is not included in the plugin archive. It does not need Steam or private reference assemblies. - -To release: - -1. Finish manual validation and review the MIT license and any third-party redistribution rights. -2. Set the project `Version`; move relevant user-facing `Unreleased` changes to `## [X.Y.Z] - YYYY-MM-DD` in `CHANGELOG.md` and retain `Unreleased` above it. Keep developer instructions, CI changes, and test implementation details out of the changelog. -3. Validate packaging locally, commit, and push a matching stable `vX.Y.Z` tag only when ready to publish. -4. The release workflow reruns CI, validates tag/version/changelog consistency, creates the allowlisted ZIP, and publishes a normal GitHub release automatically. **Tagging is the publication trigger; there is no draft/manual-publish step.** - -Local packaging after adding the dated release entry: - -```bash -python3 scripts/package.py --tag vX.Y.Z --dll src/bin/Release/netstandard2.1/UnityVietnameseFontSync.dll -``` - -The initial archive is `UnityVietnameseFontSync-1.0.0.zip`; future archives retain the `UnityVietnameseFontSync-X.Y.Z.zip` pattern. It contains exactly: - -```text -plugins/zeldrisho.UnityVietnameseFontSync/UnityVietnameseFontSync.dll -plugins/zeldrisho.UnityVietnameseFontSync/README.md -plugins/zeldrisho.UnityVietnameseFontSync/CHANGELOG.md -plugins/zeldrisho.UnityVietnameseFontSync/LICENSE -``` - -Extract into `/BepInEx/`, not the game root. The install directory is derived from the project GUID; the DLL name remains the assembly name. Guides stay in the repository. During GitHub packaging, README guide links point to that repository's release tag; local packages without GitHub metadata use plain-text documentation references instead of broken local links. - -The ZIP is the only attached release asset. The generated `notes.md` becomes the GitHub release body, not an archive entry. Separate checksum files are optional and omitted for this small distribution. No fonts, game/BepInEx/Unity DLLs, generated config, debug symbols, or internal planning notes are included. Users supply fonts and generate per-game configuration on first launch. Comparison links can be added once the repository remote and first release exist. diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..b6ca178 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,46 @@ +# Release + +See the [development guide](development.md) for toolchain setup, build/test commands, runtime design, and the manual validation checklist. + +## CI + +Workflows use minimal permissions, timeouts, concurrency controls, and action SHA pins with stable version comments. Review dependency updates manually and retain lock files. Review the Unity compatibility baseline rather than blindly accepting engine upgrades. + +CI restores in locked mode, checks formatting, builds, runs tests, checks packaging logic, and uploads only the plugin DLL. Both CI and release packaging install the pinned, hash-verified dependency from `scripts/requirements.txt`. The packager uses `defusedxml` to reject DTDs, entities, and external references when reading project metadata; this build-time dependency is not included in the plugin archive. It does not need Steam or private reference assemblies. + +## Publish a release + +1. Finish manual validation and review the MIT license and any third-party redistribution rights. +2. Set the project `Version`; move relevant user-facing `Unreleased` changes to `## [X.Y.Z] - YYYY-MM-DD` in `CHANGELOG.md` and retain `Unreleased` above it. Keep developer instructions, CI changes, and test implementation details out of the changelog. +3. Validate packaging locally, commit, and push a matching stable `vX.Y.Z` tag only when ready to publish. +4. The release workflow reruns CI, validates tag/version/changelog consistency, creates the allowlisted ZIP, and publishes a normal GitHub release automatically. **Tagging is the publication trigger; there is no draft/manual-publish step.** + +Run local packaging from the repository root, with the Python environment activated, after building the DLL and adding the dated release entry: + +```bash +python3 scripts/package.py --tag vX.Y.Z --dll src/bin/Release/netstandard2.1/UnityVietnameseFontSync.dll +``` + +## Archive layout and documentation links + +Archives use the `UnityVietnameseFontSync-X.Y.Z.zip` pattern and contain exactly: + +```text +plugins/zeldrisho.UnityVietnameseFontSync/UnityVietnameseFontSync.dll +plugins/zeldrisho.UnityVietnameseFontSync/README.md +plugins/zeldrisho.UnityVietnameseFontSync/CHANGELOG.md +plugins/zeldrisho.UnityVietnameseFontSync/LICENSE +``` + +Extract into `/BepInEx/`, not the game root. The install directory is derived from the project GUID; the DLL name remains the assembly name. Guides stay in the repository. During GitHub packaging, README guide links point to that repository's release tag; local packages without GitHub metadata use plain-text documentation references instead of broken local links. + +To verify hosted guide links in a local package, supply the repository metadata: + +```bash +GITHUB_REPOSITORY=zeldrisho/UnityVietnameseFontSync \ + python3 scripts/package.py --tag vX.Y.Z --dll src/bin/Release/netstandard2.1/UnityVietnameseFontSync.dll +``` + +This only creates local packaging output; it does not publish a release. + +The ZIP is the only attached release asset. The generated `notes.md` becomes the GitHub release body, not an archive entry. Separate checksum files are optional and omitted for this small distribution. No fonts, game/BepInEx/Unity DLLs, generated config, debug symbols, or internal planning notes are included. Users supply fonts and generate per-game configuration on first launch. diff --git a/scripts/test_package.py b/scripts/test_package.py index 461a123..f12a8e7 100644 --- a/scripts/test_package.py +++ b/scripts/test_package.py @@ -55,9 +55,14 @@ def test_exact_layout_version_and_reproducibility(self): "zeldrisho.UnityVietnameseFontSync" "") (root / "CHANGELOG.md").write_text("## [1.0.0] - 2026-10-15\n### Added\n- Example.\n") - for name in ("README.md", "LICENSE", "docs/configuration.md", "docs/development.md"): + guides = ("configuration", "development", "release") + for name in ("LICENSE", *(f"docs/{guide}.md" for guide in guides)): (root / name).write_text("example") - (root / "README.md").write_text("See [configuration](docs/configuration.md).") + # Exercise the shipped README, including every user-facing guide link. + readme = (Path(__file__).resolve().parents[1] / "README.md").read_text(encoding="utf-8") + for guide in guides: + self.assertIn(f"](docs/{guide}.md)", readme) + (root / "README.md").write_text(readme, encoding="utf-8") (root / "private.ttf").write_text("must not ship") dll = root / "UnityVietnameseFontSync.dll" dll.write_bytes(b"test fixture") @@ -70,12 +75,21 @@ def test_exact_layout_version_and_reproducibility(self): self.assertEqual(set(bundle.namelist()), { prefix + name for name in ("UnityVietnameseFontSync.dll", "README.md", "CHANGELOG.md", "LICENSE") }) - self.assertNotIn(b"docs/configuration.md", bundle.read(prefix + "README.md")) + bundled_readme = bundle.read(prefix + "README.md").decode("utf-8") + for guide in guides: + self.assertNotIn(f"docs/{guide}.md", bundled_readme) + self.assertIn("configuration (see the project repository)", bundled_readme) + self.assertIn("development guide (see the project repository)", bundled_readme) + self.assertIn("release guide (see the project repository)", bundled_readme) self.assertEqual({file.name for file in first.parent.iterdir()}, {first.name, "notes.md"}) hosted = package(root, "v1.0.0", dll, root / "hosted", "https://github.com/example/project") with zipfile.ZipFile(hosted) as bundle: - self.assertIn(b"https://github.com/example/project/blob/v1.0.0/docs/configuration.md", - bundle.read(prefix + "README.md")) + bundled_readme = bundle.read(prefix + "README.md").decode("utf-8") + for guide in guides: + self.assertIn( + f"](https://github.com/example/project/blob/v1.0.0/docs/{guide}.md)", + bundled_readme) + self.assertNotIn(f"](docs/{guide}.md)", bundled_readme) for tag in ("v1.0.1", "v1.0.0-beta", "../../evil", "v01.0.0"): with self.assertRaises(ValueError): package(root, tag, dll, root / "bad")