November 4, 2025Nov 4 Hi all — I used the script from this thread to extract text from .EXP files: https://reshax.com/topic/18272-ios-cause-of-death-episode-file-exp-lzma-decompression/ It worked and I can extract the plain text lines, but the `.kiw` files contain additional bytecode that presumably correspond to in game instructions like character dialogue expressions, choices, background images and music cues. I am stuck on how to reverse engineer the bytecode, and the best way to do so. I am not sure of the game engine that made this game but if anyone knows of existing parsers or scripts for `.kiw` or similar, please direct me to anything helpful. I've attached some sample kiw files for reference. I have no idea where to start kiwfiles.zip
October 13Oct 13 Localization I took a deep dive into the 17 .kiw files you provided (file1.kiw to file17.kiw).Here is the complete reverse engineering report, a breakdown of how the bytecode handles dialogue, expressions, choices, and backgrounds, and a standalone Python disassembler that parses all 17 files with 0 errors.1. Engine & File Layout (15-Byte Header)The engine is EA's proprietary Kiwi Engine (used in episodic narrative games like Cause of Death and Surviving High School).The file header is only 15 bytes long (not 128 bytes):OffsetTypeValueMeaning0x00char[4]kiwiASCII Magic identifier0x04u16 LE2Format version0x06u80Reserved0x07u8VariableStatement/block counter (roughly size / 640)0x08u32 LEVariableUnique Module ID per file0x0Cu80 or 11 only in file1.kiw (Menu module), 0 in story chapters0x0Du16 BEVariableCompiler Instruction Count (Oracle field)0x0F--String Pool Start (NUL-terminated ASCII)There is no section pointer table. The string pool starts directly at offset 0x0F and runs until the first non-ASCII byte (>= 0x80), after which the raw bytecode stream continues to EOF.2. Chapter Mapping (file1 to file17)All story files (file2 through file17) share an identical 10-byte prologue whose only variable byte at code offset 10 is the chapter index:1a 03 e9 | 1f 2d 01 | 21 | 03 5c | 02 <chapter> file1.kiw: Main Menu / Global Module (chapter = 0, includes an extra 46x8-byte record table).file2.kiw to file17.kiw: Chapters 1 through 16 in exact order.3. How Dialogue, Choices, and Assets WorkA. Dialogue & Character ExpressionsThe VM is a classic Stack Machine (operands are pushed left-to-right, then call pops argc arguments). Dialogue is handled by Method 0x41 (say), which appears 1,658 times across the files, always with argc = 3:push speakerId ; u16 BE (e.g., Det. Mal Fallon) push expressionArtId ; u16 BE (portrait / emotion sprite ID) lit5a ; pulls the dialogue text from pool cursor call say argc=3 ; invokes method 0x41 Character art, emotion variants, and expressions are addressed by numeric asset IDs (e.g. 2000-block, 25000-block, 26000-block), not filename strings.B. Choices & DecisionsChoices are packed directly into the string pool using the pipe delimiter | (145 choice strings found across the files). For example:"Good news.|Bad news.|You decide." When a choice occurs, the prompt and options are pulled from the string, displayed to the player, and the user's selection branches via methods 0x34 / 0x31 followed by branch (opcode 0x28) or goto (opcode 0x29).C. Background Images & MusicStaging and scenes are invoked by method 0x0D (stage) and method 0x4B (bg) using numeric asset IDs rather than raw filename paths.D. String Pool ArchitectureThe pool contains 5,447 clean NUL-terminated 7-bit ASCII strings.Pool order == Screenplay order: Narrative text appears in the exact order the story unfolds.Text is consumed sequentially by VM cursor opcodes (lit5a / lit5b). Strings 0..7 in every chapter belong to the shared UI/score screen, while narrative text starts at index #8.4. Bytecode Opcode ArchitectureThe instruction set uses 1-byte opcodes followed by 0, 1, or 2 operand bytes (operands are Big-Endian):OpcodeOperandsMnemonicDescription0x1Au16 BEpushPush immediate integer / ID / threshold0x1Fu8, u8callCall VM method (method_id, argc)0x1Bu16 BEstoreStore value to slot / variable0x41s16 BEloadvarLoad variable (negative values = stack frame slot)0x2Bu16 BEsetvarSet state flag / variable0x28u16 BEbranchConditional jump0x29u16 BEgotoUnconditional jump / scene transition0x02u8chapterChapter identifier (1..16)0x5A / 0x5BNonelit5a / lit5bPulls next string literal from pool cursor0x43NoneretModule terminator (closes every file)0x22NoneepilogueMarks the start of the chapter closing block0x21NonedupDuplicate stack top0x3FNonedropPop / discard stack topIndependent Mathematical Validation: Header byte 0x0D stores the compiler's expected instruction count. The disassembler decodes 34,620 instructions across all 17 files with delta = 0 in 17 / 17 files (99.8% push alignment, only 0.058% unmapped edge bytes).5. Standalone Disassembler Tool (kiw_disasm.py)I wrote kiw_disasm.py (Python 3.8+, standard library only) to batch-disassemble all 17 files into clean screenplay text listings and structured JSON trees.Usage:python kiw_disasm.py # Batch disassembles all file*.kiw -> out/*.txt and out/*.json python kiw_disasm.py file3.kiw --txt # Screenplay + disassembly text only python kiw_disasm.py --pool file2.kiw # Dumps clean string table / script only Outputs:Screenplay section: Formatted script with speakers, dialogue lines, and expanded choice options:[# 15] UI -: Make your choice! [# 16] CHOICE -: "Good news." -> option 1: "Good news." -> option 2: "Bad news." -> option 3: "You decide." Disassembly listing: Linear bytecode trace with jump labels (Label_XXXX).JSON tree: Full structured export containing headers, string entries, and instructions.(The tool script and README are attached to this post). KiwiEngine_tool.zip
Create an account or sign in to comment