Skip to content

Commit a12e13b

Browse files
committed
Implement basic functionality
This performs basic password password decoding, serialization, and substitutes one character at a time to find potential errors. There is no attempt to sort errors and find the most likely one based on character or phonetic similarity, or data correctness, so all possible corrections are written out for the user.
0 parents  commit a12e13b

File tree

5 files changed

+276
-0
lines changed

5 files changed

+276
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
/Cargo.lock

Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "rejumon"
3+
version = "0.1.0"
4+
authors = ["Raphaël Zumer <[email protected]>"]
5+
description = "ドラゴンクエスト ふっかつのじゅもん 修正ツール"
6+
license = "WTFPL"
7+
repository = "https://github.com/rzumer/rejumon"
8+
edition = "2021"
9+
10+
[dependencies]
11+
bitstream-io = "1.6"

LICENSE.txt

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2+
Version 2, December 2004
3+
4+
Copyright (C) 2004 Sam Hocevar <[email protected]>
5+
6+
Everyone is permitted to copy and distribute verbatim or modified
7+
copies of this license document, and changing it is allowed as long
8+
as the name is changed.
9+
10+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12+
13+
0. You just DO WHAT THE FUCK YOU WANT TO.

README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Reじゅもん
2+
3+
ドラゴンクエスト ふっかつのじゅもん 修正ツール
4+
5+
このツールは、単なるパスワード生成ツールではなく、自動的に誤字を検出して修正するツールです。
6+
紙に書いたパスワードが誤っていた場合などに役立つように作りました。
7+
8+
※誤字は1つまで検出する<br>
9+
※DQ2の形式はまだ実装されていない
10+
11+
## 使い方
12+
例:
13+
```sh
14+
>cargo run きへづみやしやねふりたすちなのへむびおの
15+
16+
Found 7 substitutions:
17+
れへづみやしやねふりたすちなのへむびおの
18+
GameData { name: ['', '', '', ''], experience: 139, gold: 73, weapon: 7, armor: 1, shield: 2, herbs: 0, keys: 0, items: [0, 4, 0, 0, 0, 0, 0, 0], progress_flags: [false, false, true, false, false], encryption_key: 3, checksum: 229 }
19+
20+
きへづみやぎやねふりたすちなのへむびおの
21+
GameData { name: ['', '', '', ''], experience: 132, gold: 33, weapon: 7, armor: 1, shield: 2, herbs: 0, keys: 0, items: [0, 4, 0, 8, 0, 0, 0, 0], progress_flags: [false, false, true, true, false], encryption_key: 3, checksum: 130 }
22+
23+
きへづみやしやねふれたすちなのへむびおの
24+
GameData { name: ['', '', '', ''], experience: 132, gold: 73, weapon: 7, armor: 1, shield: 2, herbs: 0, keys: 0, items: [0, 4, 0, 0, 0, 0, 0, 8], progress_flags: [false, false, false, false, false], encryption_key: 3, checksum: 130 }
25+
26+
きへづみやしやねふぎたすちなのへむびおの
27+
GameData { name: ['', '', '', ''], experience: 132, gold: 73, weapon: 7, armor: 1, shield: 1, herbs: 0, keys: 0, items: [0, 4, 0, 0, 0, 0, 0, 8], progress_flags: [false, false, true, false, false], encryption_key: 7, checksum: 130 }
28+
29+
きへづみやしやねふりだすちなのへむびおの
30+
GameData { name: ['', '', '', ''], experience: 132, gold: 73, weapon: 2, armor: 2, shield: 0, herbs: 0, keys: 0, items: [0, 4, 0, 0, 0, 0, 0, 0], progress_flags: [false, false, false, false, false], encryption_key: 7, checksum: 130 }
31+
32+
きへづみやしやねふりたすちなのへかびおの
33+
GameData { name: ['', '', '', ''], experience: 58756, gold: 73, weapon: 7, armor: 1, shield: 2, herbs: 0, keys: 0, items: [0, 4, 0, 0, 0, 0, 0, 0], progress_flags: [false, false, true, false, false], encryption_key: 3, checksum: 130 }
34+
35+
きへづみやしやねふりたすちなのへむろおの
36+
GameData { name: ['', '', '', ''], experience: 32900, gold: 73, weapon: 7, armor: 1, shield: 2, herbs: 0, keys: 0, items: [1, 4, 0, 0, 0, 0, 0, 0], progress_flags: [false, true, true, false, false], encryption_key: 3, checksum: 130 }
37+
```

src/main.rs

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
use std::env;
2+
use bitstream_io::{BigEndian, BitReader, BitRead, BitWriter, BitWrite};
3+
4+
const JUMON_MOJI_TABLE: [char; 64] = [
5+
'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く',
6+
'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', 'た',
7+
'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね',
8+
'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み',
9+
'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り',
10+
'る', 'れ', 'ろ', 'わ', 'が', 'ぎ', 'ぐ', 'げ',
11+
'ご', 'ざ', 'じ', 'ず', 'ぜ', 'ぞ', 'だ', 'ぢ',
12+
'づ', 'で', 'ど', 'ば', 'び', 'ぶ', 'べ', 'ぼ',
13+
];
14+
15+
const NAME_MOJI_TABLE: [char; 64] = [
16+
'0', '1', '2', '3', '4', '5', '6', '7',
17+
'8', '9', 'あ', 'い', 'う', 'え', 'お', 'か',
18+
'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ',
19+
'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に',
20+
'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ',
21+
'ま', 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ',
22+
'ら', 'り', 'る', 'れ', 'ろ', 'わ', 'を', 'ん',
23+
'っ', 'ゃ', 'ゅ', 'ょ', '゛', '゜', 'ー', ' ',
24+
];
25+
26+
#[derive(Clone, Copy, Debug, Default)]
27+
struct GameData {
28+
// Player name: 4 characters (from `NAME_MOJI_TABLE`, 6 bits each)
29+
name: [char; 4],
30+
// Experience: 16 bits
31+
experience: u16,
32+
// Gold: 16 bits
33+
gold: u16,
34+
// Weapon ID: 3 bits
35+
weapon: u8,
36+
// Armor ID: 3 bits
37+
armor: u8,
38+
// Shield ID: 2 bits
39+
shield: u8,
40+
// やくそう count: 4 bits
41+
herbs: u8,
42+
// まほうのカギ count: 4 bits
43+
keys: u8,
44+
// Item IDs: 8 slots, 4 bits each
45+
items: [u8; 8],
46+
// Progress flags, 5 total, 1 bit each
47+
progress_flags: [bool; 5],
48+
// Encryption key, 3 bits
49+
encryption_key: u8,
50+
// Checksum, 8 bits
51+
checksum: u8,
52+
}
53+
54+
impl GameData {
55+
fn from_bytes(bytes: &[u8]) -> Self {
56+
let mut data = Self::default();
57+
58+
let mut reader = BitReader::endian(bytes, BigEndian);
59+
60+
data.items[1] = reader.read::<u8>(4).unwrap();
61+
data.items[0] = reader.read::<u8>(4).unwrap();
62+
63+
data.progress_flags[0] = reader.read::<u8>(1).unwrap() != 0;
64+
data.name[1] = NAME_MOJI_TABLE[reader.read::<u8>(6).unwrap() as usize];
65+
data.progress_flags[1] = reader.read::<u8>(1).unwrap() != 0;
66+
67+
data.experience |= reader.read::<u16>(8).unwrap() << 8;
68+
69+
data.items[5] = reader.read::<u8>(4).unwrap();
70+
data.items[4] = reader.read::<u8>(4).unwrap();
71+
72+
data.herbs = reader.read::<u8>(4).unwrap();
73+
data.keys = reader.read::<u8>(4).unwrap();
74+
75+
data.gold |= reader.read::<u16>(8).unwrap() << 8;
76+
77+
data.weapon = reader.read::<u8>(3).unwrap();
78+
data.armor = reader.read::<u8>(3).unwrap();
79+
data.shield = reader.read::<u8>(2).unwrap();
80+
81+
data.encryption_key |= reader.read::<u8>(1).unwrap() << 2;
82+
data.progress_flags[2] = reader.read::<u8>(1).unwrap() != 0;
83+
data.name[3] = NAME_MOJI_TABLE[reader.read::<u8>(6).unwrap() as usize];
84+
85+
data.items[7] = reader.read::<u8>(4).unwrap();
86+
data.items[6] = reader.read::<u8>(4).unwrap();
87+
88+
data.name[0] = NAME_MOJI_TABLE[reader.read::<u8>(6).unwrap() as usize];
89+
data.progress_flags[3] = reader.read::<u8>(1).unwrap() != 0;
90+
data.encryption_key |= reader.read::<u8>(1).unwrap() << 1;
91+
92+
data.gold |= reader.read::<u16>(8).unwrap();
93+
94+
data.items[3] = reader.read::<u8>(4).unwrap();
95+
data.items[2] = reader.read::<u8>(4).unwrap();
96+
97+
data.encryption_key |= reader.read::<u8>(1).unwrap();
98+
data.progress_flags[4] = reader.read::<u8>(1).unwrap() != 0;
99+
data.name[2] = NAME_MOJI_TABLE[reader.read::<u8>(6).unwrap() as usize];
100+
101+
data.experience |= reader.read::<u16>(8).unwrap();
102+
103+
data.checksum = reader.read::<u8>(8).unwrap();
104+
105+
return data;
106+
}
107+
}
108+
109+
fn decode(input: &str) -> Result<Vec<u8>, String> {
110+
// Decode characters
111+
let mut decrypted_characters: [u8; 20] = [ 0; 20 ];
112+
let mut last_character_code = 0;
113+
for (index, character) in input.chars().enumerate() {
114+
let character_code = JUMON_MOJI_TABLE.iter().position(|&moji| moji == character);
115+
if character_code.is_none() {
116+
return Err(format!("Unsupported input character: {}", character));
117+
}
118+
119+
let character_code_u8 = u8::try_from(character_code.unwrap()).unwrap();
120+
let decrypted = character_code_u8.wrapping_sub(last_character_code).wrapping_sub(4) & 0b00111111;
121+
last_character_code = character_code_u8;
122+
123+
decrypted_characters[index] = decrypted;
124+
}
125+
126+
// Pack characters into bytes
127+
let mut writer = BitWriter::endian(Vec::new(), BigEndian);
128+
for character in decrypted_characters.iter().rev() {
129+
writer.write(6, *character).unwrap();
130+
}
131+
let mut input_bytes = writer.into_writer();
132+
133+
// Calculate the correct checksum (XMODEM-CRC)
134+
let mut crc = 0_u8;
135+
let mut divisor = 0x8000_u16;
136+
for byte in input_bytes[..input_bytes.len() - 1].iter() {
137+
for bit in 0..8 {
138+
if divisor & 0x8000 != 0 {
139+
divisor = (divisor << 1) ^ 0x1021;
140+
} else {
141+
divisor <<= 1;
142+
}
143+
144+
if *byte & (1 << bit) != 0 {
145+
crc ^= divisor as u8;
146+
}
147+
}
148+
}
149+
150+
// Confirm that the CRC is correct
151+
let last_byte = input_bytes.last_mut().unwrap();
152+
if crc == *last_byte {
153+
return Ok(input_bytes);
154+
} else {
155+
return Err("Invalid CRC".to_string());
156+
}
157+
}
158+
159+
fn process(input: &str) -> Result<String, String> {
160+
if input.chars().count() != 20 {
161+
return Err("Input must be 20 characters.".to_string());
162+
}
163+
164+
let result = decode(input);
165+
if result.is_ok() {
166+
return Ok("The password is already valid.".to_string());
167+
}
168+
169+
let mut substitutions: Vec<(String, GameData)> = Vec::new();
170+
for input_index_to_replace in 0..input.len() {
171+
for moji_index in 0..JUMON_MOJI_TABLE.len() {
172+
let mut new_string = String::with_capacity(input.len());
173+
for (input_index, input_character) in input.chars().enumerate() {
174+
if input_index == input_index_to_replace {
175+
new_string.push(JUMON_MOJI_TABLE[moji_index]);
176+
} else {
177+
new_string.push(input_character);
178+
}
179+
}
180+
if let Ok(decoded) = decode(&new_string) {
181+
let data = GameData::from_bytes(decoded.as_slice());
182+
substitutions.push((String::from(new_string), data));
183+
}
184+
}
185+
}
186+
187+
if substitutions.len() > 0 {
188+
let mut output = String::new();
189+
output += &format!("Found {} substitutions:\n", substitutions.len());
190+
for (password, data) in substitutions {
191+
output += &format!("{}\n{:?}\n\n", password, data);
192+
}
193+
return Ok(output.trim_end().to_string());
194+
}
195+
196+
return Err("Recovery failed".to_string());
197+
}
198+
199+
fn main() {
200+
let args: Vec<String> = env::args().collect();
201+
let program = args[0].clone();
202+
203+
if args.len() < 2 {
204+
eprintln!("usage: {} <input>", program);
205+
return;
206+
}
207+
208+
let result = process(&args[1]);
209+
match result {
210+
Ok(output) => println!("{}", output),
211+
Err(err) => eprintln!("{}", err),
212+
}
213+
}

0 commit comments

Comments
 (0)