Commit Graph
47447 Commits
Author SHA1 Message Date
Henrik Rydgård eda06f02d8 Merge pull request #22144 from hrydgard/symbol-demangler
Debugger: Demangle C++ symbol names when loading ELF symbols
2026-08-27 18:56:34 +02:00
Henrik RydgårdandClaude Opus 5 484898385f Interpreter: don't spin forever on a memory access that's set to be ignored
The alignment checks added in d8edeb7649 return out of the instruction handler
without advancing PC, so with IgnoreBadMemAccess - which is the default, and
which makes Core_MemoryException log and return - the run loop comes straight
back to the same instruction and never gets past it. cpu/crash/crash_read_u32
under -i logged the same SIGSEGV 585096 times in 30 seconds before being killed;
the JIT runs it to completion.

Continue instead, the way Memory::Read_U32 did before those checks existed and
the way the JIT's safe-memory path still does: loads produce zero, stores are
dropped, PC advances. When the exception is set to break rather than ignore,
Core_Break has already stopped the core by the time we get here, so nothing
changes for that case.

lv.q/sv.q are left alone - they already fall through and do the access.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 16:43:43 +02:00
Henrik RydgårdandClaude Opus 5 3fa67f22ed Interpreter: honor the guest's FPU rounding mode and flush-to-zero
Every JIT backend puts the host FPU into the mode fcr31 asks for (bits 0-1 and
24) before running emulated code, and takes it back out before calling any host
code. The plain interpreter did none of that, so all its float math rounded to
nearest with denormals intact no matter what the game had set - cpu/fpu/fpu
fails under -i and passes under the JIT on exactly this.

Move the helpers the IR interpreter already had for this out of IRInterpreter
and into MIPS.cpp as ApplyHostRoundingMode/RestoreHostRoundingMode, and use them
around the interpreter's run loop and single step, restoring around syscalls and
replacement functions, which are host code. ctc1 re-applies immediately, since
the interpreter has no block boundary to defer it to.

round.w.s changes with it: it was floorf(x + 0.5f), which is half-away-from-zero
rather than the half-to-even every JIT produces, and the add would now pick up
the guest's rounding mode on top of that. round_ieee_754 is both correct and
mode-independent, and is what cvt.w.s already used for the same rounding.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 16:43:06 +02:00
keycross b4eb292e34 Fix deferred libretro savestate size 2026-08-27 23:37:51 +09:00
Henrik RydgårdandClaude Opus 5 daa18fc25a ImDebugger: fix stale symbol list after a game is reloaded
The disasm window cached the flattened symbol list and only rebuilt it when one
of three menu items said so. Nothing marked it dirty when a game booted or
exited, and a new SymbolMap is allocated per boot, so the list kept showing the
previous game's functions.

Give SymbolMap a version counter that every mutator bumps, and let the window
compare against it instead. The counter is process-wide rather than per-map, so
a fresh map can't hand out a version a cached copy already holds.

Also re-find the selected symbol by address after a rebuild (the index means
something else afterwards), and drop the unused symbol cache members in
ImMemWindow.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 10:43:08 +02:00
Henrik Rydgård bef07b7a6b Merge pull request #22146 from NickWick13/patch-3
Update and improve Swedish translation
2026-08-27 09:06:34 +02:00
Jonatan Nyberg d6acd69388 Update sv_SE.ini 2026-08-27 09:41:46 +03:00
Henrik RydgårdandClaude Opus 5 049bcd5483 Demangle C++ symbol names when loading ELF symbols
C++ homebrew has an unreadable symbol table -
everything is _ZN10PxRenderer7DrawImmE... - which makes the disassembly and
symbol list nearly useless. Add an Itanium C++ ABI demangler and run ELF
symbols through it on load, in both ElfReader::LoadSymbols (unstripped EXECs,
which is what a CMake pspdev EBOOT actually contains) and the companion-ELF
path.

The demangling standard is called Itanium for historical reasons - it
was defined for Itanium but ended up being almost universally
applicable.

Written from scratch rather than using __cxa_demangle, which doesn't exist on
MSVC/UWP, or vendoring LLVM's demangler, whose license doesn't fit. Anything
unrecognized (arbitrary constant expressions, decltype) aborts the parse and
the caller gets the original mangled name back, so a caller never sees a
half-parsed result. Recursion is depth-capped since the input comes from a
file we didn't write.

Checked against c++filt as an oracle: of 1089 mangled symbols in a real C++
homebrew EBOOT, one differs; of 55189 from libstdc++/libLLVM/cc1plus, 22
differ and 413 are declined. Fuzzed with 220k mutated and random inputs under
ASan/UBSan.

Also adds a right-click menu to the ImDebugger symbol list.

Note that SymbolMap stores names in char[128], so the longest STL names get
truncated in the UI. Still far more readable than the mangled form.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X3DbkJ8ShYiXU7q5Tv1LZu
2026-08-26 08:12:45 +02:00
Henrik Rydgård adccd302e5 Merge pull request #22143 from a-blondel/feature/madden06
Add madden 06 to infra-dns.json
2026-08-26 00:19:56 +02:00
a-blondel 6a8b11ad2b Add madden 06 to infra-dns.json 2026-08-25 23:12:37 +02:00
Henrik Rydgård a0c10a0ea6 Merge pull request #22142 from darkguy2008/libretro-pause-handshake
libretro: restore emu-thread pause handshake so retro_serialize can't deadlock
2026-08-25 17:36:58 +02:00
AlemarandClaude Opus 4.8 821c5b4534 libretro: restore emu-thread pause handshake to fix serialize deadlock
c42a3f070a removed PAUSE_REQUESTED, which turned the acknowledge-wait in
EmuThreadPause() into dead code: emuThreadState is set to PAUSED on the line
above the `while (emuThreadState != PAUSED)` poll, so pause returns without the
emu thread parked at a frame boundary. retro_serialize / retro_unserialize then
race the GL ThreadFrame handshake and can deadlock the emu thread under periodic
save-states.

Restore the minimal handshake: EmuThreadPause() requests the pause, the emu
thread transitions PAUSE_REQUESTED -> PAUSED at its own frame boundary, and
retro_run treats the request window as paused. START_REQUESTED stays removed and
the new shutdown path is untouched.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-25 03:08:31 -06:00
Henrik Rydgård 4b57da4450 Merge pull request #22141 from hrydgard/mmio-work
Implement MMIO for the JIT
2026-08-25 01:11:54 +02:00
Henrik Rydgård a810597ac0 Implement MMIO for the JIT (by falling back to the interpreter for load/stores from kernel addresses)
Fixes the VSH in JIT mode (but NOT ir)
2026-08-25 00:22:40 +02:00
Henrik Rydgård fb385e7627 Merge pull request #22138 from hrydgard/vsh-auto-install
Allow auto-installing firmware from EBOOT.PBP firmware updates
2026-08-24 18:01:48 +02:00
Henrik Rydgård 442dc91556 Merge pull request #22139 from hrydgard/more-nid-names
Apply proper names to some more HLE functions
2026-08-24 18:01:38 +02:00
Henrik Rydgård 11b4509158 Apply proper names to some more HLE functions 2026-08-24 17:37:39 +02:00
Henrik RydgårdandClaude Opus 5 65592effc5 Block until game info is ready in LaunchFile
When launching a file from outside the main screen (file association, shortcut,
drag-and-drop), the info hasn't been computed yet, so the file type and ID checks
that decide what to do with the file were reading empty data. Add
GameInfo::WaitUntilReady() - a condition variable signalled from
MarkReadyNoLock(), which every exit path of the work item goes through - and use
it there.

Also demote a noisy PRX decryption log line to DEBUG.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EMLTdwyyzU6Mze3w8VC2JL
2026-08-24 17:23:20 +02:00
Henrik RydgårdandClaude Opus 5 cc1cdfcec4 Translate the firmware updater install strings
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-24 17:23:20 +02:00
Henrik RydgårdandClaude Opus 5 4b761f0715 Offer to install PSP firmware updaters instead of running them
Opening an official updater (a PSP/GAME/UPDATE EBOOT.PBP, identified by the
MSTKUPDATE disc ID) from the main screen now brings up a confirmation dialog
that unpacks the firmware into the NAND directory, where the emulated
flash0/flash1 live. Running the updater itself doesn't work, so there was
nothing useful to do with one before.

Unpacks the file list for the model we claim to be (iPSPModel), on a worker
thread, with a progress bar - for which PSARUnpackOptions gets an optional
progress callback.

AGENTS.md: translate UI strings last, in a separate commit

The English string is what all ~47 languages get derived from, so rewording it
after the sweep means redoing the sweep. Check the wording first.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-24 17:23:14 +02:00
Henrik Rydgård c989c2553e Merge pull request #22126 from hrydgard/vsh-hle-boot
Boot the VSH (XMB) through HLE
2026-08-24 13:30:08 +02:00
Henrik Rydgård 9205eb642a UWP buildfix, tweaks and de-clauding 2026-08-24 12:15:31 +02:00
Henrik Rydgård e9cdb2b20f UI work for VSH (Windows and ImGui) 2026-08-24 11:13:00 +02:00
Henrik RydgårdandClaude Opus 5 9de3eb1244 Answer the UMD region check, which the VSH opens with an error without
sceIoDevctl 0x01E18030 asks whether the disc's region matches the console's.
Unusually it answers through the return value rather than an output buffer -
1 matches, 0 doesn't - so leaving it unimplemented read as a mismatch, and the
VSH opened on "This disc cannot be started. The region code is not correct.",
asking 12 times. PPSSPP has no region-locked discs; anything it can load is
something it should run, so this always matches. The call now happens once.

Behind it is a second thing, not fixed here: the VSH believes a disc is inserted
at all because nothing in PPSSPP models an empty drive. sceUmd reports
PSP_UMD_PRESENT | PSP_UMD_READY unconditionally, and devctl 0x01F20001 always
answers "game disc". JPCSP answers "no disc" when no ISO is loaded, which is why
it never reaches the region question. Giving those two a notion of "no disc"
would be the real fix - low risk for games, which always have one, but it is on a
path every game uses, so it is written up in docs/VSHBootInvestigation.md rather
than done as a drive-by.

With this the shell reaches the interactive XMB: the error is dismissable with
circle and the menu behind it works. The per-frame display list stops settling
into one repeated frame and alternates between 45 and 48 stall points, which is
the headless-visible sign of a live menu rather than a static dialog.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 11:13:00 +02:00
Henrik RydgårdandClaude Opus 5 708e7e1d9c Decrypt the VSH's XMB index: add PRX decryption type 9
pspDecryptPRX() tried types 0, 1, 2, 5 and 6. flash0:/vsh/etc/index_XXg.dat -
the index of what the XMB shows, fetched through sceResmgr_9DC14891 - needs
type 9, so it failed and the shell had no menu to build.

Type 9 is type 6 with three differences, all following from a type 9 file
carrying a real ECDSA signature at 0x104..0x12C where a type 6 file has nothing:

- The "must be empty" header check stops at 0x104 instead of 0x10C. The index's
  signature starts there, so 8 of its bytes were failing type 6's check - the
  original failure.
- The signature is left out of the hashed header rather than fed into it. JPCSP
  zeroes buf2[0x34..0x5C), which is that same range once its header
  rearrangement is undone, so PRXType9 just leaves the field zero.
- ecdsa_hash in the KIRK CMD1 header stays 0. Type 6/7 set it, but the branch
  type 9 takes writes only the mode word, and setting it made KIRK reject the
  block.

Tried last in the chain: its header check is a subset of type 6's, so a genuine
type 6 PRX would pass it and then fail on the hash, and trying it earlier would
shadow the real answer. False positives are not really possible either way - the
SHA1 check inside has to match before anything is decrypted.

Verified end to end: 496 bytes in, 159 out (the comp_size in the header),
starting "release:". sceResmgr checks that prefix and says so in its log line,
since a wrong-but-plausible decrypt would otherwise look like success here and
fail much later as an unreadable index.

The VSH now draws something different - the per-frame display list settles at 24
stall points rather than 38 - but what it shows is not visually confirmed;
framebuffer readback doesn't work under headless on either backend.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 11:13:00 +02:00
Henrik RydgårdandClaude Opus 5 e7e8d362f0 Add sceResmgr, which is what the VSH uses to decrypt the XMB index
Found the cause of the red error screen the VSH ends on. Every resource load in
the boot succeeds - fonts, all the plugin RCOs, topmenu_icon.rco - and then:

  sceIoOpen(flash0:/vsh/etc/index_02g.dat) -> fd 8
  sceIoRead(8, 092a2d40, 496)
  sceIoClose(8)
  unresolved import sceResmgr/9dc14891, called from 'vsh_module'
  sceKernelExitDeleteThread(1)

index_02g.dat is the index of what the XMB displays, and it is encrypted (it
starts "PSPsysGP"). sceResmgr_9DC14891 decrypts it. There was no sceResmgr module
at all, so the call trapped, the index stayed encrypted, and the ScePafJob thread
building the top menu exited - a shell with everything loaded and nothing to show.

This adds the module and the three tags it needs (0x0B2B90F0/91F0/92F0, keys and
code 0x5C) to PrxDecrypter.

It is not the whole fix yet: pspDecryptPRX() tries decryption types 0, 1, 2, 5
and 6, and this needs type 9, which JPCSP passes explicitly. So the call is now
reached and fails cleanly with a logged error instead of trapping, but does not
yet decrypt. Type 9 is a variant of type 2 and is the next job; the notes in
docs/VSHBootInvestigation.md say where it is in JPCSP and how to check a port
(159 bytes out, starting "release:").

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 11:12:59 +02:00
Henrik Rydgård 0f36b3310f Merge pull request #22137 from hrydgard/emu-api
Document and wrap the PPSSPP "emulator:" devctls (API for homebrew)
2026-08-24 11:03:57 +02:00
Henrik RydgårdandClaude Opus 5 f449fa6ab2 Implement a batch of the VSH's unresolved imports (and document three we must not)
Mostly small stubs:

- sceImpose: GetParam/SetParam/Changes/SetStatus, plus the 6.60 alias of
  sceImposeGetBatteryIconStatus. Also fixes that function's first output - it
  is a plain "is it charging" boolean, not a BATTICON_ value. We wrote
  PSP_IMPOSE_BATTICON_NONE (0x80000000) there, which games ignore but which the
  VSH reads as "no battery" and draws the empty-battery indicator for. These
  are the bulk of the traffic: the VSH calls sceImposeChanges once a frame, so
  this alone removes ~10000 trapped calls from a boot.
- SysMemForKernel: sceKernelSetRebootKernel, sceKernelSetUmdCacheOn.
- scePower_driver: scePowerSetWakeupCondition.
- sceHprm_driver, sceUsb: one NID-named call each, as in JPCSP.

Three groups are deliberately left unresolved, with comments explaining why,
because resolving them lets real flash0 drivers walk into hardware we do not
emulate and the boot dies where it used to reach the shell:

- ThreadManForKernel mutex/fpl NIDs: the NAND and ID storage drivers use these
  to init, then poll the NAND controller at 0xbd101300 forever.
- InterruptManagerForKernel intr registration: 31 calls, then a stall in GE
  list execution with no plugin module ever started.

73 unresolved import hits over 37 distinct module/NID pairs remain in a VSH
boot, mostly sceSysEventForKernel, sceSuspendForKernel and the various
*_driver modules that need real hardware behind them.

The sceImpose savestate section goes to v2 for the two new state variables.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 10:58:35 +02:00
Henrik RydgårdandClaude Opus 5 4667f5e2da HLE: implement sceKernelLoadModuleVSH - the VSH now draws its UI
ModuleMgrForKernel/0xD5DDAB1F is how the VSH loads its own plugins. The XMB's
interface lives in flash0:/vsh/module/*_plugin.prx and vshmain pulls those in
through this kernel call rather than the user-mode sceKernelLoadModule, so the
existing note that vshmain never imports sceKernelLoadModule was true but
incomplete - it imports this instead, and it was unresolved.

The consequence was quiet: vshmain got no module id back and then called
sceKernelStartModule with id 0, which failed with UNKNOWN_MODULE. None of the
plugins that populate the XMB ever ran. The scene still had its containers,
which is why every frame set up render state per node and drew nothing inside
them - the "6x render-state-setup, 0 draws" symptom this investigation has been
chasing.

Also implements 0xD86DD11B sceKernelSearchModuleByName, the other unresolved
ModuleMgrForKernel import.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 10:58:35 +02:00
Henrik RydgårdandClaude Opus 5 23f219d276 docs: add VSHBootInvestigation.md
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 10:58:35 +02:00
Henrik Rydgård 75bd16d0f4 Apply two hacks to scePaf memory-arena hack
For some reason, a pointer used to allocate the heap for scePaf is not
initialized. This hacks aroung that.

Additionally zero out the specific 4-byte "category 1 alarm count" address in vsh_module.

This gets us much further.
2026-08-24 10:58:35 +02:00
Henrik Rydgård 6d14a10d83 HW: implement real Syscon serial protocol emulation (SysconSerialMMIO)
Extends the SysconSerialMMIO stub added for VSH boot into a real command/
response protocol matching uofw's Syscon_cmd() reference exactly (packet
framing, checksum, GPIO4 "response ready" handshake via a new GpioMMIO
cross-module hook), with handling for NOP/read-write clock/read-write
alarm commands.

Still hitting a SIGSEGV though.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
(cherry picked from commit 11887bf9e1fecd1eac56ec705c01b6fcfac09b2e)
2026-08-24 10:58:35 +02:00
Henrik Rydgård ed5c05fb28 VSH boot: load real kd/ kernel driver modules
Extends LoadAndStartVshKernelModules() to load the 11 real kd/*.prx
kernel drivers for --vsh (dmacman, systimer, memlmd_01g,
loadexec_01g, lowio, idstorage, syscon, rtc, wlan, wlanfirm_01g, utility),
ahead of the existing 4 VSH-specific modules.
Only active when g_runningVSH, no effect on normal game boot.

Improve implementations of sceKernelSm1ReferOperations and sceKernelIsIntrContext.

Add some more MMIO stubs (GPIO, SYSCON).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
(cherry picked from commit 7f3168b7df85e47438900016c9ee7d7ef01a0a28)
2026-08-24 10:58:35 +02:00
Henrik Rydgård 1d991557e8 Extend LoadExecForKernel with real VSH loadexec/exit syscalls
Adds SceKernelLoadExecVSHParam and fills in the rest of LoadExecForKernel's
NIDs: real implementations for sceKernelExitVSHVSH/Kernel
(mirrors sceKernelExitGame) and sceKernelLoadExecBufferVSHUsbWlan (loads
an exec from an in-RAM buffer instead of a file, plus UNIMPL stubs.

sceKernelLoadExecBufferVSHUsbWlan needed __KernelLoadExec split into a
file-reading front end and a shared __KernelLoadExecFromPtr back end
that both it and the new buffer-based path call into.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU
(cherry picked from commit 4adbedec9e2e3221113c9e5ad00b248bba1153e1)
2026-08-24 10:58:34 +02:00
Henrik RydgårdandClaude Sonnet 5 04a8948ded Add a VSH-only allowlist for real-loading flash0 modules, sceVshCommonUtil stub and sceVshBridge
Load and start VSH's kernel modules before booting vshmain.prx

A few flash0 modules (vshbridge.prx, paf.prx, common_gui.prx,
common_util.prx) should run for real once we know we're
actually booting the VSH rather than a game, since our fakes are unlikely
to be good substitutes for the genuine thing.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU
2026-08-24 10:58:34 +02:00
Henrik Rydgård 0dbb79bcd9 Document and wrap the PPSSPP "emulator:" devctls (API for homebrew) 2026-08-24 10:46:04 +02:00
Henrik Rydgård 0e2dfad02d Merge pull request #22136 from hrydgard/nand-mounts
Mount flash from PSP/NAND/flash0, flash1 etc, fix host0: to be more useful for homebrew
2026-08-24 10:45:50 +02:00
Henrik Rydgård b3a2178a5c ImDebugger: Show per-thread current directory in threads view 2026-08-24 10:21:27 +02:00
Henrik Rydgård 8a26e39792 Fix ImDebugger bug 2026-08-24 09:53:40 +02:00
Henrik Rydgård 78ef1eae82 Instead of flash0directory, have a NAND subdirectory under PSP, where you can keep flash0, flash1 etc.
Also make it configurable via command line.
2026-08-24 09:53:37 +02:00
Henrik Rydgård 786a1530de Correct host0: mount 2026-08-24 09:30:44 +02:00
Henrik Rydgård b437ba0fc6 MetaFileSystem: Add subdirectory mounts 2026-08-24 09:30:44 +02:00
Henrik Rydgård 632fa10b23 Use VFS directly to read HLE-sceFont fonts instead of relying on the flash0: mount 2026-08-24 09:30:42 +02:00
Henrik Rydgård e4f66f3809 Merge pull request #22132 from hrydgard/langtool-updates
Langtool updates, translation fixes
2026-08-23 17:29:33 +02:00
Henrik Rydgård 2c932e9a59 Merge pull request #22131 from hrydgard/gameinfo-cache-bundled-update
GameInfoCache: report the firmware updater bundled on a game disc
2026-08-23 17:12:36 +02:00
Henrik RydgårdandClaude Opus 5 396f9e802d GameInfoCache: report the firmware updater bundled on a game disc
Adds GameInfoFlags::BUNDLED_UPDATE_INFO, holding the version, title, size
and timestamp of the updater in PSP_GAME/SYSDIR/UPDATE. It comes from the
PARAM.SFO and the directory entry next to the archive, so it's a couple of
small reads on the ISOFileSystem the worker already has open - no
decryption, and DATA.BIN itself is only sniffed for its magic. Only
computed for ISOs; everything else is marked complete with an empty struct.

ISOFileSystem now parses the date out of the ISO9660 directory record,
stored as Unix UTC seconds and reported as the PSP's atime/ctime/mtime.
Those used to always read back as zero, so games calling sceIoGetstat on a
UMD file saw 1900 where hardware gives the mastering date.

Shown on GameScreen as e.g. "Firmware update on disc: 6.60 (2011-10-05),
25.6 MB".

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0149QcTVgZEXKXbgHyvXF4ZY
2026-08-23 16:44:38 +02:00
Henrik RydgårdandClaude Opus 5 c9ff8f55fe langtool: Mark strings that are deliberately the same as the English one
Plenty of strings are legitimately identical in the target language - "Vsync",
"Status", "Ad Hoc multiplayer" - but the only test we had for "is this
translated" is "does it differ from English", so those got sent to the AI again
on every single run of finish-language-with-ai, costing tokens and giving it
another chance to translate something that shouldn't be.

They now get a "# same as English" comment and are skipped. finish-language-with-ai
writes it when the AI hands the English string back unchanged (which the
validation added earlier was throwing away as an error), and import-single writes
it when a language's line matches the en_US one. Deleting the comment puts the
key back in play.

Also stop comparing values with their comments still attached while working out
what's untranslated, which was leaking "# AI translated" into the context block
of the prompt.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 a1d205539c langtool: Let import-single create the key in en_US.ini too
It skipped the reference file, so adding a new string meant running
add-new-key-value first just to get the key into en_US.ini (and, as a side
effect, the English text into all 46 other files whether they were about to be
translated or not). Now an en_US line in the imported file is treated like any
other language, minus the "AI translated" comment, since that one is the string
the others were translated from rather than a translation.

So the workflow is one command plus validate. Languages left out of the import
simply don't get the key, and fall back to the English string at runtime -
copy-missing-lines is still there if you want the placeholder written out.

Instructions in AGENTS.md and the /add-string command updated to match.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 10bfda59ee Document the translation workflow, and add an /add-string command for it
Translating a UI string well means knowing what it does - what widget it is,
what the placeholders hold, how the neighbouring strings in that language are
phrased. langtool's AI commands can't know any of that, which is why their
prompt has a hand-maintained glossary that grows every time someone spots a bad
translation. An agent working in the repo can just go look.

So: AGENTS.md now describes doing the translating that way and letting langtool
do the file surgery (add-new-key-value, import-single, validate) instead of
hand-editing 47 files, and .claude/commands/add-string.md wraps it as a slash
command. Both say to skip a language rather than guess at it - the English
fallback is fine, a confident wrong translation nobody can proofread is not.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 bb4563e6ca Fix the broken translations that langtool validate found
57 strings across nine language files:

* 33 in km_KH where a space snuck in between the % and the digit ("% 1"), plus
  one where "%0.2f Hz" ended up as "0.2% f Hz".
* fa_IR "Quick chat %1" had the %1 replaced by a Persian numeral, and ko_KR
  "Submitted %1 for %2" lost its %2.
* tr_TR "Earned" had the key translated instead of the value, so the whole
  sentence with its four placeholders was replaced by the word "Earned".
  The new Turkish string needs a look from someone who speaks Turkish.
* 20 strings across five files that were wrapped in quotes. PPSSPP's ini parser
  strips those on load, so nothing changes on screen, but they tripped the
  validator.

Turkish writes percentages as "% 0", so the "% <digit>" fix was only applied
where it makes the placeholders match the English string again.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00