EnglishУкраїнськаGitHub

Plugin Developer Guide

Language: English · Українська

A plugin teaches Picoripi one game or text format: how to read and write its files, what its tags are, how wide a line may be, and — optionally — what the game's own data says about each line. This page is the only plugin guide. The list of every hook is generated from the code: docs/PLUGIN_CONTRACT.md.

Source of truth: plugins/base_game_rules.py (the hooks and their defaults), plugins/spec.py (the contract as data), ui/main_window/main_window_plugin_handler.py (loading), ui/settings/logging_mixin.py → find_plugins (discovery).


Start a plugin

.\venv\Scripts\python.exe tools/new_plugin.py <id> "<Display Name>" --prefix XX

<id> is the folder name under plugins/; --prefix is the short uppercase tag of the plugin's problem ids (XX_WIDTH_EXCEEDED). The command copies plugins/default_plugin, renames the prefix and the display name, and writes tests/test_plugins/test_<id>/test_rules.py with three passing checks (the plugin loads, a sample survives load and save, the validator passes). The application lists the new plugin at once.

Then, in this order:

  1. Parsing: load_data_from_json_obj / save_data_to_json_obj in rules.py. Put a real sample into SAMPLE in the generated test before touching anything else.
  2. Tags: tag_manager.py.
  3. Widths: fonts/*.json, font_map.json, the limits in config.json.
  4. Prompts: translation_prompts/prompts.json, last.

Check and test:

$env:PYTHONPATH = "."; .\venv\Scripts\python.exe -m plugins.validate <id>
$env:PYTHONPATH = "."; .\venv\Scripts\python.exe -m pytest -n auto tests/test_plugins/test_<id>/

python -m plugins.validate calls every hook with dummy arguments and reports a hook that raises, a wrong return type, a signature the host cannot call, a broken JSON file, per-session keys in config.json, and (as warnings) main-window attributes outside the allowed list.

Collect before coding

The exact file format with sample source and translated files and the expected save output; the maximum width and lines per page; every tag and control code, which are zero-width and how wide the visible ones are; where font metrics come from; how pages break; speaker or chapter metadata; whether unknown fields must round-trip unchanged. plugins/default_plugin/AI_PLUGIN_ASSISTANT_PROMPT.md asks these questions in a form you can paste to an AI assistant.


Discovery and load

Discovery (New Project and Settings → Global → Active Game Plugin): every directory under plugins/ that contains config.json, except import_plugins. display_name in that file is the label and must be unique; the folder name is the plugin id.

Load: importlib.import_module(f"plugins.{id}.rules"). The module must define GameRules, a subclass of BaseGameRules, constructed as GameRules(main_window_ref=self.mw). It must also build without a main window (GameRules()): tests and the validator do that. No network, disk-heavy or archive work in __init__.

If the import fails, the user gets Plugin Load Error and the application falls back to BaseGameRules. Switching plugins drops every loaded module of the plugin, so module-level caches start empty.

Aliases: after load, plugins/<id>/aliases.json (shipped defaults, read-only) and then the user's own aliases.json from the per-user plugin folder are merged into default_tag_mappings. The application saves aliases only to the per-user folder.

Force aliases and names drawn in colour: an alias whose name starts with F: ("{F:Link}": "{Player}") reaches the model as the plain word, which it declines; the translation keeps that word and not the tag, so a renamed hero still reads «Лінк». If the game itself draws that name in a colour, return the colour tags from get_force_alias_wrapping() ({"{F:Link}": ("{Color:Green}", "{Color:White}")} in Minish Cap): the word is sent wrapped and the declined form comes back in the same colour; the tag warning does not count those tags as extra. Find out per game whether the engine colours the name — Twilight Princess, The Wind Waker and Tears of the Kingdom do not (TotK: none of the 76–99 {playerName} tags per language has a colour tag around it; English writes "Link" as plain text; Wind Waker HD: 326 of 359 [Name] tags stand in no colour at all; Skyward Sword HD: 626 of 631 {heroName}), Minish Cap does.

Menu actions: get_plugin_actions() may add QActions (name, text, tooltip, shortcut, handler, menu, toolbar). The AI translate actions belong to the application and are present for every game.

Voice clips: get_voice_clips(project_dir) lists the clips (id, title, disc: the .bin the original is read from, voice_dir: where a translated WAV is kept). export_voice_clip, import_voice_clip and apply_voice_clips do the work; a plugin's own Tools action opens the dialog, and every export, import or apply runs in a worker. A game without voice clips keeps the defaults (an empty list).


Files of a plugin

plugins/<id>/
  config.json                         # required: at least "display_name"
  rules.py                            # required: class GameRules(BaseGameRules)
  config.py                           # problem ids and default detection / autofix settings
  tag_manager.py                      # the game's tags and their highlighting
  font_map.json, fonts/*.json         # widths
  font_sources.json                   # the game's fonts for the font editor (optional)
  texture_sources.json                # the game's textures with text for the Textures window (optional)
  translation_prompts/prompts.json    # prompt sections the game changes

config.json — defaults for a new project: display_name, game_dialog_max_width_pixels, line_width_warning_threshold_pixels, width_tolerance_percent (how far past the target width a line may go before it must be shortened; used when a window kind gives only one width), lines_per_page, default_font_file, detection_enabled, autofix_enabled, colours and wrap flags. Per-session keys (open file paths, selection, scroll positions, search history) are refused by the validator; the application stores those in project_settings.json.

config.py — PROBLEM_DEFINITIONS, DEFAULT_DETECTION_SETTINGS, DEFAULT_AUTOFIX_SETTINGS = generate_base_config(PREFIX, overrides=…, custom_problems=…) from plugins/common/config_factory.py.

rules.py — GameRules declares its parts with class attributes and BaseGameRules wires them:

class GameRules(BaseGameRules):
    problem_prefix = "XX"
    problem_definitions = PROBLEM_DEFINITIONS
    tag_manager_class = TagManager        # default: GenericTagManager
    tag_style = "curly"                   # "square" when tags look [like this]
    analyze_whole_string_first = True

With these the base class builds the tag manager, the problem analyzer and the text fixer and answers the hooks that only pass a call on (problem definitions, highlighting, analysis, autofix, short problem names). Name problem_analyzer_class / text_fixer_class only for game-specific checks or fixes (subclass GenericProblemAnalyzer / GenericTextFixer from plugins/common/). problem_ids = problem_ids(DEFINITIONS, PREFIX, without=(…)) leaves out standard checks the game does not use.

String context for a check. While the host checks one string, the analyzer's _current_scan_block_idx and _current_scan_string_idx hold that string's block and string index. The host sets them around each analysis and restores the previous values after. The values are per thread: a thread sees only the values it set, so two threads that check strings with one analyzer do not mix them. Outside a string analysis both are None; read them with getattr(self, "_current_scan_block_idx", None) and handle None. Do not keep them after the call. For a detail under a problem's name in the editor tooltip, implement get_problem_detail(block_idx, string_idx, problem_id). Return a short text, or "" for no detail.

font_map.json and fonts/*.json — fonts/*.json are font maps ({character: {"width": N}}). The root font_map.json overrides widths of visible tags and icons: {"[A]": {"width": 16}, "{COLOR_RED}": {"width": 0}}. A visible icon tag is not zero-width. Widths the user sets in the application (tag width dialog, Settings → font map) are saved to ~/.picoripi/plugins/<plugin>/font_map.json, which wins over the plugin's file; the same holds for translation_map.json written by the BFN editor without a project and window_layouts.json written by the Settings table of per-window limits. The application never writes into plugins/.

font_sources.json (or the hook get_font_sources()) — the game's own bitmap fonts, so the font editor lists them for a project and saves them back. Each entry: label, format (bfn, n64, g1t, g1n, bffnt (Switch), bcfnt (3DS BCFNT and 3DS BFFNT; a font whose FINF encoding is Shift-JIS maps its codes through core/font_formats/sjis.py, where the eight Ukrainian letters have the free codes 0x8492–0x8499), bffnt_wiiu (Wii U BFFNT), brfnt (Wii RFNT and archived RFNA, read by the same reader), fe13_bfnt (Fire Emblem Awakening .bfnt: a glyph atlas of A4 / A8 sheets with a sorted glyph table; one cell per glyph, a redrawn glyph goes back into its box, a new character takes a free band of rows or the box of a Japanese glyph), jfnt (One Piece: Unlimited World Red 3DS: Ganbarion JFNT with an A8 jIMG picture, glyph cells in a grid, advance bytes editable), nlg_font (Luigi's Mansion: Dark Moon: the glyph description and ETC1A4 pages of a Next Level Games bundle; params.record picks one font; pixels and advance numbers are editable, new glyphs are not), cop_fnt (Agatsuma FD fonts, Code of Princess), l5_fnt (Level-5 FNT fonts of Fantasy Life: a code list and one 4-bit bitmap per glyph), pw_xpr (Peace Walker *_FONT.xpr: an XPR2 pack with an 8-bit linear atlas and a code table), ubiart_tfn (UbiArt .tfn.ckd glyph table with its .tga.ckd atlas as the companion; a new letter in an empty cell becomes a table record), asw_jfnt (Arc System Works jFNT atlas font of Dragon Ball Fusions: a range table, metrics and an embedded jIMG atlas; glyph pictures are edited in place), cfna (3DS archived fonts CFNA of NintendoWare ArchiveFont: sheets compressed one by one, with the character tables), layton_nftr (Professor Layton NFTR inside BIOS compression), layton_gfnt (Professor Layton GFNT fonts), layton_hd (Professor Layton HD port font atlas), gen4_glyphs (DS Gen 4 font bins: Diamond / Pearl, Platinum, HeartGold), laft (Monolith Soft LAFT .wifnt fonts of the Switch Xenoblade games: fixed cells in one MIBL atlas, each glyph with its code point and its left and right ink edges), phyre_bmf (PhyreEngine .fgen.NXGL.phyre bitmap fonts of Dragon Quest Builders 1: character records with a rectangle in a signed-distance atlas, rotated glyphs lie on their side; one cell per character, a redrawn cell goes back into its rectangle), qbf or gzf (Grezzo, OoT3D / MM3D), bfotf (a Switch scalable OpenType/CFF font: widths save as advances, a redrawn glyph as a traced outline; params.size is the pixel size), mgs (MGS2-engine 2bpp proportional font, The Twin Snakes; params height, first_code, upper_codepage), texture_grid (a glyph grid in one texture image that the game's code spaces itself, e.g. Twin Snakes' HUD and menu atlases inside stage.dat texture packs, or Super Metroid's raw SNES tile sheets; params texture (texture format, default tpl), image, cell [w, h], chars (a string, or a list where an entry that is not one character — a game token, "" for a free cell — leaves its cell unmapped; a "\0" entry also leaves it unmapped) or first_code, optional columns; the sheet is the image in its own colours and is encoded back in the texture's format), eternal_darkness (Eternal Darkness: a 16 x 16 glyph grid of a TPL image, stored upside down and filled column by column, shown row by row the right way up; the advances are a table in the companion pack EBootPak.bin; params image, width_table (entry of the companion's pack), optional grid, flip, encoding (cp1251: codes 0x80-0xFF show as Cyrillic)), mgs1 (Metal Gear Solid PlayStation font.res: 96 proportional 2bpp glyphs of 12 rows and the 12x12 zenkaku symbols; a glyph may change its width), mgs1_hd (M2's 4x copy of that font in Metal Gear Solid Master Collection: 48x48 2bpp glyphs, 578-byte records with width and row offset, or the 48 zenkaku symbols), mgs_nht (the MGS2 / MGS3 Master Collection text font MGS_Font_nht.raw: 8-bit coverage glyphs with offsets and advance; spare cells take new characters), m2 (an M2 PSB font: each character's box on its A8 / A8L8 / RGBA8 page; glyphs are redrawn inside their boxes, boxes and metrics stay), cv_gba (1-bit glyph records of Konami's GBA Castlevania games: a 2-byte key and one byte per row; glyphs, record, rows, keep_mask for bits that are not pixels, widths when the width table comes as the companion file, chars = character -> glyph index), gba_tiles (8x8 4bpp GBA tiles in an LZ77 block of an LZ77-packed multiboot program, the Tingle Tuner; params program_offset, program_address, tail_pointers, tiles_address, tiles_room, glyph_offset, glyph_count, advance, chars (code -> character) and optional slots (accent codes pointed at free tiles on save)), nftr (Nintendo DS NFTR, Four Swords Anniversary Edition; params columns of the editor grid, spare empty cells for new characters), ffta2 (Final Fantasy Tactics A2: UseMoji_image.bin with the width table FontWidthTable.bin as companion; 2-bit glyphs in 16x16 cells, a glyph typed into a spare cell is added up to text code 0xBF), twewy (The World Ends with You: one of the four fonts of the pack file Grp_Font.bin; params pages and widths (members of the pack), cell, columns, chars (main or big)), cv_nds (DS Castlevania: records of a 2-byte key and 1-bit rows; params cell, advance, columns, chars), vagrant (Vagrant Story's 12x12 4bpp font file VSFONT.FNT that the workspace assembles from the glyph texture and the width table; param set: 0 regular, 1 italic; layout: "credits" opens the staff-roll fonts in ENDING.PRG: 16x16 cells, texture and widths offsets, set 0 italic, 1 bold), retro_font_gx (Metroid Prime Trilogy: a Retro FONT followed by its C4 TXTR, as the workspace unpacks them; glyphs are bit fields of the texture's 4-bit index, edited in place, none added), retro_font_gc (the same file for the GameCube Metroid Prime, FONT versions 0 to 2 with 46-byte glyph records and any GX texture format; each glyph rectangle is edited as decoded RGBA and only changed cells are written back; version 4 files go to retro_font_gx), policenauts (Policenauts: the proportional 2-bit 12-pixel font at the start of KANJIFNT.* / KPRFONT.*; a glyph's width is its advance, glyphs may not outgrow the font's space), retro_font (Metroid Prime 4 and Prime Remastered: a Retro FONT followed by its R8 distance-field TXTR pages, as the workspace unpacks them; params.page picks the language set, 0 = Latin; glyphs are edited in place, none added), pgf (PSP PGF, revision 2: the system font format sceFont reads; a changed glyph is encoded again from its new ink, the glyph data laid out anew; codes are Unicode), lunar (1-bit 16x16 glyph table inside a program, Lunar: Silver Star Story Complete; params offset, count, chars or first_code; the game measures widths by ink, so only pixels are saved; Lunar 2 adds rows (15), row_order (little) and widths_offset / widths_first for its 4-bit width table, which is then saved too), mof_font (Castlevania: Mirror of Fate 3DS .bfont: a CGFX atlas with an MST table of glyph boxes; a code is 0x1F + the glyph index), mt_font (Capcom MT Framework 3DS fonts, Monster Hunter: a GFD or lfd table of glyph boxes with one MT TEX page as companion and params.page; a new letter gets its own record and a box in free room of the page), fragile_dreams (Fragile Dreams FONT: 256 single-byte codes, each a glyph box in a TPL page; a bigger glyph goes into free room of its page), xf (Level-5 3DS font, an XPCK of an IMGC texture with three glyph layers and FNT.bin; Yo-kai Watch: new characters go into free room of the texture, which grows up to its padded power-of-two height when full), is_obd (Infinite Space: the 8x16 dialogue font, pairs of OBJ tiles in an .obd sprite file; params.first, count, chars), dq9 (Dragon Quest IX: the fi_*.bin glyph list with its fd_*.bin pixel strip as companion; glyphs are matched by name, so a letter typed into one of the params.spare empty cells becomes a new glyph named by its UTF-8 bytes), zelda3 (the dialogue font of the A Link to the Past PC port snesrev/zelda3: the font.png its restool.py writes, 8x16 2bpp glyphs in rows of 16 (128, or 256 in the Ukrainian workspace font) with width markers; param chars: the game's alphabet in glyph order, entries longer than one character are icons), pmd_font (Spike Chunsoft 3DS fonts of the Mystery Dungeon games: a .dic glyph table with its .img atlas as the companion; a character typed into a spare cell gets a new glyph box in the free part of the atlas), cells (fixed-size glyph cells one after another in a byte range of a file, the DS-style fonts of The Tale of Two Towns' font_data.bin: params offset, count, cell [w, h], bpp (4), columns, chars (the characters of the cells in order), advance, tiles (a 16x16 cell as four 8x8 tiles; "columns": top left, bottom left, top right, bottom right), widths (the byte offset of a width table, one byte a cell, saved back)), jade (Rayman Raving Rabbids 1/2: a .jfnt file of the workspace — the Jade FONTDESC item, its 4/8-bit texture item and palette; glyphs are redrawn inside their texture rectangles, none added), brfna (a Wii archived font RFNA: its sheets are CX-Huffman-compressed; pixels and widths are edited, new characters are refused), swf_font (a vector DefineFont3 font of a Flash / Scaleform movie, params.font picks it by name; glyphs are drawn into cells and an edited cell is saved back as rectangle shapes, none added), rrr_errfont (the Rabbids 2 disc error font err_msg.fnt with err_msg_8bits.tex as the companion), fntg (EA FntG of the Wii: a glyph table and a C4 SHPG page; glyphs are edited in place and a glyph can take another character), g4font (Level-5 Switch G4 font: font.cfg.bin with its RGBA8 font.g4tx as the companion, four glyph layers; params.font picks the main font 0, the furigana font 1 or a second style 2/3; new characters go into free room of the texture), tmp_sdf (a Unity TextMesh Pro SDF font asset as the workspace unpacks it: the type tree in JSON with the atlas PNG as the companion; a cell is the glyph's atlas rectangle with its padding, a redrawn cell goes back into that rectangle, a width is the horizontal advance; no characters are added — Pokémon Brilliant Diamond / Shining Pearl). bfotf also opens a plain .otf / .ttf file (a Unity Font's data); ttf is the same model under its own name for a PC game's .ttf / .otf, and a character it lacks can point at an existing look-alike glyph and saves it unscrambled), path (a path or glob relative to the project's source folder, or a list of candidates — the first that matches wins; ../ may climb out; a single-file project's file is used as it is), optional member (a glob of files inside the archive at path, or a path through archives inside it: dat/file_select.arc/font/*.bfn), optional companion (a second file the font needs, relative to the source folder — the G4 font's texture; both are read and saved together), font_map (the width map the font feeds; the editor writes it to <project>/font_maps/ on save and the width checks read it) and params (the format's game constants: the cell grid and code page of a G1T atlas and, when the game keeps the advances in its executable, widths (values: the game's table, scale: game units per atlas pixel, patches: {"exefs/<build id>.ips": "<address of the table in that build>"} — saving writes one IPS32 patch per build into the translation folder), the ROM id, dmadata files and width-table offset of an N64 font, which size of a G1N file; BFN, BFFNT and the 3DS formats describe themselves, but a BFFNT source may set min_sheets: the font opens with blank sheets up to that count, and a sheet that gets glyphs is saved as a new texture layer with its characters added to the font's character map — for a font with no room for a new alphabet). The format code in core/font_formats/ holds no game constants. A file that holds both text and a font (an N64 ROM) keeps the font edits when the text is saved: the host carries them from the translation copy into the rebuilt file. A translation_map.json in the plugin folder ({"Ж": "Æ"}: the translated letter is drawn with that slot's glyph) seeds the editor's map for a project that has none; fonts that map Unicode characters (bffnt, bcfnt, qbf, gzf, g1n) take the real letters instead.

texture_sources.json (or the hook get_texture_sources()) — the game's textures that show text (title cards, menu labels, logos), so the Textures window (Tools → Textures…) lists them for a project, exports them as PNG and writes the redrawn PNG back into the translation copy. Each entry: label, kind (title_screen, area_card, menu_label, …), format (bti and tpl (GameCube/Wii), tpl_ctr (the little-endian TPL of the Xenoblade Chronicles 3D port: each image a PICA texture, RGBA4 / RGB565 / LA8 / ETC1 / A8, stored bottom row first), bflim (3DS BFLIM/BCLIM and Wii U BFLIM; the picture reads as stored, top row first, with no parameter; params.flip turns it over after the crop, and no plugin sets it), ctpk (3DS), fr3 (Jak and Daxter level files: RGBA8, BC1 and BC3 records), xenos (Xbox 360 2D-tiled pictures, Hard Corps: Uprising), layton_bg, layton_ani, layton_arj, layton_cani, layton_gfsp, layton_limg (Professor Layton DS pictures), bch (3DS BCH of Capcom's Ace Attorney Trilogy: the textures are PICA register writes of the GPU command block, ETC1 / ETC1A4 / RGB565 / RGBA8, written in place), rtex (Agatsuma RTEX of Code of Princess: 3DS PICA RGBA4 / ETC1A4 bottom row first, Switch zlib RZ data with RGBA8 / RGB8 / LA8 or PICA tiles; an edited Switch texture is compressed again), jimg (One Piece: Unlimited World Red 3DS: Ganbarion jIMG / .jtex, ETC1, ETC1A4 or A8, one picture, no mip levels), nlg_tex (Luigi's Mansion: Dark Moon: the Powe texture table and pixel member of a Next Level Games .dict / .data bundle, ETC1 / ETC1A4 with mip levels made again), gmio (Digimon World: Re:Digitize Decode GMIO: one PICA picture behind a 0x80-byte header), ztex (Level-5 ztex pictures of Fantasy Life: PICA textures with their file names, alone or in a UI container), sh2tex (Silent Hill 2 PC .tex / .tbn2 palette pictures, 8-bit indices into 256 BGRA colours), tga (uncompressed 24 or 32 bpp TGA, the Silent Hill 2 Enhanced Edition font atlas; its header is kept on write), pw_txp (Peace Walker *.txp packs of DDS textures, linear DXT1 / DXT5), ubiart_tex (UbiArt .tga.ckd / .png.ckd textures of the 3DS builds: PICA ETC1 / ETC1A4, bottom row first), cgfx (3DS CGFX .bcres/.bctex and Fire Emblem Awakening .r telops: the TXOB textures of the DATA block, every PICA format; level 0 written in place, mip levels made again from it), dmp (Dragon Quest VII 3DS: a 16-byte header and one PICA surface), ctxb (Grezzo 3DS: CTXB files and the textures inside CMB models), stex (Atlus 3DS STEX, Shin Megami Tensei IV: one PICA texture per file; the GL type / format pair names the pixel format), bntx (Switch), g1t (Koei Tecmo), txtr_gx (Retro TXTR of the Wii Metroid Prime games, decompressed: every GX format, palettes kept when the colours fit), gim (PSP GIM, alone or as the members of an FCHN pack: index4/index8 with a palette, RGBA5650/5551/4444/8888, the PSP swizzle; a palette picture keeps its palette when it holds every new colour), txtr (Retro TXTR of Metroid Prime 4 and Prime Remastered as the workspace unpacks it: R8, RGBA8, BC1–BC5 and BC7 written in place; ASTC read, and an edited ASTC texture is stored as RGBA8), imgc (Level-5 3DS .xi, alone or as a member of an XPCK .xa/.xc pack), j3d (the TEX1 textures of a GameCube/Wii .bmd/.bdl model, e.g. the Twilight Princess title logo; params.texture picks them by name), gba (GBA 4bpp tiles in LZ77 blocks of a packed multiboot program: the gba_tiles program keys plus textures: {name, block, room, offset, tiles, columns, palette, bank}), gtx (Wii U GX2 textures with their mip levels redrawn, also inside a gzip-compressed TMPK pack such as Twilight Princess HD's X.pack.gz), tim (PlayStation TIM, 4/8-bit with its palette or 15-bit, one or several back to back as in Vagrant Story's .DIS; params.clut_row picks the palette row, a TIM without palette shows grey levels; a changed pixel takes the nearest palette colour), tim2 (PS2 / PSP TIM2 pictures, 4/8-bit with their CLUT or 16-bit, alone or inside another file — every TIM2 found in it, e.g. in a program or a subtitle file; the PS2 CLUT order and alpha range are read; a changed pixel takes the nearest CLUT colour), policenauts_pak (Policenauts picture packs: row-RLE pictures in 15-bit colour or a 16-colour palette, encoded again when edited, the pack rebuilt around them), png (a PNG that a build converts into game data itself: the graphics sources of a decompilation (a pret decompilation through rgbgfx) or the textures a workspace unpacks from Unity bundles with UnityPy; saved in the original's kind: a 1-bit picture stays 1-bit, a grey one stays grey with every pixel snapped to its bit depth's grey levels or to params.levels greys (4 for the Game Boy), a palette picture keeps its palette, anything else is RGBA; with texture_grid it is also a font grid), pcx (Metal Gear Solid PlayStation PCX, 4-bit planar or 8-bit; an edited image is mapped to the texture's own palette and compressed again, so the file may change size; plain 8-bit, 4-bit and 24-bit ZSoft PCX are read too), m2 (every texture of an M2 PSB file: RGBA8 stored B, G, R, A, A8L8, A8, L8; written in place), g4tx (Level-5 Switch G4TX texture set: NXTCH textures, RGBA8, BC1, BC3, BC7), shpg (EA SHPG shapes of the Wii: C4/C8 with a palette, RGB5A3, RGBA8, CMPR), bfres (the FTEX textures of a Wii U BFRES file, e.g. Paper Mario: Color Splash UI pictures; cube maps and SNORM textures are left out), mibl (Monolith Soft MIBL textures of the Switch Xenoblade games, .witex), ctrt (Kingdom Hearts 3D .ctt pictures: a 0x80-byte header and one PICA texture), l2d (the CTRT pictures kept inside a Kingdom Hearts 3D .l2d layout), wilay (the MIBL textures of a Xenoblade .wilay layout, written back in place, and the JPEG of a tutorial picture layout), ctxr (MGS2 / MGS3 Master Collection .ctxr: the Switch NVN texture and the PC TXTR version 7), phyre (a PhyreEngine .NXGL.phyre file with one texture, Dragon Quest Builders 1: ARGB8, ARGB4444, DXT1 or DXT5, bottom-up rows, mip levels written again; a pack of many textures, the Builders 1 uitex/uiicon sprite packs, is read too: the format names in use and the fix-up groups give each texture's format), is_tex (Infinite Space .tex 3D textures with their palette), dq9_spr (Dragon Quest IX .spr pictures), jade (Jade engine textures of Rayman Raving Rabbids 1/2 as .jtex: 4/8-bit with a palette, TGA RGBA8, DDS DXT1/3/5; an edited 4/8-bit picture takes the nearest palette colours), jade_jtx (Jade Montreal JTX textures of TV Party as .jtx: DXT1, DXT1 + alpha, C4/C8 with a palette, BGRA8; mips written again), raw (headerless pixels at known offsets, e.g. N64 textures: params.pixel_format such as n64:IA8 or snes:2bpp / snes:3bpp / snes:4bpp (SNES planar 8x8 tiles, colour indices shown as grey; 3bpp is A Link to the Past's layout), width, height, and offset or a list textures of {name, offset}; n64:CI4/CI8 and psx:CI4/CI8 index a palette at tlut_offset, psx:RGB555 is PlayStation 16-bit, psx:4bpp/8bpp indices without a CLUT, saturn:1bpp_12x12 1-bit 12x12 glyphs one after another), vs_gim, vs_hf1, vs_rle (Vagrant Story screen tile maps, help-page sprites (params.sprites) and the RLE title screen (offset, end, width, height, textures rectangles); a picture that no longer compresses into its place is refused), sotn_blocks / sotn_cmp (Symphony of the Night PlayStation pictures: the 128x128 VRAM tile blocks of the F_*.BIN files, bpp, blocks, clut or palette; an image packed with the game's nibble coder at offset, width, height, room, packed again on save and refused when it no longer fits), sh3_tex (Silent Hill 3 PC pic.arc textures: PS2 GS 8-bit with a palette and 32-bit RGBA; other header kinds are not read yet), mt_tex (Capcom MT Framework 3DS TEX: one PICA picture with its mip levels, Monster Hunter), pmd_img (Spike Chunsoft 3DS .img pictures of Pokémon Mystery Dungeon: Gates to Infinity and Super Mystery Dungeon: a 128-byte \0cte header and one PICA surface stored bottom row first; RGB8, RGBA8, RGBA4, ETC1, ETC1A4, A8), ffta2_efx (the DS 3D textures of Final Fantasy Tactics A2 *.efx effect files: A3I5 / A5I3 with their own palette; a changed texel takes the nearest palette colour), tiles (GBA / DS / Saturn 8x8 character tiles of 1-, 4- or 8-bit palette indices, shown as grey levels: an NCGR takes its size from its header; headerless tiles take bpp, offset, tiles, sprite cells cell [w, h] in tiles and per_row; colours: palette (the game's BGR555 bytes as hex), bank and banks (one hex digit per tile, . = bank); nibble: "high": the first pixel of a 4-bit byte is its high nibble (Saturn); 1-bit tiles are 8 bytes, the first pixel in the high bit; linear: a plain bitmap of per_row x 8 pixels a row instead of tiles, an imported off-palette colour takes the nearest colour of the tile's bank), tilemap (a pattern name map over a bank of 8x8 cells of 4- or 8-bit indices, shown as one picture per map: Saturn VDP2 screens such as the Symphony of the Night title and menu screens; map_offset, cols, rows, cells_offset, bpp, nibble, unit (bytes per cell number), name_mask, pal_shift / pal_mask (the palette number in the name), hflip_bit / vflip_bit, blank, palette, max_tiles and textures for several screens over one bank; a redrawn block reuses an equal cell, is redrawn in place when only its map entry uses the cell, else takes a free cell or grows the bank up to max_tiles)), path (as in font_sources.json; {a,b} alternatives allowed; the folder part may hold a glob too, data/map/*/*_en.xc), optional member (a glob inside the archive at path; archives inside archives are walked — Title.szs/timg/*.bflim; an N64 ROM's dmadata files are #<index>; Grezzo ZAR/GAR archives are archives too) and params: compression of the member (or of the file when there is no member: zlib, gzip, yar; Yaz0, zstd and Grezzo LzS are found by their magic), file_offset + file_size (the texture file is that byte range, for packs indexed elsewhere) and texture (a glob of texture names in a file that holds several). The pixel format comes from the file itself; the code in core/texture_formats/ holds no game constants. A texture keeps its size; only the 4x4 blocks or tiles that changed are encoded again, so an unedited texture writes back byte for byte. A file that holds both text and pictures (a program with its title screen) keeps the edited pictures when the text is saved: the host carries them from the translation copy into the rebuilt file.

translation_prompts/prompts.json — may hold only the sections the game changes (usually translation); the rest is merged in key by key from plugins/common/defaults/prompts.json. Never add an instruction that permits changing tags.


File formats and the state that goes with them

get_file_formats() returns core.formats.FileFormat(extensions, mode, label) items. mode is the shape in which a file's content reaches load_data_from_json_obj and leaves save_data_to_json_obj: "json" (parsed), "text" (a string) or "bytes". The default is .json + .txt. A game with its own table returns [FileFormat((".tbl",), "bytes", "Text tables"), *DEFAULT_FORMATS] and parses the bytes itself; project import, loading, saving and the open/save dialogs follow without host changes. An archive format is added with ContainerManager.register(MyContainer, extensions=(".pak",)).

includes_source_file(rel_path) decides whether a source file becomes a block. rel_path is relative to the source folder and uses /. The default is True. A game whose unpacked tree holds the same text in many languages returns True only for the language it translates. The other files stay in the source folder: a build can still read them, and load_multi_reference can show them as reference columns. A block whose file the hook refuses leaves the project list at the next sync; its translation file stays on disk.

unpack_reference(path, project_dir, progress, is_cancelled) reads a reference the user picked as a file (a game image of another region). The host calls it on a worker thread. The plugin writes what load_multi_reference needs into a folder of the project and returns that folder; the host stores the folder as the reference path. Return None to use the path as it is (the default). Write into a temporary sibling and rename it into place, so a cancel leaves the older folder. load_multi_reference then reads only that folder: it runs on the UI thread and must stay fast. Example: plugins/super_mario_sunshine/reference.py writes only the BMG files of the European disc.

reference_worker_rules() (optional) lets the host run load_multi_reference on a worker thread. It returns (rules, window), built on the window's thread: rules is an object of the same game that holds the state the parse reads (a copy, taken now); window is a copy with its own tag mapping that the parse fills (the host copies its new aliases into the editor's mapping on the window's thread), or None. Default None (or no method): the parse runs on the window's thread. The text views refresh when a parse ends. Twilight Princess gives a copy with its own tag mapping. system_3ds and manual_3ds give an object with the project's files or manuals taken at that moment. A session without plugin state reads its files again and then parses the reference again (manual_3ds: the English follows the manuals read).

default_reference_path() optionally supplies a reference source when the user has not configured one. Return metadata only, without filesystem access. Pair automatic discovery with reference_worker_rules() so archive indexing and table parsing stay off the UI thread. The default None keeps explicit reference selection. The automatic path is not saved as a user preference. Yo-kai Watch uses a private snapshot of project source paths to read loose tables and existing ARC0 languages; empty or unmatched rows produce no tab.

Inside the application the text is List[List[str]] — blocks of strings — plus block names. load_data_from_json_obj returns (blocks, block_names); save_data_to_json_obj(blocks, block_names) returns what goes back to the file. Unknown fields of the format must survive the round trip.

If saving needs something the strings do not carry (table keys, the parsed binary file), keep it in the plugin and implement export_runtime_state() / restore_runtime_state(state) / reset_runtime_state() — plain JSON data that the host keeps across reloads, reverts and sessions — and prepare_save_context(context), called before each project file is built (context.block_indices, context.runtime_state, context.existing_versions() yields the file's current bytes, translation first). A session written before the plugin kept any state restores none: the host then reads the project files once on a worker for the state only (the restored strings and edits stay) and scans the issues again. The host never reads plugin attributes. Examples: plugins/pokemon_fr/rules.py (keys), plugins/manual_3ds/rules.py (one manual per block), plugins/zelda_bmg/rules.py (a binary file patched on save).

A session holds the message rows, so a game whose rows change meaning declares a version: session_data_layout() returns a number, and the session manager stamps it on each saved session. A session stamped with another number, or with none, goes through migrate_session_snapshot(snapshot). The hook maps the rows and returns the snapshot with the new stamp; without the stamp the session is refused and the project opens from its files. The default (None) adds no stamp and no migration.

The host parses a source file and a translation file with the same load_data_from_json_obj. When one file can hold both the original and the translation (several languages in one file, read in place), implement set_load_role(role): the host calls it with "translation" before it parses a translation file (the project load, the changes file, the re-parse after a save or a revert, the Project Manager's counts) and with "source" after. Example: plugins/manual_3ds/rules.py (en for the original, ua for the translation).


Capabilities and the game's own data

Everything above is the required minimum. Beyond it a plugin can mine the game's own data — message attributes, dialogue graphs, scene tables — or an outside lore source, and feed that into AI translation, glossary building and the Story Timeline. All of it is opt-in; every hook has a safe default.

If you have What becomes possible
A decompilation or the game's source window kinds, dialogue flow, scene tables, actor placement
Raw game files (message archives, stage data) the same, by parsing the binaries
A community wiki lore lookup for glossary descriptions
A fan script or walkthrough scene and speaker structure for MemPalace
Only the extracted text nothing here is needed — the text sweep still builds a glossary

get_capabilities() -> Set[str] tells the Localization Pipeline which steps to offer. Declaring nothing is a complete answer.

Name Hook it promises What appears
glossary_seed get_glossary_seed_entries() Glossary terms taken from game data without an AI request (term, description, section, icon, source_ref, blocks). The hook reads only self.mw.project_manager: a headless seed run (scripts/seed_series_glossary.py) gives that attribute and nothing else.
external_lore get_external_lore(term) Outside knowledge for glossary descriptions
speaker_attribution get_speaker_for_string() The Name the speakers step and the Speaker field
message_window_preview get_preview_window_style() and the get_window_* hooks In-game message windows in the preview, per-window limits in Settings
strict_tags — An AI reply that loses, adds or changes a {tag} of its source is refused and asked again (games whose control codes all matter)

Series tags (get_series_tag_map(), not a capability). A plugin that returns a TagMap (plugins/common/series_tags.py) gives each of its control tags a kind: a line break, a page, a scroll, a wait, a colour, a name the game inserts, a number, a word form, or a kind that names the tag itself. When two games of one series have the same kinds in the same order, a translation saved in one carries to the other, and its tags are written in the other game's syntax. A tag without a kind blocks the carry-over for that text, and the memory bar says why. Without the hook, a series row matches only the same text.

Do not declare a capability the plugin cannot deliver.

Slots, not values. get_translation_context_for_string returns metadata under keys the engine publishes — window_type, content_role, role_instruction, has_speaker, glossary_section, force_glossary, layout_class ("prose": Picoripi breaks the lines of each page itself; "lines": one source line is one output line — menus, names, choices; omitted: guessed from the source text). The values are the plugin's: content_role may be "BossName" or anything the game needs, and role_instruction is the sentence that tells the model what the role means. The engine never compares a value to a specific game, and engine code must not learn one.

Speakers. get_speaker_for_string returns the identity the game data records; the engine fills rows the user has not set and never overwrites a user's choice. is_placeholder_speaker(name) says whether that identity is an internal id the script-merge step may replace with a real name (default: yes).

Block tree folders. get_virtual_folders(block_idx, text_of) gives folders shown under a physical block in the tree. Each node is {"title", "rows", "children"}: a node with children takes its rows from them, a leaf lists (block, string) rows, and a row may sit in several folders. Selecting a folder fills the strings list with its rows in the order given. text_of(string) returns the current text of a string of the block, or its original when the translation is blank, so a title the translator edits shows after the tree rebuilds (a save or a rebuild of the tree). Build the nodes from data in memory: the call runs on the UI thread. plugins/manual_3ds/folders.py is the reference.

Message windows. With message_window_preview: get_preview_window_style(block_idx, string_idx) gives the style of a message; get_window_presets() / get_window_preset_label() / get_window_style_for_preset() feed the bar that forces a window kind; get_window_frame(style) returns geometry and picture read from the game's files; get_window_item_icon() and get_window_text_offset_y() cover the item slot and the game's vertical centring; get_window_layout_groups() / get_window_layouts_document() / save_window_layouts_document() drive the Settings table of per-window limits. get_string_layout(block, string) gives per-string width, font and lines per page (priority: per-string override > this hook > global settings). fit_text_to_window(text, block, string) returns a translation re-wrapped with the game's own line and page codes (None: the generic wrapping); the AI translation uses it when it applies a reply, and a plugin may call it from its Autofix. A font source with "preview": true is read from the project's own files: the host measures widths with it (its font_map name, unless the font editor wrote one) and draws the bitmap preview with its glyphs, for any format in core/font_formats.

Repeated texts (D46). The duplicate fold keys a repeated text on its window and translation context. get_duplicate_context_for_string(block, string) may return a JSON-serialisable dict instead: it replaces those two values in the key, and the speaker, the addressee and the user's overrides stay. Return the same dict for every place that may share a translation (the 3DS manuals return a constant one, so a paragraph in two manuals is translated once). Return None for the default key. Before a repeated, memory or series translation is written to a place, get_text_fit_problems_for_string(block, string, text) lists the layout problems that stop it from fitting that place (ids of your own choosing, for example WORD or LINES); an empty list means it fits. Only layout belongs there: a check that needs a page render or the editor's current texts stays with the issue scan. A refused place stays untranslated after the run. A memory row saved before the dict existed keeps working at its own place.

Custom preview. get_custom_preview_widget(main_window) returns a QWidget that draws the current text in the game's own viewer, in place of the BFN preview; None keeps the BFN preview. The host docks the widget in the preview column, hides the BFN preview, calls refresh() after each selection change and text edit, and calls shutdown() before it deletes the widget. Read the project through main_window; the widget must not write to it. plugins/manual_3ds/preview_panel.py is the reference.

MSBT tag names from MSBT Editor. plugins/common/gcf.py reads the game configs of AeonSake's MSBT Editor (.gcf) and lays their names, descriptions and value names over a catalogue of plugins.common.lms_tags (gcf.overlay); the argument layout stays the one verified against the game's text, and renamed tags keep their old names as aliases (TagCodec(..., aliases=...)).

Reference implementation. plugins/zelda_bmg/ (Twilight Princess) reads the game's files and a decompilation: message attributes that identify the window and therefore the role of a message (window_kinds.py), dialogue flow graphs (msg_flow.py), scene tables (stage_data.py), per-window layout, a preview with colours, scaling and icons, window frames from a local dump (window_frame_loader.py). Copy the approach — find the field in the game's data that already encodes what a message is, and expose it through a hook — not the tables.


Project Manager: roles, unpacking and build targets

The Project Manager's new-project wizard, its Build, Export and Run buttons and its list icons use six optional hooks. Each hook has a default, so a plugin without them still works: the wizard then takes the role and the platform from the user, uses the files as they are, and the buttons stay off.

These hooks run on a worker thread, except project_icon, which runs in the scan process. They must not create widgets or read self.mw. The host passes progress and is_cancelled; check is_cancelled() in long loops. The host builds into a temporary folder and renames it when the build succeeds. Export copies the built folder to a folder the user chooses. Run starts the emulator command the user set in the Project Manager, with the built path as an argument.

plugins/plain_text/rules.py is the reference: its single target copies the translation files as they are.

Game images: core/project_library/images.py reads GameCube discs (NKit GameCube discs too) and Nintendo DS cartridges by their header. Before unpacking a game image the host checks that the target drive has room for it; a plain file, such as a GBA ROM, is used as it is. Call identify or base_identity(path, game_codes) in identify_source, and extract(path, target_dir, progress, is_cancelled, select=...) in unpack_sources. The game's unpack.py lists the files it keeps, as its zt script did; plugins/zelda_fsae/ is the short example. ImageError means a format the helper does not read; let it reach the host, which shows the message.

Families of versions (family_plan)

A game sold in several versions (for example GameCube, Wii and Wii U HD) can share one translation. family_plan(sources, project_dir) gets the folders or files the user gave and returns a core.family_projects.model.FamilyPlan, or None when the game has no family (the default). The plugin recognises each source by its content, not by the user's folder name: Twilight Princess uses the message count of its files as a fingerprint (plugins/zelda_bmg/family.py). The plan lists the members (FamilyMember(key, label, path, base, parents)), the base version, and read(member_key), which returns (unit_id, text) pairs.

Core (core/family_projects/) links the units of each member to the units of its parents by equal match key (the text by default), stores one translation per shared unit, keeps the differing texts as per-member overrides, and fans the texts back out. A member may link to several parents in order: a member that shares most of its text with a second member links to that one first. Fewer sources than members is valid. The Twilight Princess project builder (plugins/zelda_bmg/family_project.py) writes one project. Each unit tree (one message and its linked copies) is one project message, in the folder "Shared" (a base-rooted tree of more than one version), "Shared HD Msguk" (the same for a tree rooted in HD Msguk), or "

Tests for a plugin

The generated test file is the start. Add, with real data: a sample that parses into the expected blocks; a save round trip that keeps every field; valid and invalid tags; a width warning on a deliberately long line; autofix returning (str, bool) without damaging tags. plugins/testing.py has the shared checks (check_loads, check_round_trip, check_validator).


What not to do