Nihuya sebe
This commit is contained in:
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,18 +1,19 @@
|
|||||||
# ---> Rust
|
# Build artifacts
|
||||||
# Generated by Cargo
|
/src-tauri/target/
|
||||||
# will have compiled files and executables
|
/node_modules/
|
||||||
debug/
|
/dist/
|
||||||
target/
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# IDE
|
||||||
**/*.rs.bk
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# OS
|
||||||
*.pdb
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# RustRover
|
# Logs
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
*.log
|
||||||
# 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
|
# Lock files (optional - remove if you want reproducible builds)
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# package-lock.json
|
||||||
#.idea/
|
# Cargo.lock
|
||||||
|
|||||||
223
FORMAT.md
Normal file
223
FORMAT.md
Normal 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
37
Makefile
Normal 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
2020
Persona_jap.tbl
Normal file
File diff suppressed because it is too large
Load Diff
181
README.md
181
README.md
@@ -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 (E0–E3)
|
||||||
|
|
||||||
|
```
|
||||||
|
[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
|
||||||
|
- `0x01–0x7F` — 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
33
index.html
Normal 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
1424
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal 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
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
24
src-tauri/Cargo.toml
Normal 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
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build();
|
||||||
|
}
|
||||||
10
src-tauri/capabilities/default.json
Normal file
10
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capabilities for the app",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"dialog:default",
|
||||||
|
"log:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
File diff suppressed because one or more lines are too long
1
src-tauri/gen/schemas/capabilities.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"Default capabilities for the app","local":true,"windows":["main"],"permissions":["core:default","dialog:default","log:default"]}}
|
||||||
2376
src-tauri/gen/schemas/desktop-schema.json
Normal file
2376
src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2376
src-tauri/gen/schemas/windows-schema.json
Normal file
2376
src-tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src-tauri/icons/icon.ico
Normal file
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
121
src-tauri/src/e0.rs
Normal 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
328
src-tauri/src/encoding.rs
Normal 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
208
src-tauri/src/iso.rs
Normal 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(§or)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, §or_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
4
src-tauri/src/lib.rs
Normal 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
382
src-tauri/src/main.rs
Normal 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
206
src-tauri/src/script.rs
Normal 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
36
src-tauri/tauri.conf.json
Normal 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
238
src/main.ts
Normal 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightControls(s: string): string {
|
||||||
|
return s.replace(/\[(.*?)\]/g, '<span class="ctrl">[$1]</span>');
|
||||||
|
}
|
||||||
189
src/style.css
Normal file
189
src/style.css
Normal 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
19
tsconfig.json
Normal 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
378
unknown_glyphs.html
Normal 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
15
vite.config.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user