mirror of
https://github.com/HayatoG/tlg_pc.git
synced 2026-11-07 10:46:09 -10:00
Merge PR #141
This commit is contained in:
commit
a5c24ecc49
12 files changed
+388
-12
No files matched your search
@@ -164,6 +164,7 @@ The screenshots and the performance chart are at the top of this page.
|
||||
- **60 FPS and unlocked frame rate (experimental):** launcher → Video → "Frame rate (experimental)", or `TLG_FPS=30|60|unlocked`. The default is still 30 FPS, as on PS4. Details in [`docs/60fps.md`](docs/60fps.md) (in Portuguese, [#5](https://github.com/HayatoG/tlg_pc/issues/5)).
|
||||
- **Image filters:** SMAA anti-aliasing, RCAS (FSR 1) or CAS (AMD) sharpening and colour adjustments (brightness, contrast, saturation, gamma), in the launcher and live in the in-game menu (Insert → Image) ([#2](https://github.com/HayatoG/tlg_pc/issues/2)).
|
||||
- **Mouse and keyboard:** the mouse turns the camera (sensitivity and inverted Y in the launcher), and keys and mouse buttons can be remapped ([#8](https://github.com/HayatoG/tlg_pc/issues/8)).
|
||||
- **Japanese text (experimental):** on EU CUSA03745 update 01.03, choose **日本語 (EU 1.03 text fallback)** in Game language. The port handles the embedded-asset fallback without moving or deleting archives. See [setup and limitations](docs/japanese.md); this is not Japanese-region disc support.
|
||||
- **Motion sensor:** the DualShock 4/DualSense gyro works as is. On controllers without one, a tap on Share (or L3+R3) toggles "sensor mode": the left stick tilts the controller and cross/A shakes it. On the keyboard, T/G tilt and Y shakes.
|
||||
- **PS4-like brightness and lighting:** the game's auto exposure now reads the right value from the GPU, so outdoor scenes no longer blow out to white (`TLG_READBACK_LINEAR=1`, on by default; `0` restores the old behaviour) ([#38](https://github.com/HayatoG/tlg_pc/issues/38), [#40](https://github.com/HayatoG/tlg_pc/issues/40)).
|
||||
- **Direct memory access (shaders):** new launcher option (Graphics → GPU), **off by default** (`TLG_DIRECT_MEMORY_ACCESS=1`). Shaders read constants straight from the game's memory: fixes some broken scenes, but the game runs slower. Its shaders live in a cache of their own and are compiled again the first time.
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Japanese text on the EU build
|
||||
|
||||
## What this supports
|
||||
|
||||
The verified setup is **EU CUSA03745 with update 01.03 installed/merged into the game
|
||||
folder**, displaying Japanese assets already included in the shared boot archives.
|
||||
This is **not support for the Japanese-region disc executable**. US, Korean, Chinese,
|
||||
other updates and modified asset sets have not been validated for this fallback.
|
||||
Japanese menus and gameplay prompts have been seen in-game; full-game text, narration
|
||||
and audio coverage have **not** been exhaustively verified.
|
||||
|
||||
The launcher option and automatic handling below require **both a launcher and native
|
||||
runtime built with this change**. Do not copy only the new launcher over an old runtime:
|
||||
the old runtime will not know how to hide the French packs.
|
||||
|
||||
## Setup — no archive editing or moving required
|
||||
|
||||
1. Use your own EU game dump with its **01.03 update applied to the same game folder**.
|
||||
An update PKG merely stored beside the base game is not an installed update.
|
||||
2. In the launcher, select the folder containing `eboot.bin` and `sce_sys/param.sfo`.
|
||||
Keep the normal game files, including `trico_boot.psarc`, `trico_bootp.psarc`,
|
||||
`trico_frf.psarc` and `trico_frfp.psarc`. Do not rename, delete or edit them.
|
||||
3. In **Game language**, choose **日本語 (EU 1.03 text fallback)**, then **Play**.
|
||||
Both the Qt and classic Tk launchers expose this choice for the verified asset layout.
|
||||
|
||||
The port automatically uses the appropriate internal language id and hides the two
|
||||
French localization packs **only from the game's view for that launch**. Their bytes
|
||||
and names on disk remain unchanged. No extra language download, save conversion, package
|
||||
modification, region spoof or custom launcher is required.
|
||||
|
||||
To return to French, select **Français** and launch again. Other languages also disable
|
||||
the temporary mask automatically. Japanese text is opt-in: **Automatic** does not quietly
|
||||
select this experimental fallback just because the operating system is Japanese.
|
||||
|
||||
This does not swap controller buttons. Japanese-style O-confirm/X-back is a separate
|
||||
menu preference/change ([PR #140](https://github.com/HayatoG/tlg_pc/pull/140)); selecting
|
||||
Japanese text alone does not enable it.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **The option is missing:** verify that the launcher/runtime contain this change, the
|
||||
game folder is EU `CUSA03745`, `APP_VER` is `01.03`, and both boot archives are present.
|
||||
The launcher does not advertise this workaround for unverified regions or versions.
|
||||
- **A previously saved Japanese choice is shown for a different game folder:** that
|
||||
choice is preserved, but the fallback is disabled and the game uses English. The Qt
|
||||
launcher displays an explanation; this does not enable Japanese-region disc support.
|
||||
- **It still shows French:** ensure the native runtime was rebuilt too. Remove a manual
|
||||
`TLG_LANGUAGE` override that conflicts with the launcher selection. The normal launch
|
||||
log should report `Japanese text fallback: French root archives hidden from guest`.
|
||||
- **It shows English with a forced language id:** setting `TLG_LANGUAGE=0` is not this
|
||||
workaround; that fell back to English in the tested EU build. Let the launcher select
|
||||
the internal id instead.
|
||||
- **You already moved the French packs for the old workaround:** the option also works
|
||||
without them, but French cannot be restored by changing the language until you put
|
||||
your original packs back. Never replace game archives from an unknown source.
|
||||
|
||||
Saves stay under the same EU title id and user folder. This feature does not rewrite
|
||||
saves, game archives, metadata or the game executable. Backups before each launch still
|
||||
follow the port's normal save-backup policy.
|
||||
|
||||
## Why the code is needed
|
||||
|
||||
Japanese paths remain in the shared boot archives, but the EU launcher's normal language
|
||||
table does not expose them. The working fallback requests the internal French language
|
||||
id (`2`) while making the France-French packs unavailable. With those packs absent, the
|
||||
game falls back to its embedded Japanese assets. Simply requesting native Japanese id
|
||||
`0` did not produce the same result.
|
||||
|
||||
Previously a player had to select the misleading **French** option and manually move
|
||||
`trico_frf.psarc` and `trico_frfp.psarc`, then restore them to play in French. The new
|
||||
launcher explicitly names the Japanese fallback and sets `TLG_JAPANESE_FALLBACK=1`
|
||||
together with the internal id. `runtime_file.c` returns `ENOENT` for those two root
|
||||
archives in `/app0`, `/hostapp` and equivalent relative paths, and omits them from guest
|
||||
directory listings. It does not hide similarly named files in saves or nested folders,
|
||||
other language packs, or any host files from other processes.
|
||||
|
||||
Switching away clears the mask, even if the launcher's parent environment contained a
|
||||
stale value. An explicit incompatible `TLG_LANGUAGE` override wins and disables the mask;
|
||||
remove that debugging override to use the launcher option. Direct native-runtime users
|
||||
must supply both `TLG_LANGUAGE=2` and `TLG_JAPANESE_FALLBACK=1` on the verified build;
|
||||
the native flag alone does not identify or validate a disc region.
|
||||
|
||||
## Validation and limits
|
||||
|
||||
The earlier manual archive-omission setup was tested in-game on EU 01.03 and produced
|
||||
Japanese menus/gameplay prompts. Its actual kernel opens were `/app0/trico_frf.psarc`
|
||||
and `/app0/trico_frfp.psarc`, the paths now masked by this change.
|
||||
|
||||
Automated C tests exercise real guest open/stat/list behavior with French packs still
|
||||
present on disk, check relative and hostapp aliases, ensure saves/nested files and other
|
||||
packs remain readable, and verify French is readable again when the option is off.
|
||||
Python tests cover version/region/layout gating, UI choices, language-id/mask pairing,
|
||||
normal French, stale environment cleanup, explicit overrides and non-mutation.
|
||||
|
||||
A full newly rebuilt launcher/runtime still needs end-to-end in-game regression testing.
|
||||
The previous manual proof is not presented as testing of a rebuilt version of this PR.
|
||||
@@ -104,6 +104,8 @@ if [[ ${1:-} == --test ]]; then
|
||||
out/runtime-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_file_mods.c -o out/file-mods-test
|
||||
out/file-mods-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_japanese_language.c -o out/japanese-language-test
|
||||
out/japanese-language-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_sema.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/sema-test
|
||||
out/sema-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_rwlock_rp.c -o out/rwlock-rp-test
|
||||
|
||||
@@ -625,6 +625,10 @@ class Backend(QObject):
|
||||
@Property(str, notify=gameLanguageChanged)
|
||||
def gameLanguageNow(self):
|
||||
"""'Automatic: Português (Brasil)' while 'auto' is active, else empty."""
|
||||
if self.settings.get('game_language') == 'ja':
|
||||
return self.t_('game_language_japanese_fallback' if
|
||||
tlg_run.japanese_fallback_available(self.settings.get('game_dir')) else
|
||||
'game_language_japanese_unavailable')
|
||||
if self.settings.get('game_language', 'auto') in tlg_run.GAME_LANGUAGES:
|
||||
return ''
|
||||
return self.t_('game_language_now', name=logic.GAME_LANGUAGE_NAMES[tlg_run.resolve_game_language(self.settings)])
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"game_language_label": {"en": "Game language", "pt": "Idioma do jogo"},
|
||||
"game_language_auto": {"en": "Automatic (system language)", "pt": "Automático (idioma do sistema)"},
|
||||
"game_language_japanese_fallback": {"en": "Experimental Japanese text from embedded EU 1.03 assets. French archives stay on disk and are hidden only from this launch. Full-game audio/text coverage is not yet verified.", "pt": "Texto japonês experimental dos arquivos europeus 1.03. Os arquivos franceses ficam no disco e são ocultados apenas nesta execução. A cobertura de áudio/texto do jogo inteiro ainda não foi verificada."},
|
||||
"game_language_japanese_unavailable": {"en": "Japanese text fallback requires EU CUSA03745 update 01.03 and both boot archives. This game folder will use English instead. This does not add Japanese-region disc support.", "pt": "O texto japonês exige CUSA03745 europeu com atualização 01.03 e os dois arquivos boot. Esta pasta usará inglês. Isso não adiciona suporte à edição japonesa do disco."},
|
||||
"game_language_now": {"en": "Automatic: {name}", "pt": "Automático: {name}"},
|
||||
"game_language_hint": {"en": "Applies on the next start. Works if your game files have that language.", "pt": "Vale na próxima partida. Funciona se os arquivos do seu jogo tiverem esse idioma."},
|
||||
"nav_play": {"en": "Play", "pt": "Jogar"},
|
||||
|
||||
@@ -30,7 +30,7 @@ GAME_LANGUAGE_NAMES = {'en': 'English', 'en_GB': 'English (UK)', 'pt_BR': 'Portu
|
||||
'fr_CA': 'Français (Canada)', 'fr': 'Français', 'de': 'Deutsch', 'it': 'Italiano',
|
||||
'nl': 'Nederlands', 'pl': 'Polski', 'tr': 'Türkçe', 'ru': 'Русский', 'cs': 'Čeština',
|
||||
'hu': 'Magyar', 'el': 'Ελληνικά', 'ro': 'Română', 'fi': 'Suomi', 'sv': 'Svenska', 'da': 'Dansk',
|
||||
'no': 'Norsk', 'ar': 'العربية', 'ja': '日本語', 'ko': '한국어', 'zh_TW': '繁體中文',
|
||||
'no': 'Norsk', 'ar': 'العربية', 'ja': tlg_run.JAPANESE_FALLBACK_LABEL, 'ko': '한국어', 'zh_TW': '繁體中文',
|
||||
'zh_CN': '简体中文', 'th': 'ไทย', 'vi': 'Tiếng Việt', 'id': 'Bahasa Indonesia'}
|
||||
|
||||
|
||||
|
||||
@@ -189,7 +189,8 @@ COFFEE_URL = 'https://buymeacoffee.com/hayatog'
|
||||
CHANNEL_URL = 'https://www.youtube.com/@hayatog-dev'
|
||||
DISCORD_URL = 'https://discord.gg/UMkdMYZh9g'
|
||||
SPONSOR_URL = 'https://github.com/sponsors/HayatoG'
|
||||
GAME_LANGUAGE_NAMES = {'en': 'English', 'fr': 'Français', 'es_ES': 'Español (España)',
|
||||
GAME_LANGUAGE_NAMES = {'ja': tlg_run.JAPANESE_FALLBACK_LABEL,
|
||||
'en': 'English', 'fr': 'Français', 'es_ES': 'Español (España)',
|
||||
'de': 'Deutsch', 'it': 'Italiano', 'nl': 'Nederlands',
|
||||
'pt_PT': 'Português (Portugal)', 'ru': 'Русский', 'fi': 'Suomi',
|
||||
'sv': 'Svenska', 'da': 'Dansk', 'no': 'Norsk', 'pl': 'Polski',
|
||||
@@ -527,7 +528,7 @@ class Launcher:
|
||||
"""Reload the combo for the game folder: another CUSA ships another set of languages."""
|
||||
if getattr(self, 'game_lang_box', None) is None:
|
||||
return
|
||||
values = list(tlg_run.game_languages(self.settings.get('game_dir')))
|
||||
values = [key for key in tlg_run.game_language_choices(self.settings) if key != 'auto']
|
||||
self.game_lang_values = values
|
||||
self.game_lang_box.configure(values=[GAME_LANGUAGE_NAMES.get(v, v) for v in values])
|
||||
current = self.settings.get('game_language')
|
||||
|
||||
@@ -17,6 +17,7 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -85,8 +86,23 @@ def default_language(game_dir=''):
|
||||
return DEFAULT_LANGUAGE_BY_TITLE.get(title, 'es_LA')
|
||||
|
||||
|
||||
# Every language key either disc has (the Qt launcher's names table covers them all).
|
||||
GAME_LANGUAGES = {**LANGUAGES_US, **LANGUAGES_EU, **LANGUAGES_ASIA}
|
||||
JAPANESE_FALLBACK_LABEL = '日本語 (EU 1.03 text fallback)'
|
||||
|
||||
|
||||
def japanese_fallback_available(game_dir):
|
||||
"""Only the verified EU 01.03 asset layout; not Japanese-region disc support."""
|
||||
if not game_dir or shader_cache.title_id(game_dir) != 'CUSA03745':
|
||||
return False
|
||||
try:
|
||||
return (mod_patches.game_app_version(game_dir) == '01.03' and
|
||||
all((Path(game_dir) / name).is_file()
|
||||
for name in ('trico_boot.psarc', 'trico_bootp.psarc')))
|
||||
except (OSError, ValueError, struct.error):
|
||||
return False
|
||||
|
||||
|
||||
# 'ja' is an explicit embedded-asset fallback, not native engine language 0.
|
||||
GAME_LANGUAGES = {**LANGUAGES_US, **LANGUAGES_EU, **LANGUAGES_ASIA, 'ja': 2}
|
||||
# The closest choice when the system's language is missing from the disc.
|
||||
_LANGUAGE_SIBLING = {'pt_BR': 'pt_PT', 'pt_PT': 'pt_BR', 'es_LA': 'es_ES', 'es_ES': 'es_LA',
|
||||
'fr_CA': 'fr', 'fr': 'fr_CA'}
|
||||
@@ -127,6 +143,8 @@ def game_language_choices(settings):
|
||||
"""The game language box of the Qt launcher: 'auto', the languages the disc has and the saved
|
||||
choice even when the disc lacks it (never changed behind the player's back)."""
|
||||
keys = ['auto'] + list(game_languages(settings.get('game_dir', '')))
|
||||
if japanese_fallback_available(settings.get('game_dir', '')):
|
||||
keys.append('ja')
|
||||
choice = settings.get('game_language', 'auto')
|
||||
return keys + [choice] if choice in GAME_LANGUAGES and choice not in keys else keys
|
||||
|
||||
@@ -136,6 +154,8 @@ def resolve_game_language(settings, environ=None):
|
||||
system (or its closest variant); anything else is English, as before."""
|
||||
table = game_languages(settings.get('game_dir', ''))
|
||||
choice = settings.get('game_language', 'auto')
|
||||
if choice == 'ja' and japanese_fallback_available(settings.get('game_dir', '')):
|
||||
return 'ja'
|
||||
if choice in table:
|
||||
return choice
|
||||
if choice == 'auto':
|
||||
@@ -145,6 +165,21 @@ def resolve_game_language(settings, environ=None):
|
||||
return key
|
||||
return 'en'
|
||||
|
||||
|
||||
def game_language_environment(settings, environ=None):
|
||||
"""Select the internal French id plus a non-destructive guest archive mask for Japanese.
|
||||
|
||||
Explicit engine-language overrides still win; a different override disables the mask.
|
||||
Never inherit a stale mask from a previous launch when another language is selected.
|
||||
"""
|
||||
environ = os.environ if environ is None else environ
|
||||
language = resolve_game_language(settings, environ)
|
||||
engine_id = 2 if language == 'ja' else game_languages(settings.get('game_dir')).get(language, 1)
|
||||
selected = environ.get('TLG_LANGUAGE', str(engine_id))
|
||||
return {'TLG_LANGUAGE': selected,
|
||||
'TLG_JAPANESE_FALLBACK': '1' if language == 'ja' and selected == '2' else '0'}
|
||||
|
||||
|
||||
# TLG defaults (see README "Opções" and the reasons in each comment).
|
||||
DEFAULT_ENV = {
|
||||
'TLG_PIPELINE_BINARIES': '0', # VK_KHR_pipeline_binary crashes on the NVIDIA 595 driver
|
||||
@@ -633,8 +668,7 @@ def build_env(settings, extra=None):
|
||||
env['TLG_MENU_CONFIRM'] = menu_confirm(settings)
|
||||
env['TLG_STICK_DEADZONE'] = os.environ.get(
|
||||
'TLG_STICK_DEADZONE', f"{int(settings.get('stick_deadzone', 10)) / 100:.2f}")
|
||||
env['TLG_LANGUAGE'] = os.environ.get('TLG_LANGUAGE', str(
|
||||
game_languages(settings.get('game_dir')).get(resolve_game_language(settings), 1)))
|
||||
env.update(game_language_environment(settings))
|
||||
if settings.get('ui_language', 'auto') != 'auto':
|
||||
env['TLG_UI_LANG'] = settings['ui_language']
|
||||
# Without the console module the port's own mixer (runtime_ngs2.c) plays the audio.
|
||||
|
||||
@@ -40,7 +40,9 @@ Tudo vem do **seu** PS4 (GoldHEN + Itemzflow). Nada disso vai no download. Use *
|
||||
3. Abra **The Last Guardian** (`The Last Guardian.exe` no Windows): é o launcher novo (Qt), com telas de Gráficos, Controles, Correções, Saves, Mods, Suporte e Atualizações. O launcher antigo continua ao lado como **The Last Guardian (classic)**. Por padrão o launcher fecha quando o jogo abre e volta quando o jogo fecha (dá para mudar em Gráficos → Áudio e launcher).
|
||||
- No Windows, se aparecer "O Windows protegeu o computador", clique em *Mais informações* e depois em *Executar assim mesmo*. O programa não é assinado digitalmente.
|
||||
4. Em **Pasta do jogo**, escolha a pasta que tem o `eboot.bin`.
|
||||
5. Escolha o idioma do jogo (inglês, português, espanhol ou francês) e clique em **Jogar**. A primeira partida demora um pouco mais.
|
||||
5. Escolha um dos idiomas oferecidos para sua pasta do jogo e clique em **Jogar**. A primeira partida demora um pouco mais.
|
||||
|
||||
**Texto japonês experimental (EU 01.03):** com CUSA03745 e a atualização 01.03 aplicada na mesma pasta, escolha **日本語 (EU 1.03 text fallback)**. Não mova nem apague os arquivos franceses: o port os oculta apenas da visão do jogo nesta execução. Para voltar ao francês, escolha Français e inicie novamente. É necessário um launcher e runtime que incluam esse recurso; isso não adiciona suporte ao executável da edição japonesa do disco. Menus e dicas de jogo em japonês foram verificados; áudio/texto do jogo inteiro ainda não foram testados. Os botões de controle não são trocados.
|
||||
|
||||
No Linux, `./install_desktop.sh` adiciona o jogo ao menu de aplicativos.
|
||||
|
||||
@@ -119,7 +121,9 @@ Everything comes from **your own** PS4 (GoldHEN + Itemzflow). None of it is in t
|
||||
3. Open **The Last Guardian** (`The Last Guardian.exe` on Windows): the new (Qt) launcher, with Graphics, Controls, Fixes, Saves, Mods, Support and Updates screens. The old launcher is still next to it as **The Last Guardian (classic)**. By default the launcher closes when the game starts and comes back when the game closes (change it in Graphics → Audio and launcher).
|
||||
- On Windows, if "Windows protected your PC" appears, click *More info*, then *Run anyway*. The program is not digitally signed.
|
||||
4. Under **Game folder**, choose the folder that has `eboot.bin`.
|
||||
5. Pick the game language (English, Portuguese, Spanish or French) and click **Play**. The first start takes a little longer.
|
||||
5. Pick one of the languages offered for your game folder and click **Play**. The first start takes a little longer.
|
||||
|
||||
**Experimental Japanese text (EU 01.03):** with CUSA03745 and update 01.03 applied to the same folder, select **日本語 (EU 1.03 text fallback)**. Do not move or delete the French archives: the port hides them only from the game's view for that launch. To return to French, select Français and launch again. Both launcher and runtime must contain this feature; it does not add Japanese-region disc executable support. Japanese menus and gameplay prompts were verified, not full-game audio/text coverage. Controller buttons are not swapped.
|
||||
|
||||
On Linux, `./install_desktop.sh` adds the game to the applications menu.
|
||||
|
||||
|
||||
+38
-3
@@ -90,6 +90,9 @@ void runtime_file_configure(const char *app0,const char *user) {
|
||||
snprintf(user_root,sizeof(user_root),"%s",user);
|
||||
runtime_file_mount("/app0",app0);
|
||||
runtime_file_mount("/hostapp",app0);
|
||||
const char *japanese=getenv("TLG_JAPANESE_FALLBACK");
|
||||
if (japanese && !strcmp(japanese,"1"))
|
||||
puts("Runtime: Japanese text fallback: French root archives hidden from guest; host files unchanged");
|
||||
const char *writable[]={"temp0","download0","data"};
|
||||
mkdir(user,0755);
|
||||
for (int i=0;i<3;++i) {
|
||||
@@ -99,6 +102,34 @@ void runtime_file_configure(const char *app0,const char *user) {
|
||||
runtime_file_mount(guest,path);
|
||||
}
|
||||
}
|
||||
/* Opt-in EU Japanese-text fallback: hide only the root French localization archives
|
||||
* from the guest. Never move/delete host files or affect saves/other language packs. */
|
||||
static int language_archive_name(const char *name,const char *expected) {
|
||||
for (;*name && *expected;++name,++expected) {
|
||||
unsigned char c=(unsigned char)*name;
|
||||
if (c>='A' && c<='Z') c+=(unsigned char)('a'-'A');
|
||||
if (c!=(unsigned char)*expected) return 0;
|
||||
}
|
||||
return !*name && !*expected;
|
||||
}
|
||||
static int japanese_hidden_archive(const char *guest) {
|
||||
const char *enabled=getenv("TLG_JAPANESE_FALLBACK");
|
||||
if (!enabled || strcmp(enabled,"1") || !guest) return 0;
|
||||
const char *p=guest;
|
||||
if (*p=='/') {
|
||||
if (!strncmp(p,"/app0/",6)) p+=6;
|
||||
else if (!strncmp(p,"/hostapp/",9)) p+=9;
|
||||
else return 0;
|
||||
}
|
||||
for (;;) {
|
||||
while (*p=='/') ++p;
|
||||
if (p[0]=='.' && p[1]=='/') { p+=2; continue; }
|
||||
break;
|
||||
}
|
||||
return language_archive_name(p,"trico_frf.psarc") ||
|
||||
language_archive_name(p,"trico_frfp.psarc");
|
||||
}
|
||||
|
||||
/* Resolve a guest path to a host path; returns 0 or a host errno. */
|
||||
static int translate(const char *guest,char *out,size_t size) {
|
||||
if (!guest || !*guest) return ENOENT;
|
||||
@@ -107,6 +138,7 @@ static int translate(const char *guest,char *out,size_t size) {
|
||||
else snprintf(buffer,sizeof(buffer),"%s",guest);
|
||||
for (const char *p=buffer;(p=strstr(p,".."));p+=2)
|
||||
if ((p==buffer || p[-1]=='/') && (p[2]==0 || p[2]=='/')) return EACCES;
|
||||
if (japanese_hidden_archive(buffer)) return ENOENT;
|
||||
pthread_mutex_lock(&lock);
|
||||
size_t best=0; const Mount *m=NULL;
|
||||
for (size_t i=0;i<mount_count;++i) {
|
||||
@@ -136,13 +168,16 @@ static int host_flags(int flags) {
|
||||
return r|O_CLOEXEC;
|
||||
}
|
||||
static void free_listing(Listing *l) { if (l) { free(l->names); free(l->offsets); free(l->types); free(l); } }
|
||||
static Listing *list_directory(const char *path) {
|
||||
static Listing *list_directory(const char *path,const char *guest) {
|
||||
DIR *d=opendir(path);
|
||||
if (!d) return NULL;
|
||||
Listing *l=calloc(1,sizeof(*l));
|
||||
size_t capacity=0,bytes=0,cap_names=0;
|
||||
struct dirent *e;
|
||||
while (l && (e=readdir(d))) {
|
||||
char child_guest[1400];
|
||||
if ((size_t)snprintf(child_guest,sizeof(child_guest),"%s/%s",guest,e->d_name)<sizeof(child_guest) &&
|
||||
japanese_hidden_archive(child_guest)) continue;
|
||||
size_t n=strlen(e->d_name)+1;
|
||||
if (l->count==capacity) {
|
||||
capacity=capacity ? capacity*2 : 64;
|
||||
@@ -227,7 +262,7 @@ static int64_t do_open(const char *guest,int flags,int mode) {
|
||||
int host=-1;
|
||||
if (!host_stat(path,&s) && S_ISDIR(s.st_mode)) {
|
||||
if (flags&3) return -EISDIR;
|
||||
if (!(dir=list_directory(path))) return -EACCES;
|
||||
if (!(dir=list_directory(path,guest))) return -EACCES;
|
||||
} else if (flags&0x20000) {
|
||||
e=errno==ENOENT ? ENOENT : ENOTDIR;
|
||||
if (e==ENOENT) { ++missing; printf("Runtime: open(%s) -> not found\n",guest); }
|
||||
@@ -252,7 +287,7 @@ static int64_t do_open(const char *guest,int flags,int mode) {
|
||||
}
|
||||
struct stat s;
|
||||
Listing *dir=NULL;
|
||||
if (!fstat(host,&s) && S_ISDIR(s.st_mode)) dir=list_directory(path);
|
||||
if (!fstat(host,&s) && S_ISDIR(s.st_mode)) dir=list_directory(path,guest);
|
||||
#endif
|
||||
pthread_mutex_lock(&lock);
|
||||
int fd=-1;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/* The Japanese-text view must look like missing French packs, without touching host files. */
|
||||
#undef NDEBUG
|
||||
#include "../src/runtime_file.c"
|
||||
#include <assert.h>
|
||||
|
||||
static int32_t guest_errno;
|
||||
int32_t *runtime_errno(void) { return &guest_errno; }
|
||||
int32_t runtime_guest_errno(int e) { return e; }
|
||||
void runtime_wait_begin(const char *kind,const void *object,const char *name,const void *const *owner,uintptr_t frame) {
|
||||
(void)kind; (void)object; (void)name; (void)owner; (void)frame;
|
||||
}
|
||||
void runtime_wait_end(void) {}
|
||||
uintptr_t runtime_lookup(const RuntimeExport *table,size_t count,const char *name) {
|
||||
(void)table; (void)count; (void)name; return 0;
|
||||
}
|
||||
static void make_file(const char *path) {
|
||||
FILE *f=fopen(path,"wb"); assert(f);
|
||||
assert(fputs("unchanged archive",f)>=0 && !fclose(f));
|
||||
}
|
||||
static void readable(const char *guest) {
|
||||
int fd=(int)do_open(guest,0,0); assert(fd>=3);
|
||||
char data[32]={0}; assert(do_read(fd,data,17)==17);
|
||||
assert(!strcmp(data,"unchanged archive") && !do_close(fd));
|
||||
}
|
||||
static int listed(const char *guest,const char *name) {
|
||||
int fd=(int)do_open(guest,0x20000,0); assert(fd>=3);
|
||||
char entries[4096]; int64_t count=do_getdents(fd,entries,sizeof(entries),NULL); assert(count>0);
|
||||
int found=0;
|
||||
for (int64_t at=0;at<count;) {
|
||||
uint16_t length; memcpy(&length,entries+at+4,2); assert(length);
|
||||
if (!strcmp(entries+at+8,name)) found=1;
|
||||
at+=length;
|
||||
}
|
||||
assert(!do_close(fd)); return found;
|
||||
}
|
||||
int main(void) {
|
||||
char root[512];
|
||||
snprintf(root,sizeof(root),"%s/tlg-jp-files-XXXXXX",getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp");
|
||||
assert(mkdtemp(root));
|
||||
char game[600],user[600],path[800],nested[800];
|
||||
snprintf(game,sizeof(game),"%s/game",root);
|
||||
snprintf(user,sizeof(user),"%s/user",root);
|
||||
assert(!mkdir(game,0755));
|
||||
const char *packs[]={"trico_frf.psarc","trico_frfp.psarc","trico_boot.psarc","trico_frc.psarc"};
|
||||
for (size_t i=0;i<4;++i) { snprintf(path,sizeof(path),"%s/%s",game,packs[i]); make_file(path); }
|
||||
snprintf(nested,sizeof(nested),"%s/nested",game); assert(!mkdir(nested,0755));
|
||||
snprintf(path,sizeof(path),"%s/nested/trico_frf.psarc",game); make_file(path);
|
||||
unsetenv("TLG_JAPANESE_FALLBACK");
|
||||
runtime_file_configure(game,user);
|
||||
snprintf(path,sizeof(path),"%s/data/trico_frf.psarc",user); make_file(path);
|
||||
readable("/app0/trico_frf.psarc"); readable("/app0/trico_frfp.psarc");
|
||||
assert(listed("/app0","trico_frf.psarc") && listed("/hostapp","trico_frfp.psarc"));
|
||||
|
||||
setenv("TLG_JAPANESE_FALLBACK","1",1);
|
||||
const char *hidden[]={"trico_frf.psarc","/app0/trico_frf.psarc","/hostapp/trico_frfp.psarc",
|
||||
"./trico_frfp.psarc","/app0/./trico_frf.psarc","/app0//TRICO_FRF.PSARC"};
|
||||
for (size_t i=0;i<sizeof(hidden)/sizeof(*hidden);++i) {
|
||||
GuestStat info;
|
||||
assert(do_open(hidden[i],0,0)==-ENOENT);
|
||||
assert(do_stat(hidden[i],&info)==-ENOENT);
|
||||
}
|
||||
assert(!listed("/app0","trico_frf.psarc") && !listed("/hostapp","trico_frfp.psarc"));
|
||||
assert(!listed(".","trico_frf.psarc") && !listed("/app0/.","trico_frfp.psarc"));
|
||||
assert(listed("/app0","trico_boot.psarc") && listed("/app0","trico_frc.psarc"));
|
||||
readable("/app0/trico_boot.psarc"); readable("/app0/trico_frc.psarc");
|
||||
readable("/app0/nested/trico_frf.psarc"); readable("/data/trico_frf.psarc");
|
||||
assert(listed("/app0/nested","trico_frf.psarc") && listed("/data","trico_frf.psarc"));
|
||||
assert(do_open("/app0/nested/../trico_frf.psarc",0,0)==-EACCES);
|
||||
|
||||
for (int i=0;i<2;++i) {
|
||||
setenv("TLG_JAPANESE_FALLBACK",i ? "invalid" : "0",1);
|
||||
readable("/app0/trico_frf.psarc"); readable("/app0/trico_frfp.psarc");
|
||||
assert(listed("/app0","trico_frf.psarc"));
|
||||
}
|
||||
unsetenv("TLG_JAPANESE_FALLBACK");
|
||||
readable("/hostapp/trico_frf.psarc");
|
||||
for (size_t i=0;i<4;++i) { snprintf(path,sizeof(path),"%s/%s",game,packs[i]); assert(!unlink(path)); }
|
||||
snprintf(path,sizeof(path),"%s/nested/trico_frf.psarc",game); assert(!unlink(path));
|
||||
assert(!rmdir(nested) && !rmdir(game));
|
||||
snprintf(path,sizeof(path),"%s/data/trico_frf.psarc",user); assert(!unlink(path));
|
||||
const char *dirs[]={"temp0","download0","data"};
|
||||
for (size_t i=0;i<3;++i) { snprintf(path,sizeof(path),"%s/%s",user,dirs[i]); assert(!rmdir(path)); }
|
||||
assert(!rmdir(user) && !rmdir(root));
|
||||
puts("Japanese text view: open/stat/list masking, isolation and reversible host-file preservation PASS");
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
from paths import ROOT
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(ROOT / 'launcher'))
|
||||
import tlg_run
|
||||
|
||||
|
||||
def param_sfo(title='CUSA03745', version='01.03'):
|
||||
keys, values, entries = bytearray(), bytearray(), []
|
||||
for key, value in [('TITLE_ID', title), ('APP_VER', version)]:
|
||||
encoded = value.encode() + b'\0'
|
||||
size = (len(encoded) + 3) & ~3
|
||||
entries.append(struct.pack('<HHIII', len(keys), 0x204, len(encoded), size, len(values)))
|
||||
keys += key.encode() + b'\0'
|
||||
values += encoded + b'\0' * (size - len(encoded))
|
||||
key_offset = 20 + 16 * len(entries)
|
||||
value_offset = (key_offset + len(keys) + 3) & ~3
|
||||
return (struct.pack('<4sIIII', b'\0PSF', 0x101, key_offset, value_offset, len(entries)) +
|
||||
b''.join(entries) + keys + b'\0' * (value_offset - key_offset - len(keys)) + values)
|
||||
|
||||
|
||||
class JapaneseLanguageTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.game = Path(self.tmp.name)
|
||||
(self.game / 'sce_sys').mkdir()
|
||||
for name in ('trico_boot.psarc', 'trico_bootp.psarc', 'trico_frf.psarc', 'trico_frfp.psarc'):
|
||||
(self.game / name).write_bytes(b'untouched archive fixture')
|
||||
self.metadata()
|
||||
self.settings = {'game_dir': str(self.game), 'game_language': 'ja'}
|
||||
|
||||
def metadata(self, title='CUSA03745', version='01.03'):
|
||||
(self.game / 'sce_sys/param.sfo').write_bytes(param_sfo(title, version))
|
||||
|
||||
def test_verified_layout_offers_explicit_japanese_choice(self):
|
||||
self.assertTrue(tlg_run.japanese_fallback_available(self.game))
|
||||
self.assertIn('ja', tlg_run.game_language_choices(dict(self.settings, game_language='auto')))
|
||||
self.assertEqual(tlg_run.resolve_game_language(self.settings, {}), 'ja')
|
||||
self.assertNotIn('ja', tlg_run.game_languages(self.game)) # native engine table unchanged
|
||||
|
||||
def test_japanese_emits_internal_french_id_and_guest_mask(self):
|
||||
self.assertEqual(tlg_run.game_language_environment(self.settings, {}),
|
||||
{'TLG_LANGUAGE': '2', 'TLG_JAPANESE_FALLBACK': '1'})
|
||||
|
||||
def test_ordinary_french_keeps_both_french_archives_visible(self):
|
||||
s = dict(self.settings, game_language='fr')
|
||||
self.assertEqual(tlg_run.game_language_environment(s, {'TLG_JAPANESE_FALLBACK': '1'}),
|
||||
{'TLG_LANGUAGE': '2', 'TLG_JAPANESE_FALLBACK': '0'})
|
||||
|
||||
def test_other_languages_do_not_inherit_a_stale_mask(self):
|
||||
for choice, code in [('en', '1'), ('de', '4')]:
|
||||
self.assertEqual(tlg_run.game_language_environment(dict(self.settings, game_language=choice),
|
||||
{'TLG_JAPANESE_FALLBACK': '1'}),
|
||||
{'TLG_LANGUAGE': code, 'TLG_JAPANESE_FALLBACK': '0'})
|
||||
|
||||
def test_automatic_does_not_silently_opt_into_experimental_fallback(self):
|
||||
self.assertEqual(tlg_run.game_language_environment(dict(self.settings, game_language='auto'),
|
||||
{'LANG': 'ja_JP.UTF-8'}),
|
||||
{'TLG_LANGUAGE': '1', 'TLG_JAPANESE_FALLBACK': '0'})
|
||||
|
||||
def test_other_regions_and_versions_are_not_advertised(self):
|
||||
for title, version in [('CUSA03627', '01.03'), ('CUSA04936', '01.03'),
|
||||
('CUSA04943', '01.03'), ('CUSA99999', '01.03'),
|
||||
('CUSA03745', '01.00'), ('CUSA03745', '01.04')]:
|
||||
self.metadata(title, version)
|
||||
self.assertFalse(tlg_run.japanese_fallback_available(self.game))
|
||||
self.assertNotIn('ja', tlg_run.game_language_choices(dict(self.settings, game_language='auto')))
|
||||
self.assertEqual(tlg_run.game_language_environment(self.settings, {}),
|
||||
{'TLG_LANGUAGE': '1', 'TLG_JAPANESE_FALLBACK': '0'})
|
||||
|
||||
def test_missing_boot_archive_or_bad_metadata_fails_closed(self):
|
||||
(self.game / 'trico_bootp.psarc').unlink()
|
||||
self.assertFalse(tlg_run.japanese_fallback_available(self.game))
|
||||
(self.game / 'sce_sys/param.sfo').write_bytes(b'broken')
|
||||
self.assertFalse(tlg_run.japanese_fallback_available(self.game))
|
||||
with patch.object(tlg_run.shader_cache, 'title_id', return_value='CUSA03745'), \
|
||||
patch.object(tlg_run.mod_patches, 'game_app_version', side_effect=struct.error('broken')):
|
||||
self.assertFalse(tlg_run.japanese_fallback_available(self.game))
|
||||
|
||||
def test_explicit_engine_override_is_not_combined_with_wrong_mask(self):
|
||||
self.assertEqual(tlg_run.game_language_environment(self.settings, {'TLG_LANGUAGE': '1'}),
|
||||
{'TLG_LANGUAGE': '1', 'TLG_JAPANESE_FALLBACK': '0'})
|
||||
|
||||
def test_preexisting_manual_workaround_does_not_need_french_archives_restored(self):
|
||||
for name in ('trico_frf.psarc', 'trico_frfp.psarc'):
|
||||
(self.game / name).unlink()
|
||||
self.assertTrue(tlg_run.japanese_fallback_available(self.game))
|
||||
self.assertEqual(tlg_run.game_language_environment(self.settings, {})['TLG_JAPANESE_FALLBACK'], '1')
|
||||
|
||||
def test_build_env_uses_the_same_language_policy(self):
|
||||
with patch.dict(os.environ, {}, clear=True), \
|
||||
patch.object(tlg_run, 'window_icon', return_value=None), \
|
||||
patch.object(tlg_run, 'ngs2_module', return_value=None), \
|
||||
patch.object(tlg_run, 'user_dir', return_value=self.game / 'unused-user'), \
|
||||
patch.object(tlg_run, 'probe_env', return_value={}):
|
||||
settings = dict(tlg_run.DEFAULT_SETTINGS, **self.settings)
|
||||
env = tlg_run.build_env(settings)
|
||||
self.assertEqual(env['TLG_LANGUAGE'], '2')
|
||||
self.assertEqual(env['TLG_JAPANESE_FALLBACK'], '1')
|
||||
|
||||
def test_environment_planning_does_not_modify_game_or_saves(self):
|
||||
before = {str(p.relative_to(self.game)): p.read_bytes() for p in self.game.rglob('*') if p.is_file()}
|
||||
tlg_run.game_language_environment(self.settings, {})
|
||||
after = {str(p.relative_to(self.game)): p.read_bytes() for p in self.game.rglob('*') if p.is_file()}
|
||||
self.assertEqual(before, after)
|
||||
Reference in new issue
Block a user