Nihuya sebe

This commit is contained in:
2026-07-17 01:04:47 +03:00
parent bbaa21d14e
commit cd8e743429
29 changed files with 15952 additions and 16 deletions

31
.gitignore vendored
View File

@@ -1,18 +1,19 @@
# ---> Rust
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Build artifacts
/src-tauri/target/
/node_modules/
/dist/
# These are backup files generated by rustfmt
**/*.rs.bk
# IDE
.vscode/
.idea/
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# OS
Thumbs.db
.DS_Store
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Logs
*.log
# Lock files (optional - remove if you want reproducible builds)
# package-lock.json
# Cargo.lock

223
FORMAT.md Normal file
View File

@@ -0,0 +1,223 @@
# Megami Ibunroku Persona — Event Script Format Documentation
## Overview
The game stores event/dialogue scripts inside `ADV\E0.BIN` on the CD.
This file is a **container** holding 224 sub-files (individual event scripts).
---
## 1. CD Image (BIN/CUE)
- Format: MODE2/2352 (raw CD-ROM XA)
- Sector size: 2352 bytes
- User data offset: 24 bytes from sector start
- User data size: 2048 bytes per sector
The game uses internal file tables (not ISO9660) to locate files:
- **FSECT** (LBA 634): array of uint32_le, each entry = starting LBA of a game file
- **FSIZE** (LBA 623): array of uint32_le, each entry = size in bytes
- E0.BIN is at index **273** in both tables
---
## 2. E0 Container Format
```
┌─────────────────────────────────────────────┐
│ Pointer Table (variable length) │
│ uint16_le[N+1] pointers │
│ Terminated by 0x0000 │
├─────────────────────────────────────────────┤
│ Padding to 0x800 alignment │
├─────────────────────────────────────────────┤
│ Sub-file 0 (aligned to 0x800) │
├─────────────────────────────────────────────┤
│ Sub-file 1 (aligned to 0x800) │
├─────────────────────────────────────────────┤
│ ... │
├─────────────────────────────────────────────┤
│ Sub-file N-1 │
└─────────────────────────────────────────────┘
```
### Pointer Table
| Offset | Type | Description |
|--------|-----------|----------------------------------------------------|
| 0 | uint16_le | Pointer to sub-file 0 (in 0x800-byte units) |
| 2 | uint16_le | Pointer to sub-file 1 |
| ... | ... | ... |
| N*2 | uint16_le | End-of-data pointer (marks end of last sub-file) |
| (N+1)*2| uint16_le | 0x0000 (terminator) |
- Original E0 has **225 entries** (224 sub-files + 1 end marker)
- To get byte offset: `pointer_value * 0x800`
- Sub-file N size: `(ptr[N+1] - ptr[N]) * 0x800`
---
## 3. Event Script Sub-file Format
Each sub-file has:
```
┌─────────────────────────────────────────────┐
│ Sub-file Header (8 bytes typically) │
├─────────────────────────────────────────────┤
│ Script Data Section │
│ ├── Event Command Table (offsets 0x00-0x60+)│
│ ├── Script Bytecode │
│ ├── String Pointer Table │
│ ├── Text Data (strings) │
│ └── Trailing Data │
└─────────────────────────────────────────────┘
```
### Sub-file Header
| Offset | Type | Value | Description |
|--------|-----------|---------|--------------------------|
| 0 | uint16_le | 8 | Header size in bytes |
| 2 | uint16_le | 0x8010 | Marker (always 10 80) |
| 4 | uint16_le | varies | (purpose TBD) |
| 6 | uint16_le | 0x8010 | Marker (always 10 80) |
### Script Data Section
The data section starts immediately after the header (`offset = hdr_size`).
All offsets below are relative to the start of the data section.
#### Key Fields
| Offset | Type | Name | Description |
|--------|-----------|------------|--------------------------------------------------|
| 52 | uint16_le | ett | End of text table / start of text blob area |
| 96 | uint16_le | table_ptr | Offset to string pointer table within data |
If `table_ptr == 0xFFFF`, the sub-file has no dialogue strings.
#### String Pointer Table
Located at `data[table_ptr]`. Entries follow this pattern:
```
FF 55 00 00 [ptr_lo] [ptr_hi] 10 80
```
- `FF 55 00 00` = prefix marker (4 bytes before the pointer)
- `ptr_lo, ptr_hi` = uint16_le offset within data section pointing to string start
- `10 80` = suffix marker
The table continues until a different pattern is encountered.
To find all strings, scan from `table_ptr` looking for `FF 55 00 00 XX XX 10 80` sequences.
---
## 4. Text Encoding
### Single-byte characters (0x01-0x7F)
Originally mapped to Japanese kana. Values 0x01-0x7F are indices into the font file.
| Range | Original Content |
|-----------|--------------------------|
| 0x01-0x2E | Hiragana (あ-ん) |
| 0x2F-0x5E | Katakana (ア-ン) |
| 0x5F-0x7F | Extended katakana + marks |
For Russian translation, these are remapped to Cyrillic:
| Range | Content |
|-----------|--------------------------|
| 0x01-0x42 | А-я (66 letters + Ё/ё) |
| 0x43 | Space |
### Two-byte characters (0x80xx)
High byte 0x80+ triggers a two-byte read. The pair forms a code looked up in the TBL file.
Common punctuation codes:
| Code | Character |
|--------|-----------|
| 80 A5 | . (period) |
| 80 CB | : (colon) |
| 80 CC |   (JP space) |
| 80 D0 | ? (question)|
| 80 D1 | ! (exclaim) |
| 80 D5 | , (comma) |
| 80 E1 | - (dash) |
### Control codes (0xFF xx)
| Code | Parameters | Name | Description |
|-------------|---------------|--------------|------------------------------------|
| FF 00 | none | [ff00] | Unknown/NOP |
| FF 01 | none | (terminator) | End of string |
| FF 02 | none | [end] | End of dialogue sequence |
| FF 03 | none | [nl] | Newline |
| FF 04 | none | [clear] | Clear text box |
| FF 05 XX 00 | 1 byte + 00 | [wait=XX] | Pause for XX frames |
| FF 06 XX | 1 byte | [color=XX] | Set text color |
| FF 07 | none | [firstname] | Insert player's first name |
| FF 08 | none | [nickname] | Insert player's nickname |
| FF 09 | none | [ff09] | Unknown |
| FF 0A | none | [ff0a] | Unknown |
| FF 0B | none | [ff0b] | Unknown |
| FF 0C | none | [ff0c] | Unknown |
| FF 0D | none | [ff0d] | Unknown |
| FF 0E XX | 1 byte | [choice=XX] | Display choice menu (XX = menu ID) |
| FF 0F | none | [lastname] | Insert player's last name |
---
## 5. Font File
- Located at game file index **5** in FSECT/FSIZE
- Size: 65536 bytes
- Glyph size: 32 bytes each (16x16 pixels, 1bpp, 2 bytes per row)
- Glyph index N is at byte offset `N * 32`
- Indices 0x01-0x7F correspond to single-byte text codes
- Indices 0x80+ correspond to two-byte text code second bytes (TBL lookup)
---
## 6. TBL File (Persona_jap.tbl)
Plain text, UTF-8, format: `XXXX=char` per line where XXXX is hex code.
Example:
```
8090=漢
80A5=。
80CB=
80CC= 
80D0=
80D1=
80D5=、
```
The hex code is the 2-byte value as it appears in the script (big-endian in the TBL, but stored little-endian in the binary when the injector tool writes it).
---
## 7. Workflow for Translation
1. **Unpack**: Read ISO → extract E0 → parse pointer table → for each sub-file, parse strings
2. **Edit**: Modify string text (respecting encoding constraints and control codes)
3. **Repack**: Encode strings → rebuild sub-file (update string pointers) → rebuild E0 container → patch ISO
### Constraints when editing:
- New text blob must fit within `sub-file_size - ett` bytes (or sub-file must be resized)
- String pointer table positions are fixed (they're part of the script bytecode)
- Only the text area (after `ett`) is safe to resize
- If sub-file grows, the E0 container must be relocated in the ISO
---
## 8. Statistics (Original Japanese Rev 1)
- E0 container: 1,828,864 bytes (893 sectors)
- Sub-files: 224
- Sub-file sizes: 6,144 - 34,816 bytes
- Total dialogue strings: ~2,500 across all sub-files
- Sub-files with no strings (table_ptr=0xFFFF): ~27

37
Makefile Normal file
View File

@@ -0,0 +1,37 @@
# Makefile for persona-script-editor (Tauri + TypeScript/Vite)
# Requires: Node.js, npm, Rust, MSVC or MinGW, make (GNU Make in MSYS2/MinGW)
PROJECT_DIR := /c/Users/romas/Desktop/Projects/persona-script-editor
SRC_TAURI := $(PROJECT_DIR)/src-tauri
.PHONY: default install build dev clean release help
default: build
install:
cd $(PROJECT_DIR) && npm install
cd $(SRC_TAURI) && cargo install
build:
cd $(PROJECT_DIR) && npm run tauri build
dev:
cd $(PROJECT_DIR) && npm run tauri dev
clean:
cd $(PROJECT_DIR) && npm clean
cd $(SRC_TAURI) && cargo clean
rm -rf $(SRC_TAURI)/target
rm -f *.log
release: build
help:
@echo Available commands:
@echo default - build the application (npm run tauri build)
@echo install - install dependencies (npm install + cargo install)
@echo build - build the final application (Windows .exe/.msi)
@echo dev - run in development mode with hot reload
@echo clean - remove dependencies and target directory
@echo release - alias for build
@echo help - show this help message

2020
Persona_jap.tbl Normal file

File diff suppressed because it is too large Load Diff

181
README.md
View File

@@ -1,2 +1,181 @@
# persona-script-editor
# Persona Script Editor
A GUI tool for translating the PS1 game **Megami Ibunroku Persona** (真・女神転生ペルソナ / Revelations: Persona).
Parses dialogue scripts directly from a BIN/CUE disc image, provides JSON export/import for translators, and writes translations back into the image.
Built with Rust + Tauri. Runs on Windows. Minimal RAM usage (~6MB for script data, not the full 667MB image).
---
## Quick Start
1. Place `Persona_jap.tbl` (or any `.tbl` file) next to the exe
2. Run `persona-script-editor.exe`
3. **Open ISO** → select the `.bin` disc image (Redump Rev 1, ~699MB)
4. Four files appear: E0, E1, E2, E3 — all game dialogue (~12000 strings total)
5. Click a file → scene list → click a scene → strings
---
## Translation Workflow
### For the project lead
```
Open ISO → Export JSON → distribute to translators → Import JSON → Save ISO
```
### For translators
You receive a JSON file. Open it in any text editor (VS Code, Notepad++, etc).
Fill in the `translation` field for each string:
```json
{
"index": 0,
"original": "南条:やあ、[firstname]。[nl]今日もいい天気だな。[end]",
"translation": "Nanjo: Hey, [firstname].[nl]Nice weather today.[end]"
}
```
Rules:
- **Do not remove or modify** control codes: `[nl]`, `[clear]`, `[end]`, `[firstname]`, `[lastname]`, `[choice=N]`
- `[nl]` = line break (max ~20 characters per line)
- `[clear]` = clear text box (next "page")
- `[end]` = end of dialogue
- `[choice=N]` = choice menu
- Maximum **3 lines** between `[clear]` tags (PSX text box limitation)
---
## Control Codes
| Code | Meaning |
|------|---------|
| `[nl]` | New line |
| `[clear]` | Clear text box |
| `[end]` | End dialogue |
| `[wait=N]` | Pause for N frames |
| `[color=N]` | Set text color |
| `[firstname]` | Player's first name |
| `[lastname]` | Player's last name |
| `[nickname]` | Player's nickname |
| `[choice=N]` | Choice menu (ID=N) |
---
## Game File Structure
| File | Contents | Strings |
|------|----------|---------|
| ADV/E0.BIN | Dialogue (main story) | 3878 |
| ADV/E1.BIN | Dialogue (side scenes) | 2242 |
| ADV/E2.BIN | Dialogue (side scenes) | 4632 |
| ADV/E3.BIN | Dialogue (ending) | 1380 |
| **Total** | | **~12000** |
All other files (MES, BGM, BVB, EBG, SE, B/*) contain graphics, audio, or map data. No translatable text.
---
## Technical Details
### Disc Image Format
- MODE2/2352 (raw CD-ROM XA)
- Sector: 2352 bytes total, user data at offset 24, size 2048
- Game uses internal FSECT table (LBA 634) to locate files, not ISO9660 directory
### Container Format (E0E3)
```
[Pointer Table] uint16_le[] × (N+1), terminated by 0x0000
[Sub-files] aligned to 0x800 (2048) bytes
```
Each pointer value × 0x800 = byte offset of the sub-file within the container.
### Event Script Sub-file Format
```
[Header] 8 bytes: hdr_size(u16) + marker 0x8010(u16) + data(4 bytes)
[Data] Script bytecode + string pointer table + text blob
```
Key fields in the data section (offsets relative to data start):
- `data[52]` (uint16_le) = `ett` — start of text blob area
- `data[96]` (uint16_le) = `table_ptr` — start of string pointer table (0xFFFF = no strings)
String pointer entries follow the pattern: `FF 55 00 00 [ptr_lo] [ptr_hi] 10 80`
### Text Encoding
- `0x010x7F` — single-byte characters (kana in JP, can be remapped to Cyrillic/Latin via font)
- `0x80xx` — two-byte characters (kanji, punctuation). Mapped via `.tbl` file
- `0xFF xx` — control codes (see table above)
- `0xFF 0x01` — string terminator
### TBL File
Plain text mapping of byte codes to Unicode characters. Format: `XXYY=char` per line.
Example:
```
80A5=。
80CB=
80D0=
80D1=
80D5=、
```
The editor auto-loads any `.tbl` file found in the same directory as the executable.
---
## Memory Usage
The editor does **not** load the entire disc image into RAM. It reads sectors on demand via file seek. Only the parsed script containers (~6MB total) are kept in memory.
---
## Saving
When saving a translated ISO:
- If the rebuilt container fits in its original location → written in-place
- If the container grew (longer translations) → appended to end of image, FSECT table updated automatically
---
## Logs
Log file: `%LOCALAPPDATA%\com.persona.script-editor\logs\Persona Script Editor.log`
Logs are also output to:
- stdout (when launched from terminal)
- DevTools console (F12 inside the app window)
---
## Building from Source
```bash
cd persona-script-editor
npm install
npx tauri build
```
Requirements: Rust 1.70+, Node 18+, npm, Windows (MSVC toolchain).
Output:
- `src-tauri/target/release/persona-script-editor.exe` — portable exe
- `src-tauri/target/release/bundle/nsis/Persona Script Editor_0.1.0_x64-setup.exe` — installer
**Important:** Place any `.tbl` file (e.g. `Persona_jap.tbl`) in the same folder as the exe before running.
> **Note:** Do not use `cargo build --release` directly — it builds only the Rust backend without the frontend.
> Always use `npx tauri build` to get a working executable with the UI embedded.
---
## License
MIT

33
index.html Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Persona Script Editor</title>
<link rel="stylesheet" href="/src/style.css" />
</head>
<body>
<div id="app">
<header>
<h1>Persona Script Editor</h1>
<button id="btn-open">Open ISO</button>
<button id="btn-save" disabled>Save ISO</button>
<button id="btn-export" disabled>Export JSON</button>
<button id="btn-import" disabled>Import JSON</button>
<span id="status"></span>
</header>
<main>
<aside id="sidebar">
<div id="file-list"></div>
<div id="scene-list"></div>
</aside>
<section id="editor">
<div id="strings-container">
<p class="placeholder">Open an ISO file to begin</p>
</div>
</section>
</main>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1424
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "persona-script-editor",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-log": "^2"
},
"devDependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/cli": "^2",
"typescript": "^5.6",
"vite": "^6"
}
}

5084
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "persona-script-editor"
version = "0.1.0"
edition = "2024"
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tauri-plugin-log = "2"
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[lib]
name = "persona_script_editor_lib"
path = "src/lib.rs"
[[bin]]
name = "persona-script-editor"
path = "src/main.rs"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build();
}

View File

@@ -0,0 +1,10 @@
{
"identifier": "default",
"description": "Default capabilities for the app",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"log:default"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"default":{"identifier":"default","description":"Default capabilities for the app","local":true,"windows":["main"],"permissions":["core:default","dialog:default","log:default"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

121
src-tauri/src/e0.rs Normal file
View File

@@ -0,0 +1,121 @@
//! E0/E1/E2/E3 container format parser
//!
//! Each container has:
//! - Pointer table: uint16_le[] values, each = offset in 0x800-byte units
//! Terminated by 0x0000. Last entry marks end-of-data.
//! - Sub-files: aligned to 0x800 boundaries
use log::{debug, info, warn};
use serde::{Deserialize, Serialize};
pub const ALIGNMENT: usize = 0x800;
/// Parsed E0-style container
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Container {
/// Raw sub-file data for each slot
pub sub_files: Vec<Vec<u8>>,
}
/// Parse a container from raw bytes
pub fn parse_container(data: &[u8]) -> Container {
let ptrs = parse_pointer_table(data);
let num_sub_files = if ptrs.is_empty() { 0 } else { ptrs.len() - 1 };
debug!("parse_container: {} bytes, {} pointer entries, {} sub-files", data.len(), ptrs.len(), num_sub_files);
let mut sub_files = Vec::with_capacity(num_sub_files);
for i in 0..num_sub_files {
let start = ptrs[i] as usize * ALIGNMENT;
let end = ptrs[i + 1] as usize * ALIGNMENT;
let end = end.min(data.len());
if start < end && start < data.len() {
sub_files.push(data[start..end].to_vec());
} else {
warn!("parse_container: sub-file {} has invalid range [{:#x}..{:#x}], data.len()={:#x}", i, start, end, data.len());
sub_files.push(Vec::new());
}
}
Container { sub_files }
}
/// Rebuild container from sub-files back to raw bytes
pub fn build_container(container: &Container) -> Vec<u8> {
let n = container.sub_files.len();
info!("build_container: rebuilding {} sub-files", n);
// Pointer table: n+1 entries (sub-file starts + end marker) + terminator 0x0000
// Padded to ALIGNMENT
let _table_size = (n + 2) * 2; // +1 for end ptr, +1 for 0x0000 terminator
let data_start = ALIGNMENT; // First sub-file always at 0x800
// Calculate offsets for each sub-file
let mut offsets: Vec<usize> = Vec::with_capacity(n + 1);
let mut current = data_start;
for sf in &container.sub_files {
offsets.push(current);
let aligned_size = (sf.len() + ALIGNMENT - 1) & !(ALIGNMENT - 1);
current += if sf.is_empty() { ALIGNMENT } else { aligned_size };
}
offsets.push(current); // end-of-data pointer
let total_size = current;
let mut out = vec![0u8; total_size];
// Write pointer table
for (i, &off) in offsets.iter().enumerate() {
let ptr_val = (off / ALIGNMENT) as u16;
let table_off = i * 2;
if table_off + 2 <= out.len() {
out[table_off..table_off + 2].copy_from_slice(&ptr_val.to_le_bytes());
}
}
// Terminator 0x0000 is already there (vec initialized to 0)
// Write sub-files
for (i, sf) in container.sub_files.iter().enumerate() {
if !sf.is_empty() {
let off = offsets[i];
out[off..off + sf.len()].copy_from_slice(sf);
}
}
out
}
/// Parse pointer table from container start, returns vec of uint16 values
fn parse_pointer_table(data: &[u8]) -> Vec<u16> {
let mut ptrs = Vec::new();
let mut p = 0;
while p + 2 <= data.len() && p < 4096 {
let v = u16::from_le_bytes([data[p], data[p + 1]]);
if v == 0 {
break;
}
if !ptrs.is_empty() && v <= *ptrs.last().unwrap() {
break;
}
ptrs.push(v);
p += 2;
}
ptrs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_roundtrip() {
// Create a simple container with 2 sub-files
let container = Container {
sub_files: vec![vec![0x42; 100], vec![0x55; 200]],
};
let built = build_container(&container);
let parsed = parse_container(&built);
assert_eq!(parsed.sub_files.len(), 2);
// Sub-files are padded to ALIGNMENT in the container
assert!(parsed.sub_files[0].starts_with(&[0x42; 100]));
assert!(parsed.sub_files[1].starts_with(&[0x55; 200]));
}
}

328
src-tauri/src/encoding.rs Normal file
View File

@@ -0,0 +1,328 @@
//! Text encoding/decoding for Persona scripts
//!
//! Original Japanese encoding:
//! - Single byte 0x01-0x7F: kana characters (font glyph index)
//! - Two bytes 0x80xx: kanji/punctuation (TBL lookup)
//! - 0xFF xx: control codes (newline, clear, wait, etc.)
//!
//! For Russian translation, single bytes 0x01-0x43 are remapped to Cyrillic.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// FF-prefix control code
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ControlCode {
End, // FF 01 (string terminator)
EndDialogue, // FF 02
Newline, // FF 03
Clear, // FF 04
Wait(u8), // FF 05 XX 00
Color(u8), // FF 06 XX
FirstName, // FF 07
Nickname, // FF 08
Choice(u8), // FF 0E XX
LastName, // FF 0F
Unknown(u8), // FF XX (anything else)
}
/// A decoded text element (either a character or control code)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TextElement {
/// A displayable character (decoded to Unicode)
Char(char),
/// A raw two-byte code that couldn't be decoded
RawCode(u8, u8),
/// A control code
Control(ControlCode),
}
/// Character table mapping two-byte codes to Unicode characters
#[derive(Debug, Clone)]
pub struct CharTable {
/// Map from 2-byte code (big-endian u16) to character
pub code_to_char: HashMap<u16, char>,
/// Map from character to 2-byte code
pub char_to_code: HashMap<char, u16>,
/// Single-byte glyph table (index -> char)
pub single_byte: HashMap<u8, char>,
/// Reverse single-byte (char -> index)
pub single_byte_rev: HashMap<char, u8>,
}
/// Standard Japanese kana table (single-byte 0x01-0x7F)
const KANA: &[char] = &[
'\0', 'あ','い','う','え','お','か','き','く','け','こ',
'さ','し','す','せ','そ','た','ち','つ','て','と',
'な','に','ぬ','ね','の','は','ひ','ふ','へ','ほ',
'ま','み','む','め','も','や','ゆ','よ','ら','り',
'る','れ','ろ','わ','を','ん',
'ア','イ','ウ','エ','オ','カ','キ','ク','ケ','コ',
'サ','シ','ス','セ','ソ','タ','チ','ツ','テ','ト',
'ナ','ニ','ヌ','ネ','','ハ','ヒ','フ','ヘ','ホ',
'マ','ミ','ム','メ','モ','ヤ','ユ','ヨ','ラ','リ',
'ル','レ','ロ','ワ','ヲ','ン',
'ガ','ギ','グ','ゲ','ゴ','ザ','ジ','ズ','ゼ','ゾ',
'ダ','ヂ','ヅ','デ','ド','バ','ビ','ブ','ベ','ボ',
'パ','ピ','プ','ペ','ポ','ァ','ィ','ゥ','ェ','ォ',
'ャ','ュ','ョ','ッ','。',
// 0x7F = 。 is the last single-byte kana (0x01-0x7F range)
// 0x80+ are always first byte of two-byte codes (via TBL)
];
/// Russian Cyrillic single-byte mapping (0x01-0x43)
const CYRILLIC: &str = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя";
impl CharTable {
/// Create a table for original Japanese encoding
pub fn japanese() -> Self {
let mut single_byte = HashMap::new();
let mut single_byte_rev = HashMap::new();
for (i, &ch) in KANA.iter().enumerate().skip(1) {
if ch != '\0' {
single_byte.insert(i as u8, ch);
single_byte_rev.insert(ch, i as u8);
}
}
CharTable {
code_to_char: HashMap::new(),
char_to_code: HashMap::new(),
single_byte,
single_byte_rev,
}
}
/// Create a table for Russian Cyrillic encoding
pub fn russian() -> Self {
let mut single_byte = HashMap::new();
let mut single_byte_rev = HashMap::new();
for (i, ch) in CYRILLIC.chars().enumerate() {
let code = (i + 1) as u8;
single_byte.insert(code, ch);
single_byte_rev.insert(ch, code);
}
// Space = 0x43 (index 67)
let space_code = (CYRILLIC.chars().count() + 1) as u8;
single_byte.insert(space_code, ' ');
single_byte_rev.insert(' ', space_code);
CharTable {
code_to_char: HashMap::new(),
char_to_code: HashMap::new(),
single_byte,
single_byte_rev,
}
}
/// Load a TBL file to populate two-byte mappings
pub fn load_tbl(&mut self, tbl_content: &str) {
for line in tbl_content.lines() {
let line = line.trim();
if let Some((hex_part, char_part)) = line.split_once('=') {
if let Ok(code) = u16::from_str_radix(hex_part.trim(), 16) {
if let Some(ch) = char_part.chars().next() {
self.code_to_char.insert(code, ch);
self.char_to_code.entry(ch).or_insert(code);
}
}
}
}
}
}
/// Decode raw script bytes into text elements
pub fn decode_string(raw: &[u8], table: &CharTable) -> Vec<TextElement> {
let mut result = Vec::new();
let mut i = 0;
while i < raw.len() {
let b = raw[i];
if b == 0xFF {
// Control code
if i + 1 >= raw.len() {
break;
}
let cmd = raw[i + 1];
let ctrl = match cmd {
0x01 => { i += 2; ControlCode::End }
0x02 => { i += 2; ControlCode::EndDialogue }
0x03 => { i += 2; ControlCode::Newline }
0x04 => { i += 2; ControlCode::Clear }
0x05 => {
let val = if i + 2 < raw.len() { raw[i + 2] } else { 0 };
i += 4; // FF 05 XX 00
ControlCode::Wait(val)
}
0x06 => {
let val = if i + 2 < raw.len() { raw[i + 2] } else { 0 };
i += 3;
ControlCode::Color(val)
}
0x07 => { i += 2; ControlCode::FirstName }
0x08 => { i += 2; ControlCode::Nickname }
0x0E => {
let val = if i + 2 < raw.len() { raw[i + 2] } else { 0 };
i += 3;
ControlCode::Choice(val)
}
0x0F => { i += 2; ControlCode::LastName }
_ => { i += 2; ControlCode::Unknown(cmd) }
};
result.push(TextElement::Control(ctrl));
} else if b >= 0x80 {
// Two-byte character (kanji, punctuation via TBL)
if i + 1 >= raw.len() {
break;
}
let hi = b;
let lo = raw[i + 1];
let code = ((hi as u16) << 8) | (lo as u16);
if let Some(&ch) = table.code_to_char.get(&code) {
result.push(TextElement::Char(ch));
} else {
result.push(TextElement::RawCode(hi, lo));
}
i += 2;
} else if b >= 0x01 {
// Single-byte character (0x01-0x7F kana only)
if let Some(&ch) = table.single_byte.get(&b) {
result.push(TextElement::Char(ch));
} else {
result.push(TextElement::RawCode(0x00, b));
}
i += 1;
} else {
// Null byte - skip
i += 1;
}
}
result
}
/// Convert text elements back to a display string (for the editor UI)
pub fn elements_to_display(elements: &[TextElement]) -> String {
let mut s = String::new();
for el in elements {
match el {
TextElement::Char(ch) => s.push(*ch),
TextElement::RawCode(hi, lo) => {
s.push_str(&format!("|{:02X}{:02X}", hi, lo));
}
TextElement::Control(ctrl) => {
match ctrl {
ControlCode::End => s.push_str("[end]"),
ControlCode::EndDialogue => {} // don't show terminator
ControlCode::Newline => s.push_str("[nl]"),
ControlCode::Clear => s.push_str("[clear]"),
ControlCode::Wait(n) => s.push_str(&format!("[wait={}]", n)),
ControlCode::Color(n) => s.push_str(&format!("[color={}]", n)),
ControlCode::FirstName => s.push_str("[firstname]"),
ControlCode::Nickname => s.push_str("[nickname]"),
ControlCode::Choice(n) => s.push_str(&format!("[choice={}]", n)),
ControlCode::LastName => s.push_str("[lastname]"),
ControlCode::Unknown(n) => s.push_str(&format!("[ff{:02x}]", n)),
}
}
}
}
s
}
/// Encode a display string (with [nl], [clear], etc.) back to raw script bytes.
/// This is the inverse of decode_string + elements_to_display.
pub fn encode_display_string(text: &str, table: &CharTable) -> Vec<u8> {
let mut out = Vec::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
let ch = chars[i];
if ch == '[' {
// Parse control code tag
if let Some(end) = chars[i..].iter().position(|&c| c == ']') {
let tag: String = chars[i + 1..i + end].iter().collect();
let encoded = encode_control_tag(&tag);
out.extend_from_slice(&encoded);
i += end + 1;
continue;
}
}
if ch == '|' && i + 4 < chars.len() {
// Raw hex code |XXYY
let hex: String = chars[i + 1..i + 5].iter().collect();
if let Ok(val) = u16::from_str_radix(&hex, 16) {
if val <= 0xFF {
out.push(val as u8);
} else {
out.push((val >> 8) as u8);
out.push((val & 0xFF) as u8);
}
i += 5;
continue;
}
}
// Try single-byte encoding first
if let Some(&code) = table.single_byte_rev.get(&ch) {
out.push(code);
} else if let Some(&code) = table.char_to_code.get(&ch) {
// Two-byte encoding
out.push((code >> 8) as u8);
out.push((code & 0xFF) as u8);
} else {
// Unknown character - skip with warning
log::warn!("encode_display_string: unknown char '{}' (U+{:04X}), skipping", ch, ch as u32);
}
i += 1;
}
// Ensure string ends with FF 01
if !out.ends_with(&[0xFF, 0x01]) {
out.push(0xFF);
out.push(0x01);
}
out
}
/// Encode a single control tag like "nl", "clear", "wait=32", "choice=16"
fn encode_control_tag(tag: &str) -> Vec<u8> {
let lower = tag.to_lowercase();
// Check for tags with values: "wait=X", "color=X", "choice=X"
if let Some(val_str) = lower.strip_prefix("wait=") {
let val: u8 = val_str.parse().unwrap_or(32);
return vec![0xFF, 0x05, val, 0x00];
}
if let Some(val_str) = lower.strip_prefix("color=") {
let val: u8 = val_str.parse().unwrap_or(0);
return vec![0xFF, 0x06, val];
}
if let Some(val_str) = lower.strip_prefix("choice=") {
let val: u8 = val_str.parse().unwrap_or(0);
return vec![0xFF, 0x0E, val];
}
// Check for ff## pattern (raw FF code)
if lower.starts_with("ff") && lower.len() == 4 {
if let Ok(val) = u8::from_str_radix(&lower[2..], 16) {
return vec![0xFF, val];
}
}
match lower.as_str() {
"end" => vec![0xFF, 0x02],
"nl" => vec![0xFF, 0x03],
"clear" => vec![0xFF, 0x04],
"firstname" => vec![0xFF, 0x07],
"nickname" => vec![0xFF, 0x08],
"lastname" => vec![0xFF, 0x0F],
"close" => vec![0xFF, 0x01], // close = terminator
_ => {
log::warn!("encode_control_tag: unknown tag [{}]", tag);
vec![]
}
}
}

208
src-tauri/src/iso.rs Normal file
View File

@@ -0,0 +1,208 @@
//! ISO 9660 MODE2/2352 raw CD image — sector-based I/O
//!
//! PSX discs use raw sectors of 2352 bytes each.
//! User data is at offset 24, size 2048 per sector.
//! This module reads/writes sectors on demand via file seek, not loading the entire image.
use log::{info, warn};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
pub const SECTOR_SIZE: usize = 2352;
pub const USER_OFFSET: usize = 24;
pub const USER_SIZE: usize = 2048;
/// Game file table LBA (FSECT = sector start for each game file)
pub const FSECT_LBA: u32 = 634;
/// Known file indices in the game's internal file table
pub const FILE_IDX_E0: usize = 273;
pub const FILE_IDX_E1: usize = 274;
pub const FILE_IDX_E2: usize = 275;
pub const FILE_IDX_E3: usize = 276;
pub const FILE_IDX_FONT: usize = 5;
/// Handle to an opened ISO image (sector-based I/O)
pub struct IsoImage {
pub path: PathBuf,
pub sector_count: u32,
}
impl IsoImage {
/// Open an ISO image and validate it
pub fn open(path: &Path) -> io::Result<Self> {
let metadata = std::fs::metadata(path)?;
let file_size = metadata.len() as usize;
if file_size % SECTOR_SIZE != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("File size {} is not a multiple of {} (not a raw CD image)", file_size, SECTOR_SIZE),
));
}
let sector_count = (file_size / SECTOR_SIZE) as u32;
info!("ISO opened: {} bytes, {} sectors", file_size, sector_count);
Ok(IsoImage {
path: path.to_path_buf(),
sector_count,
})
}
/// Read user data (2048 bytes) from a single sector
pub fn read_sector(&self, lba: u32) -> io::Result<Vec<u8>> {
let mut file = File::open(&self.path)?;
let offset = lba as u64 * SECTOR_SIZE as u64 + USER_OFFSET as u64;
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; USER_SIZE];
file.read_exact(&mut buf)?;
Ok(buf)
}
/// Read multiple consecutive sectors' user data
pub fn read_sectors(&self, lba: u32, count: u32) -> io::Result<Vec<u8>> {
let mut file = File::open(&self.path)?;
let mut data = Vec::with_capacity(count as usize * USER_SIZE);
for i in 0..count {
let offset = (lba + i) as u64 * SECTOR_SIZE as u64 + USER_OFFSET as u64;
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; USER_SIZE];
file.read_exact(&mut buf)?;
data.extend_from_slice(&buf);
}
Ok(data)
}
/// Read a game file (consecutive sectors, trimmed to exact size)
pub fn read_file(&self, lba: u32, size: u32) -> io::Result<Vec<u8>> {
let sector_count = (size as usize + USER_SIZE - 1) / USER_SIZE;
let mut data = self.read_sectors(lba, sector_count as u32)?;
data.truncate(size as usize);
Ok(data)
}
/// Write user data to consecutive sectors (preserves sector headers)
pub fn write_file(&self, lba: u32, data: &[u8]) -> io::Result<()> {
let mut file = std::fs::OpenOptions::new().write(true).open(&self.path)?;
let sector_count = (data.len() + USER_SIZE - 1) / USER_SIZE;
for i in 0..sector_count {
let src_start = i * USER_SIZE;
let src_end = std::cmp::min(src_start + USER_SIZE, data.len());
let chunk = &data[src_start..src_end];
let offset = (lba as usize + i) * SECTOR_SIZE + USER_OFFSET;
file.seek(SeekFrom::Start(offset as u64))?;
file.write_all(chunk)?;
// Zero-pad remainder if chunk is short
if chunk.len() < USER_SIZE {
let padding = vec![0u8; USER_SIZE - chunk.len()];
file.write_all(&padding)?;
}
}
Ok(())
}
/// Append data at end of image (extends the file). Returns new LBA.
pub fn append_file(&self, data: &[u8]) -> io::Result<u32> {
let mut file = std::fs::OpenOptions::new().write(true).read(true).open(&self.path)?;
let file_size = file.seek(SeekFrom::End(0))?;
let current_sectors = file_size as usize / SECTOR_SIZE;
let new_lba = current_sectors as u32;
let sector_count = (data.len() + USER_SIZE - 1) / USER_SIZE;
// Write new sectors (full 2352-byte sectors with minimal headers)
for i in 0..sector_count {
let src_start = i * USER_SIZE;
let src_end = std::cmp::min(src_start + USER_SIZE, data.len());
let chunk = &data[src_start..src_end];
// Write a blank sector first
let mut sector = vec![0u8; SECTOR_SIZE];
// Copy user data at offset 24
sector[USER_OFFSET..USER_OFFSET + chunk.len()].copy_from_slice(chunk);
file.write_all(&sector)?;
}
info!("Appended {} sectors at LBA {}", sector_count, new_lba);
Ok(new_lba)
}
/// Read uint32_le from FSECT table at given index
pub fn read_fsect_entry(&self, index: usize) -> io::Result<u32> {
let sector_in_table = (index * 4) / USER_SIZE;
let offset_in_sector = (index * 4) % USER_SIZE;
let sector_data = self.read_sector(FSECT_LBA + sector_in_table as u32)?;
Ok(u32::from_le_bytes(
sector_data[offset_in_sector..offset_in_sector + 4].try_into().unwrap(),
))
}
/// Write uint32_le to FSECT table at given index
pub fn write_fsect_entry(&self, index: usize, value: u32) -> io::Result<()> {
let sector_in_table = (index * 4) / USER_SIZE;
let offset_in_sector = (index * 4) % USER_SIZE;
let mut sector_data = self.read_sector(FSECT_LBA + sector_in_table as u32)?;
sector_data[offset_in_sector..offset_in_sector + 4].copy_from_slice(&value.to_le_bytes());
self.write_sector_data(FSECT_LBA + sector_in_table as u32, &sector_data)
}
/// Write 2048 bytes of user data to a specific sector
fn write_sector_data(&self, lba: u32, data: &[u8]) -> io::Result<()> {
assert!(data.len() == USER_SIZE);
let mut file = std::fs::OpenOptions::new().write(true).open(&self.path)?;
let offset = lba as u64 * SECTOR_SIZE as u64 + USER_OFFSET as u64;
file.seek(SeekFrom::Start(offset))?;
file.write_all(data)?;
Ok(())
}
/// Determine container size by reading its pointer table from first sector
pub fn get_container_size(&self, lba: u32) -> io::Result<u32> {
let first_sector = self.read_sector(lba)?;
let mut last_ptr: u16 = 0;
let mut p = 0;
while p + 2 <= first_sector.len() {
let v = u16::from_le_bytes([first_sector[p], first_sector[p + 1]]);
if v == 0 {
break;
}
if last_ptr > 0 && v <= last_ptr {
break;
}
last_ptr = v;
p += 2;
}
Ok((last_ptr as u32) * (crate::e0::ALIGNMENT as u32))
}
/// Get info about all text container files
pub fn get_text_files(&self) -> io::Result<Vec<GameFile>> {
let files = [
(FILE_IDX_E0, "E0.BIN"),
(FILE_IDX_E1, "E1.BIN"),
(FILE_IDX_E2, "E2.BIN"),
(FILE_IDX_E3, "E3.BIN"),
];
let mut result = Vec::new();
for &(idx, name) in &files {
let lba = self.read_fsect_entry(idx)?;
let size = self.get_container_size(lba)?;
result.push(GameFile { index: idx, name, lba, size });
}
Ok(result)
}
}
/// Information about a game file
#[derive(Debug, Clone)]
pub struct GameFile {
pub index: usize,
pub name: &'static str,
pub lba: u32,
pub size: u32,
}

4
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod e0;
pub mod script;
pub mod encoding;
pub mod iso;

382
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,382 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use log::{error, info, warn};
use persona_script_editor_lib::{e0, encoding, iso, script};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Instant;
use tauri::State;
/// Translation store: file_index -> scene_index -> string_index -> translated text
type TranslationMap = HashMap<usize, HashMap<usize, HashMap<usize, String>>>;
struct AppState {
iso: Mutex<Option<iso::IsoImage>>,
/// Cached containers (only E0-E3, ~6MB total)
containers: Mutex<HashMap<usize, e0::Container>>,
table: Mutex<encoding::CharTable>,
translations: Mutex<TranslationMap>,
}
#[derive(Serialize)]
struct FileInfo {
name: String,
index: usize,
lba: u32,
size: u32,
sub_file_count: usize,
string_count: usize,
}
#[derive(Serialize)]
struct SceneInfo {
index: usize,
size: usize,
string_count: usize,
has_strings: bool,
}
#[derive(Serialize)]
struct StringInfo {
index: usize,
original: String,
translation: String,
raw_hex: String,
}
#[tauri::command]
fn open_iso(path: String, state: State<AppState>) -> Result<Vec<FileInfo>, String> {
let start = Instant::now();
info!("Opening ISO: {}", path);
let image = iso::IsoImage::open(std::path::Path::new(&path)).map_err(|e| {
error!("Failed to open ISO: {}", e);
format!("Failed to open: {}", e)
})?;
info!("ISO validated: {} sectors in {:?}", image.sector_count, start.elapsed());
let text_files = image.get_text_files().map_err(|e| format!("Read error: {}", e))?;
info!("Found {} text containers", text_files.len());
let mut result = Vec::new();
let mut containers = HashMap::new();
for gf in &text_files {
let t = Instant::now();
let file_data = image.read_file(gf.lba, gf.size).map_err(|e| format!("Read error: {}", e))?;
let container = e0::parse_container(&file_data);
let mut string_count = 0;
let mut scenes_with_strings = 0;
for sf in &container.sub_files {
if let Some(parsed) = script::parse_script(sf) {
if !parsed.strings.is_empty() {
scenes_with_strings += 1;
string_count += parsed.strings.len();
}
}
}
info!(" {} : LBA={} size={} scenes={}/{} strings={} ({:?})",
gf.name, gf.lba, gf.size, scenes_with_strings, container.sub_files.len(), string_count, t.elapsed());
result.push(FileInfo {
name: gf.name.to_string(),
index: gf.index,
lba: gf.lba,
size: gf.size,
sub_file_count: container.sub_files.len(),
string_count,
});
containers.insert(gf.index, container);
}
let total: usize = result.iter().map(|f| f.string_count).sum();
info!("Total: {} strings, ~{}MB in RAM. Done in {:?}", total,
containers.values().map(|c| c.sub_files.iter().map(|s| s.len()).sum::<usize>()).sum::<usize>() / 1024 / 1024,
start.elapsed());
*state.iso.lock().unwrap() = Some(image);
*state.containers.lock().unwrap() = containers;
state.translations.lock().unwrap().clear();
Ok(result)
}
#[tauri::command]
fn get_scenes(file_index: usize, state: State<AppState>) -> Result<Vec<SceneInfo>, String> {
let containers = state.containers.lock().unwrap();
let container = containers.get(&file_index).ok_or("File not loaded")?;
let mut scenes = Vec::new();
for (i, sf) in container.sub_files.iter().enumerate() {
let (string_count, has_strings) = if let Some(parsed) = script::parse_script(sf) {
(parsed.strings.len(), !parsed.strings.is_empty())
} else {
(0, false)
};
scenes.push(SceneInfo { index: i, size: sf.len(), string_count, has_strings });
}
Ok(scenes)
}
#[tauri::command]
fn get_strings(file_index: usize, scene_index: usize, state: State<AppState>) -> Result<Vec<StringInfo>, String> {
let containers = state.containers.lock().unwrap();
let table = state.table.lock().unwrap();
let translations = state.translations.lock().unwrap();
let container = containers.get(&file_index).ok_or("File not loaded")?;
let sf = container.sub_files.get(scene_index).ok_or("Scene not found")?;
let parsed = script::parse_script(sf).ok_or("Failed to parse script")?;
let scene_trans = translations.get(&file_index).and_then(|f| f.get(&scene_index));
let mut result = Vec::new();
for (i, s) in parsed.strings.iter().enumerate() {
let elements = encoding::decode_string(&s.raw, &table);
let original = encoding::elements_to_display(&elements);
let raw_hex = s.raw.iter().map(|b| format!("{:02x}", b)).collect::<String>();
let translation = scene_trans
.and_then(|st| st.get(&i))
.cloned()
.unwrap_or_else(|| original.clone());
result.push(StringInfo { index: i, original, translation, raw_hex });
}
Ok(result)
}
#[tauri::command]
fn update_string(file_index: usize, scene_index: usize, string_index: usize, translation: String, state: State<AppState>) -> Result<(), String> {
state.translations.lock().unwrap()
.entry(file_index).or_default()
.entry(scene_index).or_default()
.insert(string_index, translation);
Ok(())
}
#[tauri::command]
fn export_json(file_index: usize, output_path: String, state: State<AppState>) -> Result<String, String> {
let start = Instant::now();
info!("export_json: file={} -> {}", file_index, output_path);
let containers = state.containers.lock().unwrap();
let table = state.table.lock().unwrap();
let translations = state.translations.lock().unwrap();
let container = containers.get(&file_index).ok_or("File not loaded")?;
let file_name = match file_index { 273=>"E0.BIN", 274=>"E1.BIN", 275=>"E2.BIN", 276=>"E3.BIN", _=>"unknown" };
let scene_trans = translations.get(&file_index);
#[derive(Serialize)]
struct ExportScene { index: usize, strings: Vec<ExportString> }
#[derive(Serialize)]
struct ExportString { index: usize, original: String, translation: String }
#[derive(Serialize)]
struct ExportFile { file_index: usize, file_name: String, scenes: Vec<ExportScene> }
let mut scenes = Vec::new();
for (si, sf) in container.sub_files.iter().enumerate() {
if let Some(parsed) = script::parse_script(sf) {
if parsed.strings.is_empty() { continue; }
let st = scene_trans.and_then(|f| f.get(&si));
let strings: Vec<ExportString> = parsed.strings.iter().enumerate().map(|(i, s)| {
let elements = encoding::decode_string(&s.raw, &table);
let original = encoding::elements_to_display(&elements);
let translation = st.and_then(|m| m.get(&i)).cloned().unwrap_or_else(|| original.clone());
ExportString { index: i, original, translation }
}).collect();
scenes.push(ExportScene { index: si, strings });
}
}
let export = ExportFile { file_index, file_name: file_name.to_string(), scenes };
let json = serde_json::to_string_pretty(&export).map_err(|e| format!("JSON error: {}", e))?;
std::fs::write(&output_path, &json).map_err(|e| format!("Write error: {}", e))?;
let sc = export.scenes.len();
let stc: usize = export.scenes.iter().map(|s| s.strings.len()).sum();
info!("Exported {} scenes, {} strings in {:?}", sc, stc, start.elapsed());
Ok(format!("Exported {} scenes, {} strings", sc, stc))
}
#[tauri::command]
fn import_json(input_path: String, state: State<AppState>) -> Result<String, String> {
let start = Instant::now();
info!("import_json: {}", input_path);
let content = std::fs::read_to_string(&input_path).map_err(|e| format!("Read error: {}", e))?;
#[derive(Deserialize)]
struct ImportFile { file_index: usize, scenes: Vec<ImportScene>, #[serde(default)] file_name: String }
#[derive(Deserialize)]
struct ImportScene { index: usize, strings: Vec<ImportString> }
#[derive(Deserialize)]
struct ImportString { index: usize, translation: String, #[serde(default)] original: String }
let import: ImportFile = serde_json::from_str(&content).map_err(|e| format!("JSON error: {}", e))?;
let mut translations = state.translations.lock().unwrap();
let mut count = 0;
for scene in &import.scenes {
for s in &scene.strings {
if s.translation != s.original && !s.translation.is_empty() {
translations.entry(import.file_index).or_default()
.entry(scene.index).or_default()
.insert(s.index, s.translation.clone());
count += 1;
}
}
}
info!("Imported {} translations in {:?}", count, start.elapsed());
Ok(format!("Imported {} translations for {}", count, import.file_name))
}
#[tauri::command]
fn save_iso(output_path: String, state: State<AppState>) -> Result<String, String> {
let start = Instant::now();
info!("save_iso: {}", output_path);
let iso_guard = state.iso.lock().unwrap();
let image = iso_guard.as_ref().ok_or("No ISO loaded")?;
let containers = state.containers.lock().unwrap();
let table = state.table.lock().unwrap();
let translations = state.translations.lock().unwrap();
if translations.is_empty() {
return Err("No translations to save".into());
}
// Copy original ISO to output path
info!("Copying ISO to output...");
std::fs::copy(&image.path, &output_path).map_err(|e| format!("Copy error: {}", e))?;
let out_image = iso::IsoImage::open(std::path::Path::new(&output_path))
.map_err(|e| format!("Open output error: {}", e))?;
let mut files_modified = 0;
let mut strings_applied = 0;
for (&file_index, file_trans) in translations.iter() {
let container = match containers.get(&file_index) {
Some(c) => c,
None => continue,
};
let orig_lba = out_image.read_fsect_entry(file_index).map_err(|e| format!("FSECT read: {}", e))?;
let orig_size = out_image.get_container_size(orig_lba).map_err(|e| format!("Size read: {}", e))?;
let mut new_container = container.clone();
let mut modified = false;
for (&scene_idx, scene_trans) in file_trans.iter() {
if scene_idx >= new_container.sub_files.len() { continue; }
let sf = &new_container.sub_files[scene_idx];
let parsed = match script::parse_script(sf) {
Some(p) if !p.strings.is_empty() => p,
_ => continue,
};
let mut new_strings: Vec<Vec<u8>> = Vec::new();
let mut scene_modified = false;
for (i, orig) in parsed.strings.iter().enumerate() {
if let Some(trans_text) = scene_trans.get(&i) {
new_strings.push(encoding::encode_display_string(trans_text, &table));
strings_applied += 1;
scene_modified = true;
} else {
new_strings.push(orig.raw.clone());
}
}
if scene_modified {
if let Some(rebuilt) = script::rebuild_script(&parsed, &new_strings) {
info!(" file={} S{:03}: {} -> {} bytes", file_index, scene_idx, sf.len(), rebuilt.len());
new_container.sub_files[scene_idx] = rebuilt;
modified = true;
} else {
warn!(" file={} S{:03}: rebuild FAILED", file_index, scene_idx);
}
}
}
if modified {
let new_data = e0::build_container(&new_container);
info!(" Container rebuilt: {} bytes (was {})", new_data.len(), orig_size);
if new_data.len() as u32 <= orig_size {
// Fits in-place
out_image.write_file(orig_lba, &new_data).map_err(|e| format!("Write error: {}", e))?;
info!(" Written in-place at LBA {}", orig_lba);
} else {
// Relocate: append at end, update FSECT
let new_lba = out_image.append_file(&new_data).map_err(|e| format!("Append error: {}", e))?;
out_image.write_fsect_entry(file_index, new_lba).map_err(|e| format!("FSECT write: {}", e))?;
info!(" Relocated: LBA {} -> {} (FSECT updated)", orig_lba, new_lba);
}
files_modified += 1;
}
}
let msg = format!("Saved! {} files, {} strings applied in {:?}", files_modified, strings_applied, start.elapsed());
info!("{}", msg);
Ok(msg)
}
fn main() {
let mut table = encoding::CharTable::japanese();
// Look for any *.tbl file next to the exe
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()));
let mut tbl_loaded = false;
if let Some(dir) = &exe_dir {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("tbl") {
if let Ok(content) = std::fs::read_to_string(&path) {
table.load_tbl(&content);
eprintln!("[init] Loaded TBL: {} codes from {:?}", table.code_to_char.len(), path.file_name().unwrap());
tbl_loaded = true;
break;
}
}
}
}
}
if !tbl_loaded {
eprintln!("[init] WARNING: No .tbl file found next to exe!");
}
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(
tauri_plugin_log::Builder::new()
.target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout))
.target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: None }))
.target(tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview))
.level(log::LevelFilter::Info)
.build(),
)
.manage(AppState {
iso: Mutex::new(None),
containers: Mutex::new(HashMap::new()),
table: Mutex::new(table),
translations: Mutex::new(HashMap::new()),
})
.invoke_handler(tauri::generate_handler![
open_iso,
get_scenes,
get_strings,
update_string,
export_json,
import_json,
save_iso,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

206
src-tauri/src/script.rs Normal file
View File

@@ -0,0 +1,206 @@
//! Event script parser - extracts and rebuilds dialogue strings
//!
//! Sub-file internal structure:
//! - Header: 8 bytes (hdr_size=8, marker=0x8010, varies, 0x8010)
//! - Data section (everything after header):
//! - Script bytecode and command tables
//! - f[52]: uint16_le = ett (end of text table / start of text blob)
//! - f[96]: uint16_le = table_ptr (string pointer table offset, 0xFFFF = no strings)
//! - String pointer table entries: pattern FF 55 00 00 [ptr_lo ptr_hi] 10 80
//! - Text blob: strings terminated by FF 01
use log::{debug, trace, warn};
use serde::{Deserialize, Serialize};
/// A single dialogue string extracted from a script
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScriptString {
/// Offset of the pointer in the string pointer table (relative to data section)
pub ptr_table_offset: usize,
/// Offset of the actual text data (relative to data section)
pub text_offset: usize,
/// Raw encoded bytes of the string (including FF 01 terminator)
pub raw: Vec<u8>,
}
/// Parsed event script sub-file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventScript {
/// Header size in bytes (typically 8)
pub header_size: usize,
/// Raw header bytes
pub header: Vec<u8>,
/// End-of-text-table offset (start of text blob area) within data section
pub ett: usize,
/// String pointer table offset within data section (0xFFFF = no strings)
pub table_ptr: usize,
/// Extracted dialogue strings
pub strings: Vec<ScriptString>,
/// Full raw data section (everything after header)
pub data: Vec<u8>,
}
/// Parse an event script sub-file
pub fn parse_script(sub_file: &[u8]) -> Option<EventScript> {
if sub_file.len() < 108 {
trace!("parse_script: sub_file too small ({} bytes)", sub_file.len());
return None;
}
let header_size = u16::from_le_bytes([sub_file[0], sub_file[1]]) as usize;
if header_size < 4 || header_size > 256 || header_size >= sub_file.len() {
warn!("parse_script: invalid header_size={} (file_len={})", header_size, sub_file.len());
return None;
}
let header = sub_file[..header_size].to_vec();
let data = sub_file[header_size..].to_vec();
if data.len() < 100 {
trace!("parse_script: data section too small ({} bytes)", data.len());
return None;
}
let ett = u16::from_le_bytes([data[52], data[53]]) as usize;
let table_ptr = u16::from_le_bytes([data[96], data[97]]) as usize;
debug!("parse_script: size={} hdr={} data={} ett=0x{:04x} table_ptr=0x{:04x}",
sub_file.len(), header_size, data.len(), ett, table_ptr);
// Scan for string pointers using FF 55 00 00 [lo] [hi] 10 80 pattern.
// If table_ptr is valid, start scanning from there.
// If table_ptr is 0xFFFF, do a full scan of the data section (fallback).
let scan_start = if table_ptr != 0xFFFF && table_ptr < data.len() {
table_ptr
} else {
debug!("parse_script: table_ptr=0x{:04x}, using full scan fallback", table_ptr);
4 // start from beginning (need at least 4 bytes for prefix)
};
let mut strings = Vec::new();
let mut i = scan_start;
while i + 4 <= data.len() {
// Check if bytes at i+2..i+4 are 10 80
// AND bytes at i-4..i are FF 55 00 00
if i >= 4
&& i + 4 <= data.len()
&& data[i + 2] == 0x10
&& data[i + 3] == 0x80
&& data[i - 4] == 0xFF
&& data[i - 3] == 0x55
&& data[i - 2] == 0x00
&& data[i - 1] == 0x00
{
let ptr_val = u16::from_le_bytes([data[i], data[i + 1]]) as usize;
if ptr_val > 0 && ptr_val < data.len() {
// Read string until FF 01 terminator
let mut raw = Vec::new();
let mut j = ptr_val;
while j < data.len() - 1 {
if data[j] == 0xFF && data[j + 1] == 0x01 {
raw.push(0xFF);
raw.push(0x01);
break;
}
raw.push(data[j]);
j += 1;
}
// Only accept if we found the terminator
if raw.ends_with(&[0xFF, 0x01]) {
strings.push(ScriptString {
ptr_table_offset: i,
text_offset: ptr_val,
raw,
});
}
}
i += 4;
} else {
i += 2;
}
}
debug!("parse_script: found {} strings", strings.len());
Some(EventScript {
header_size,
header,
ett,
table_ptr,
strings,
data,
})
}
/// Rebuild a script sub-file with new string data
///
/// Takes the original script and a list of new raw string bytes (one per original string).
/// Returns the rebuilt sub-file bytes.
pub fn rebuild_script(script: &EventScript, new_strings: &[Vec<u8>]) -> Option<Vec<u8>> {
if new_strings.len() != script.strings.len() {
warn!("rebuild_script: string count mismatch: got {} expected {}", new_strings.len(), script.strings.len());
return None;
}
debug!("rebuild_script: rebuilding with {} strings, ett=0x{:04x}", new_strings.len(), script.ett);
let mut data = script.data.clone();
// Build new text blob starting at ett
let mut blob = Vec::new();
let mut new_offsets: Vec<usize> = Vec::with_capacity(new_strings.len());
for raw in new_strings {
let offset = script.ett + blob.len();
new_offsets.push(offset);
let mut s = raw.clone();
if !s.ends_with(&[0xFF, 0x01]) {
s.push(0xFF);
s.push(0x01);
}
blob.extend_from_slice(&s);
}
// Update string pointer values in the data section
for (i, string_info) in script.strings.iter().enumerate() {
let ptr_off = string_info.ptr_table_offset;
let new_val = new_offsets[i] as u16;
if ptr_off + 2 <= data.len() {
data[ptr_off] = new_val as u8;
data[ptr_off + 1] = (new_val >> 8) as u8;
}
}
// Replace text area: truncate at ett, append new blob, then append trailing data
// "Trailing data" = everything after the last string in the original
let original_text_end = if let Some(last) = script.strings.last() {
last.text_offset + last.raw.len()
} else {
script.ett
};
let trailing = if original_text_end < script.data.len() {
script.data[original_text_end..].to_vec()
} else {
Vec::new()
};
data.truncate(script.ett);
data.extend_from_slice(&blob);
data.extend_from_slice(&trailing);
// Rebuild full sub-file: header + data
let mut result = script.header.clone();
result.extend_from_slice(&data);
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_empty() {
assert!(parse_script(&[]).is_none());
assert!(parse_script(&[0; 50]).is_none());
}
}

36
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,36 @@
{
"$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/tauri.conf.json",
"productName": "Persona Script Editor",
"version": "0.1.0",
"identifier": "com.persona.script-editor",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "Persona Script Editor",
"width": 1200,
"height": 800,
"resizable": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

238
src/main.ts Normal file
View File

@@ -0,0 +1,238 @@
import { invoke } from "@tauri-apps/api/core";
import { open, save } from "@tauri-apps/plugin-dialog";
import { attachConsole, info, error } from "@tauri-apps/plugin-log";
interface FileInfo {
name: string;
index: number;
lba: number;
size: number;
sub_file_count: number;
string_count: number;
}
interface SceneInfo {
index: number;
size: number;
string_count: number;
has_strings: boolean;
}
interface StringInfo {
index: number;
original: string;
translation: string;
raw_hex: string;
}
let currentFileIndex: number | null = null;
let currentSceneIndex: number | null = null;
const btnOpen = document.getElementById("btn-open")!;
const btnExport = document.getElementById("btn-export")!;
const btnImport = document.getElementById("btn-import")!;
const btnSave = document.getElementById("btn-save") as HTMLButtonElement | null;
const statusEl = document.getElementById("status")!;
const fileList = document.getElementById("file-list")!;
const sceneList = document.getElementById("scene-list")!;
const stringsContainer = document.getElementById("strings-container")!;
btnOpen.addEventListener("click", openIso);
btnExport.addEventListener("click", exportJson);
btnImport.addEventListener("click", importJson);
if (btnSave) btnSave.addEventListener("click", saveIso);
// Attach console to receive backend logs in devtools
attachConsole();
info("[UI] Persona Script Editor started");
async function openIso() {
const path = await open({
filters: [{ name: "CD Image", extensions: ["bin", "img"] }],
});
if (!path) return;
statusEl.textContent = "Loading...";
info(`[UI] Opening ISO: ${path}`);
try {
const files: FileInfo[] = await invoke("open_iso", { path });
renderFiles(files);
const totalStrings = files.reduce((a, f) => a + f.string_count, 0);
statusEl.textContent = `Loaded: ${path.split("\\").pop()} (${totalStrings} strings)`;
info(`[UI] ISO loaded: ${files.length} files, ${totalStrings} total strings`);
btnExport.removeAttribute("disabled");
btnImport.removeAttribute("disabled");
if (btnSave) btnSave.removeAttribute("disabled");
} catch (e) {
error(`[UI] Failed to open ISO: ${e}`);
statusEl.textContent = `Error: ${e}`;
}
}
async function exportJson() {
if (currentFileIndex === null) return;
const path = await save({
filters: [{ name: "JSON", extensions: ["json"] }],
defaultPath: `E${currentFileIndex - 273}_strings.json`,
});
if (!path) return;
info(`[UI] Exporting file_index=${currentFileIndex} to ${path}`);
try {
const result: string = await invoke("export_json", {
fileIndex: currentFileIndex,
outputPath: path,
});
statusEl.textContent = result;
info(`[UI] Export done: ${result}`);
} catch (e) {
error(`[UI] Export failed: ${e}`);
statusEl.textContent = `Export error: ${e}`;
}
}
async function importJson() {
const path = await open({
filters: [{ name: "JSON", extensions: ["json"] }],
});
if (!path) return;
info(`[UI] Importing from ${path}`);
try {
const result: string = await invoke("import_json", { inputPath: path });
statusEl.textContent = result;
info(`[UI] Import done: ${result}`);
// Refresh current scene if loaded
if (currentFileIndex !== null && currentSceneIndex !== null) {
const strings: StringInfo[] = await invoke("get_strings", {
fileIndex: currentFileIndex,
sceneIndex: currentSceneIndex,
});
renderStrings(strings);
}
} catch (e) {
error(`[UI] Import failed: ${e}`);
statusEl.textContent = `Import error: ${e}`;
}
}
async function saveIso() {
const path = await save({
filters: [{ name: "CD Image", extensions: ["bin"] }],
defaultPath: "Persona_translated.bin",
});
if (!path) return;
statusEl.textContent = "Saving...";
info(`[UI] Saving ISO to ${path}`);
try {
const result: string = await invoke("save_iso", { outputPath: path });
statusEl.textContent = result;
info(`[UI] Save done: ${result}`);
} catch (e) {
error(`[UI] Save failed: ${e}`);
statusEl.textContent = `Save error: ${e}`;
}
}
function renderFiles(files: FileInfo[]) {
fileList.innerHTML = "";
for (const f of files) {
const div = document.createElement("div");
div.className = "file-item";
div.innerHTML = `<span>${f.name}</span><span class="count">${f.string_count} str</span>`;
div.addEventListener("click", () => selectFile(f.index, div));
fileList.appendChild(div);
}
}
async function selectFile(index: number, el: HTMLElement) {
currentFileIndex = index;
document.querySelectorAll(".file-item").forEach((e) => e.classList.remove("active"));
el.classList.add("active");
info(`[UI] Loading scenes for file_index=${index}`);
const scenes: SceneInfo[] = await invoke("get_scenes", { fileIndex: index });
info(`[UI] Got ${scenes.length} scenes (${scenes.filter((s) => s.has_strings).length} with strings)`);
renderScenes(scenes);
stringsContainer.innerHTML = `<p class="placeholder">Select a scene</p>`;
}
function renderScenes(scenes: SceneInfo[]) {
sceneList.innerHTML = "";
for (const s of scenes) {
const div = document.createElement("div");
div.className = `scene-item${s.has_strings ? "" : " no-strings"}`;
div.innerHTML = `<span>S${String(s.index).padStart(3, "0")}</span><span class="count">${s.string_count}</span>`;
if (s.has_strings) {
div.addEventListener("click", () => selectScene(s.index, div));
}
sceneList.appendChild(div);
}
}
async function selectScene(index: number, el: HTMLElement) {
if (currentFileIndex === null) return;
currentSceneIndex = index;
document.querySelectorAll(".scene-item").forEach((e) => e.classList.remove("active"));
el.classList.add("active");
info(`[UI] Loading strings for scene S${String(index).padStart(3, "0")}`);
const strings: StringInfo[] = await invoke("get_strings", {
fileIndex: currentFileIndex,
sceneIndex: index,
});
info(`[UI] Scene S${String(index).padStart(3, "0")}: ${strings.length} strings loaded`);
renderStrings(strings);
}
function renderStrings(strings: StringInfo[]) {
stringsContainer.innerHTML = "";
if (strings.length === 0) {
stringsContainer.innerHTML = `<p class="placeholder">No strings in this scene</p>`;
return;
}
for (const s of strings) {
const row = document.createElement("div");
row.className = "string-row";
row.innerHTML = `
<div class="label">String #${s.index}</div>
<div class="original">${highlightControls(escapeHtml(s.original))}</div>
<textarea data-index="${s.index}" rows="3">${escapeHtml(s.translation)}</textarea>
`;
stringsContainer.appendChild(row);
// Debounced save on edit
const textarea = row.querySelector("textarea")!;
let saveTimeout: number | undefined;
textarea.addEventListener("input", () => {
clearTimeout(saveTimeout);
saveTimeout = window.setTimeout(() => {
saveString(s.index, textarea.value);
}, 500);
});
}
}
async function saveString(stringIndex: number, translation: string) {
if (currentFileIndex === null || currentSceneIndex === null) return;
try {
await invoke("update_string", {
fileIndex: currentFileIndex,
sceneIndex: currentSceneIndex,
stringIndex,
translation,
});
} catch (e) {
error(`[UI] Failed to save string ${stringIndex}: ${e}`);
}
}
function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function highlightControls(s: string): string {
return s.replace(/\[(.*?)\]/g, '<span class="ctrl">[$1]</span>');
}

189
src/style.css Normal file
View File

@@ -0,0 +1,189 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #1a1a2e;
--surface: #16213e;
--surface2: #0f3460;
--primary: #e94560;
--text: #eaeaea;
--text-dim: #8892a4;
--border: #2a2a4a;
--font: "Segoe UI", system-ui, sans-serif;
--mono: "Cascadia Code", "Consolas", monospace;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
height: 100vh;
overflow: hidden;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
header {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
header h1 {
font-size: 16px;
font-weight: 600;
margin-right: auto;
}
header button {
padding: 6px 14px;
background: var(--surface2);
color: var(--text);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
header button:hover:not(:disabled) {
background: var(--primary);
border-color: var(--primary);
}
header button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
#status {
font-size: 12px;
color: var(--text-dim);
}
main {
display: flex;
flex: 1;
overflow: hidden;
}
#sidebar {
width: 280px;
min-width: 200px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
#file-list {
padding: 8px;
border-bottom: 1px solid var(--border);
}
#scene-list {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.file-item, .scene-item {
padding: 6px 10px;
margin: 2px 0;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
display: flex;
justify-content: space-between;
}
.file-item:hover, .scene-item:hover {
background: var(--surface2);
}
.file-item.active, .scene-item.active {
background: var(--primary);
}
.scene-item .count {
color: var(--text-dim);
font-size: 11px;
}
.scene-item.no-strings {
opacity: 0.4;
}
#editor {
flex: 1;
overflow-y: auto;
padding: 16px;
}
#strings-container .placeholder {
color: var(--text-dim);
text-align: center;
margin-top: 40px;
}
.string-row {
display: flex;
flex-direction: column;
margin-bottom: 12px;
padding: 10px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
}
.string-row .label {
font-size: 11px;
color: var(--text-dim);
margin-bottom: 4px;
}
.string-row .original {
font-family: var(--mono);
font-size: 13px;
color: var(--text);
padding: 6px 8px;
background: var(--bg);
border-radius: 4px;
margin-bottom: 8px;
white-space: pre-wrap;
word-break: break-all;
}
.string-row textarea {
font-family: var(--mono);
font-size: 13px;
color: var(--text);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 6px 8px;
resize: vertical;
min-height: 60px;
width: 100%;
}
.string-row textarea:focus {
outline: none;
border-color: var(--primary);
}
/* Control code highlighting */
.ctrl {
color: var(--primary);
font-weight: bold;
}

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2021",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

378
unknown_glyphs.html Normal file
View File

@@ -0,0 +1,378 @@
<!DOCTYPE html><html><head><meta charset="utf-8">
<style>
body{font-family:sans-serif;background:#1a1a2e;color:#eee;padding:20px}
.glyph{display:inline-block;margin:8px;padding:12px;background:#16213e;border:1px solid #444;border-radius:8px;vertical-align:top;width:340px}
canvas{background:#000;border:1px solid #555;image-rendering:pixelated}
.code{font-family:monospace;color:#e94560;font-size:18px;font-weight:bold}
.count{color:#aaa;font-size:12px}
.context{margin-top:8px;font-size:13px;color:#ccc;background:#0f3460;padding:6px;border-radius:4px;font-family:"Yu Mincho",serif}
input.guess{margin-top:8px;width:100%;background:#0f3460;color:#fff;border:1px solid #444;padding:4px;font-size:18px;font-family:"Yu Mincho",serif}
</style></head><body>
<h1>Unknown 2-byte codes in Persona scripts</h1>
<p>26 unknown kanji glyphs from game font. Compare with real kanji and fill in the input field.</p>
<div>
<div class="glyph">
<div class="code">0x8116</div>
<div class="count">used 609 times | glyph #406</div>
<canvas id="c8116" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8116"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8116">
</div>
<div class="glyph">
<div class="code">0x872F</div>
<div class="count">used 557 times | glyph #1967</div>
<canvas id="c872F" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c872F"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="872F">
</div>
<div class="glyph">
<div class="code">0x872D</div>
<div class="count">used 317 times | glyph #1965</div>
<canvas id="c872D" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c872D"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="872D">
</div>
<div class="glyph">
<div class="code">0x8723</div>
<div class="count">used 268 times | glyph #1955</div>
<canvas id="c8723" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8723"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8723">
</div>
<div class="glyph">
<div class="code">0x873D</div>
<div class="count">used 174 times | glyph #1981</div>
<canvas id="c873D" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c873D"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="873D">
</div>
<div class="glyph">
<div class="code">0x8739</div>
<div class="count">used 169 times | glyph #1977</div>
<canvas id="c8739" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8739"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8739">
</div>
<div class="glyph">
<div class="code">0x8736</div>
<div class="count">used 118 times | glyph #1974</div>
<canvas id="c8736" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8736"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8736">
</div>
<div class="glyph">
<div class="code">0x8738</div>
<div class="count">used 115 times | glyph #1976</div>
<canvas id="c8738" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8738"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8738">
</div>
<div class="glyph">
<div class="code">0x8254</div>
<div class="count">used 81 times | glyph #724</div>
<canvas id="c8254" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8254"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8254">
</div>
<div class="glyph">
<div class="code">0x8733</div>
<div class="count">used 81 times | glyph #1971</div>
<canvas id="c8733" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8733"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8733">
</div>
<div class="glyph">
<div class="code">0x8724</div>
<div class="count">used 56 times | glyph #1956</div>
<canvas id="c8724" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8724"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8724">
</div>
<div class="glyph">
<div class="code">0x8354</div>
<div class="count">used 38 times | glyph #980</div>
<canvas id="c8354" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8354"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8354">
</div>
<div class="glyph">
<div class="code">0x8392</div>
<div class="count">used 24 times | glyph #1042</div>
<canvas id="c8392" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8392"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8392">
</div>
<div class="glyph">
<div class="code">0x8783</div>
<div class="count">used 21 times | glyph #2051</div>
<canvas id="c8783" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8783"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8783">
</div>
<div class="glyph">
<div class="code">0x873B</div>
<div class="count">used 16 times | glyph #1979</div>
<canvas id="c873B" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c873B"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="873B">
</div>
<div class="glyph">
<div class="code">0x8732</div>
<div class="count">used 15 times | glyph #1970</div>
<canvas id="c8732" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8732"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8732">
</div>
<div class="glyph">
<div class="code">0x846A</div>
<div class="count">used 13 times | glyph #1258</div>
<canvas id="c846A" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c846A"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="846A">
</div>
<div class="glyph">
<div class="code">0x8399</div>
<div class="count">used 11 times | glyph #1049</div>
<canvas id="c8399" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8399"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8399">
</div>
<div class="glyph">
<div class="code">0x8572</div>
<div class="count">used 10 times | glyph #1522</div>
<canvas id="c8572" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8572"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8572">
</div>
<div class="glyph">
<div class="code">0x8391</div>
<div class="count">used 9 times | glyph #1041</div>
<canvas id="c8391" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8391"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8391">
</div>
<div class="glyph">
<div class="code">0x85F8</div>
<div class="count">used 8 times | glyph #1656</div>
<canvas id="c85F8" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c85F8"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="85F8">
</div>
<div class="glyph">
<div class="code">0x8758</div>
<div class="count">used 2 times | glyph #2008</div>
<canvas id="c8758" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8758"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8758">
</div>
<div class="glyph">
<div class="code">0x87CD</div>
<div class="count">used 2 times | glyph #2125</div>
<canvas id="c87CD" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c87CD"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="87CD">
</div>
<div class="glyph">
<div class="code">0x80FB</div>
<div class="count">used 1 times | glyph #379</div>
<canvas id="c80FB" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c80FB"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="80FB">
</div>
<div class="glyph">
<div class="code">0x8684</div>
<div class="count">used 1 times | glyph #1796</div>
<canvas id="c8684" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8684"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8684">
</div>
<div class="glyph">
<div class="code">0x8719</div>
<div class="count">used 1 times | glyph #1945</div>
<canvas id="c8719" width="160" height="160"></canvas>
<script>
(function(){
var c=document.getElementById("c8719"),x=c.getContext("2d");
x.fillStyle="#000";x.fillRect(0,0,160,160);x.fillStyle="#fff";
var p=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
for(var i=0;i<256;i++){if(p[i]){var r=Math.floor(i/16),col=i%16;x.fillRect(col*10,r*10,10,10);}}
})();
</script>
<input class="guess" type="text" placeholder="kanji?" data-code="8719">
</div>
</div></body></html>

15
vite.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from "vite";
export default defineConfig({
clearScreen: false,
server: {
port: 5173,
strictPort: true,
},
envPrefix: ["VITE_", "TAURI_"],
build: {
target: "esnext",
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_DEBUG,
},
});