Mercurial > touhou
comparison formats/src/th06/ecl.rs @ 757:21b186be2590
Split the Rust version into multiple crates.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Tue, 05 Jan 2021 02:16:32 +0100 |
parents | src/th06/ecl.rs@fc937d93a57c |
children |
comparison
equal
deleted
inserted
replaced
756:4d91790cf8ab | 757:21b186be2590 |
---|---|
1 //! ECL enemy script format support. | |
2 | |
3 use nom::{ | |
4 IResult, | |
5 number::complete::{le_u8, le_u16, le_u32, le_i16, le_i32, le_f32}, | |
6 sequence::tuple, | |
7 multi::{count, many0}, | |
8 error::ErrorKind, | |
9 Err, | |
10 }; | |
11 use encoding_rs::SHIFT_JIS; | |
12 use bitflags::bitflags; | |
13 | |
14 bitflags! { | |
15 /// Bit flags describing the current difficulty level. | |
16 pub struct Rank: u16 { | |
17 /// Easy mode. | |
18 const EASY = 0x100; | |
19 | |
20 /// Normal mode. | |
21 const NORMAL = 0x200; | |
22 | |
23 /// Hard mode. | |
24 const HARD = 0x400; | |
25 | |
26 /// Lunatic mode. | |
27 const LUNATIC = 0x800; | |
28 | |
29 /// Any or all modes. | |
30 const ALL = 0xff00; | |
31 } | |
32 } | |
33 | |
34 impl std::str::FromStr for Rank { | |
35 type Err = String; | |
36 | |
37 fn from_str(s: &str) -> Result<Rank, Self::Err> { | |
38 Ok(match s { | |
39 "easy" => Rank::EASY, | |
40 "normal" => Rank::NORMAL, | |
41 "hard" => Rank::HARD, | |
42 "lunatic" => Rank::LUNATIC, | |
43 _ => return Err(format!("unknown rank {}", s)) | |
44 }) | |
45 } | |
46 } | |
47 | |
48 /// A single instruction, part of a `Script`. | |
49 #[derive(Debug, Clone)] | |
50 pub struct CallSub { | |
51 /// Time at which this instruction will be called. | |
52 pub time: i32, | |
53 | |
54 /// The difficulty level(s) this instruction will be called at. | |
55 pub rank_mask: Rank, | |
56 | |
57 /// TODO | |
58 pub param_mask: u16, | |
59 | |
60 /// The instruction to call. | |
61 pub instr: SubInstruction, | |
62 } | |
63 | |
64 impl CallSub { | |
65 /// Create a new instruction call. | |
66 pub fn new(time: i32, rank_mask: Rank, instr: SubInstruction) -> CallSub { | |
67 CallSub { | |
68 time, | |
69 rank_mask, | |
70 param_mask: 0, | |
71 instr, | |
72 } | |
73 } | |
74 } | |
75 | |
76 /// Script driving an animation. | |
77 #[derive(Debug, Clone)] | |
78 pub struct Sub { | |
79 /// List of instructions in this script. | |
80 pub instructions: Vec<CallSub>, | |
81 } | |
82 | |
83 /// A single instruction, part of a `Script`. | |
84 #[derive(Debug, Clone)] | |
85 pub struct CallMain { | |
86 /// Time at which this instruction will be called. | |
87 pub time: u16, | |
88 | |
89 /// Subroutine to call for this enemy. | |
90 pub sub: u16, | |
91 | |
92 /// The instruction to call. | |
93 pub instr: MainInstruction, | |
94 } | |
95 | |
96 /// Script driving an animation. | |
97 #[derive(Debug, Clone)] | |
98 pub struct Main { | |
99 /// List of instructions in this script. | |
100 pub instructions: Vec<CallMain>, | |
101 } | |
102 | |
103 /// Main struct of the ANM0 animation format. | |
104 #[derive(Debug, Clone)] | |
105 pub struct Ecl { | |
106 /// A list of subs. | |
107 pub subs: Vec<Sub>, | |
108 | |
109 /// A list of mains. | |
110 pub mains: Vec<Main>, | |
111 } | |
112 | |
113 impl Ecl { | |
114 /// Parse a slice of bytes into an `Ecl` struct. | |
115 pub fn from_slice(data: &[u8]) -> IResult<&[u8], Ecl> { | |
116 parse_ecl(data) | |
117 } | |
118 } | |
119 | |
120 macro_rules! declare_main_instructions { | |
121 ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => { | |
122 /// Available instructions in an `Ecl`. | |
123 #[allow(missing_docs)] | |
124 #[derive(Debug, Clone, Copy)] | |
125 pub enum MainInstruction { | |
126 $( | |
127 $name($($arg_type),*) | |
128 ),* | |
129 } | |
130 | |
131 fn parse_main_instruction_args(input: &[u8], opcode: u16) -> IResult<&[u8], MainInstruction> { | |
132 let mut i = &input[..]; | |
133 let instr = match opcode { | |
134 $( | |
135 $opcode => { | |
136 $( | |
137 let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?; | |
138 i = i2; | |
139 )* | |
140 MainInstruction::$name($($arg),*) | |
141 } | |
142 )* | |
143 _ => unreachable!() | |
144 }; | |
145 Ok((i, instr)) | |
146 } | |
147 }; | |
148 } | |
149 | |
150 /// Parse a SHIFT_JIS byte string of length 34 into a String. | |
151 #[allow(non_snake_case)] | |
152 pub fn le_String(i: &[u8]) -> IResult<&[u8], String> { | |
153 let data = i.splitn(2, |c| *c == b'\0').nth(0).unwrap(); | |
154 let (string, _encoding, _replaced) = SHIFT_JIS.decode(data); | |
155 Ok((&i[34..], string.into_owned())) | |
156 } | |
157 | |
158 macro_rules! declare_sub_instructions { | |
159 ($($opcode:tt => fn $name:ident($($arg:ident: $arg_type:ident),*)),*,) => { | |
160 /// Available instructions in an `Ecl`. | |
161 #[allow(missing_docs)] | |
162 #[derive(Debug, Clone)] | |
163 pub enum SubInstruction { | |
164 $( | |
165 $name($($arg_type),*) | |
166 ),* | |
167 } | |
168 | |
169 fn parse_sub_instruction_args(input: &[u8], opcode: u16) -> IResult<&[u8], SubInstruction> { | |
170 let mut i = &input[..]; | |
171 let instr = match opcode { | |
172 $( | |
173 $opcode => { | |
174 $( | |
175 let (i2, $arg) = concat_idents!(le_, $arg_type)(i)?; | |
176 i = i2; | |
177 )* | |
178 SubInstruction::$name($($arg),*) | |
179 } | |
180 )* | |
181 _ => unreachable!() | |
182 }; | |
183 Ok((i, instr)) | |
184 } | |
185 }; | |
186 } | |
187 | |
188 declare_main_instructions!{ | |
189 0 => fn SpawnEnemy(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32), | |
190 2 => fn SpawnEnemyMirrored(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32), | |
191 4 => fn SpawnEnemyRandom(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32), | |
192 6 => fn SpawnEnemyMirroredRandom(x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: u32), | |
193 8 => fn CallMessage(), | |
194 9 => fn WaitMessage(), | |
195 10 => fn ResumeEcl(x: f32, y: f32), | |
196 12 => fn WaitForBossDeath(), | |
197 } | |
198 | |
199 declare_sub_instructions!{ | |
200 0 => fn Noop(), | |
201 1 => fn Destroy(unused: u32), | |
202 2 => fn RelativeJump(frame: i32, ip: i32), | |
203 3 => fn RelativeJumpEx(frame: i32, ip: i32, variable_id: i32), | |
204 4 => fn SetInt(var: i32, value: i32), | |
205 5 => fn SetFloat(var: i32, value: f32), | |
206 6 => fn SetRandomInt(var: i32, max: i32), | |
207 7 => fn SetRandomIntMin(var: i32, max: i32, min: i32), | |
208 8 => fn SetRandomFloat(var: i32, max: f32), | |
209 9 => fn SetRandomFloatMin(var: i32, amplitude: f32, min: f32), | |
210 10 => fn StoreX(var: i32), | |
211 11 => fn StoreY(var: i32), | |
212 12 => fn StoreZ(var: i32), | |
213 13 => fn AddInt(var: i32, a: i32, b: i32), | |
214 14 => fn SubstractInt(var: i32, a: i32, b: i32), | |
215 15 => fn MultiplyInt(var: i32, a: i32, b: i32), | |
216 16 => fn DivideInt(var: i32, a: i32, b: i32), | |
217 17 => fn ModuloInt(var: i32, a: i32, b: i32), | |
218 18 => fn Increment(var: i32), | |
219 19 => fn Decrement(var: i32), | |
220 20 => fn AddFloat(var: i32, a: f32, b: f32), | |
221 21 => fn SubstractFloat(var: i32, a: f32, b: f32), | |
222 22 => fn MultiplyFloat(var: i32, a: f32, b: f32), | |
223 23 => fn DivideFloat(var: i32, a: f32, b: f32), | |
224 24 => fn ModuloFloat(var: i32, a: f32, b: f32), | |
225 25 => fn GetDirection(var: i32, x1: f32, y1: f32, x2: f32, y2: f32), | |
226 26 => fn FloatToUnitCircle(var: i32), | |
227 27 => fn CompareInts(a: i32, b: i32), | |
228 28 => fn CompareFloats(a: f32, b: f32), | |
229 29 => fn RelativeJumpIfLowerThan(frame: i32, ip: i32), | |
230 30 => fn RelativeJumpIfLowerOrEqual(frame: i32, ip: i32), | |
231 31 => fn RelativeJumpIfEqual(frame: i32, ip: i32), | |
232 32 => fn RelativeJumpIfGreaterThan(frame: i32, ip: i32), | |
233 33 => fn RelativeJumpIfGreaterOrEqual(frame: i32, ip: i32), | |
234 34 => fn RelativeJumpIfNotEqual(frame: i32, ip: i32), | |
235 35 => fn Call(sub: i32, param1: i32, param2: f32), | |
236 36 => fn Return(), | |
237 37 => fn CallIfSuperior(sub: i32, param1: i32, param2: f32, a: i32, b: i32), | |
238 38 => fn CallIfSuperiorOrEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32), | |
239 39 => fn CallIfEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32), | |
240 40 => fn CallIfInferior(sub: i32, param1: i32, param2: f32, a: i32, b: i32), | |
241 41 => fn CallIfInferiorOrEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32), | |
242 42 => fn CallIfNotEqual(sub: i32, param1: i32, param2: f32, a: i32, b: i32), | |
243 43 => fn SetPosition(x: f32, y: f32, z: f32), | |
244 45 => fn SetAngleAndSpeed(angle: f32, speed: f32), | |
245 46 => fn SetRotationSpeed(speed: f32), | |
246 47 => fn SetSpeed(speed: f32), | |
247 48 => fn SetAcceleration(acceleration: f32), | |
248 49 => fn SetRandomAngle(min: f32, max: f32), | |
249 50 => fn SetRandomAngleEx(min: f32, max: f32), | |
250 51 => fn TargetPlayer(angle: f32, speed: f32), | |
251 52 => fn MoveInDecel(duration: i32, angle: f32, speed: f32), | |
252 56 => fn MoveToLinear(duration: i32, x: f32, y: f32, z: f32), | |
253 57 => fn MoveToDecel(duration: i32, x: f32, y: f32, z: f32), | |
254 59 => fn MoveToAccel(duration: i32, x: f32, y: f32, z: f32), | |
255 61 => fn StopIn(duration: i32), | |
256 63 => fn StopInAccel(duration: i32), | |
257 65 => fn SetScreenBox(xmin: f32, ymin: f32, xmax: f32, ymax: f32), | |
258 66 => fn ClearScreenBox(), | |
259 67 => fn SetBulletAttributes1(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32), | |
260 68 => fn SetBulletAttributes2(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32), | |
261 69 => fn SetBulletAttributes3(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32), | |
262 70 => fn SetBulletAttributes4(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32), | |
263 71 => fn SetBulletAttributes5(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32), | |
264 74 => fn SetBulletAttributes6(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32), | |
265 75 => fn SetBulletAttributes7(anim: i16, sprite_index_offset: i16, bullets_per_shot: i32, number_of_shots: i32, speed: f32, speed2: f32, launch_angle: f32, angle: f32, flags: u32), | |
266 76 => fn SetBulletInterval(interval: i32), | |
267 77 => fn SetBulletIntervalEx(interval: i32), | |
268 78 => fn DelayAttack(), | |
269 79 => fn NoDelayAttack(), | |
270 81 => fn SetBulletLaunchOffset(x: f32, y: f32, z: f32), | |
271 82 => fn SetExtendedBulletAttributes(a: i32, b: i32, c: i32, d: i32, e: f32, f: f32, g: f32, h: f32), | |
272 83 => fn ChangeBulletsInStarBonus(), | |
273 // TODO: Found in stage 4 onward. | |
274 84 => fn SetBulletSound(sound: i32), | |
275 85 => fn NewLaser(laser_type: i16, sprite_idx_offset: i16, angle: f32, speed: f32, start_offset: f32, end_offset: f32, max_length: f32, width: f32, start_duration: i32, duration: i32, end_duration: i32, grazing_delay: i32, grazing_extra_duration: i32, UNK1: i32), | |
276 86 => fn NewLaserTowardsPlayer(laser_type: i16, sprite_idx_offset: i16, angle: f32, speed: f32, start_offset: f32, end_offset: f32, max_length: f32, width: f32, start_duration: i32, duration: i32, end_duration: i32, grazing_delay: i32, grazing_extra_duration: i32, UNK1: i32), | |
277 87 => fn SetUpcomingLaserId(id: u32), | |
278 88 => fn AlterLaserAngle(id: u32, delta: f32), | |
279 90 => fn RepositionLaser(id: u32, ox: f32, oy: f32, oz: f32), | |
280 91 => fn LaserSetCompare(id: u32), | |
281 92 => fn CancelLaser(id: u32), | |
282 93 => fn SetSpellcard(face: i16, number: i16, name: String), | |
283 94 => fn EndSpellcard(), | |
284 95 => fn SpawnEnemy(sub: i32, x: f32, y: f32, z: f32, life: i16, bonus_dropped: i16, die_score: i32), | |
285 96 => fn KillAllEnemies(), | |
286 97 => fn SetAnim(script: i32), | |
287 98 => fn SetMultipleAnims(default: i16, end_left: i16, end_right: i16, left: i16, right: i16, _unused: i16), | |
288 99 => fn SetAuxAnm(number: i32, script: i32), | |
289 100 => fn SetDeathAnim(sprite_index: i32), | |
290 101 => fn SetBossMode(value: i32), | |
291 102 => fn CreateSquares(UNK1: i32, UNK2: f32, UNK3: f32, UNK4: f32, UNK5: f32), | |
292 103 => fn SetHitbox(width: f32, height: f32, depth: f32), | |
293 104 => fn SetCollidable(collidable: i32), | |
294 105 => fn SetDamageable(damageable: i32), | |
295 106 => fn PlaySound(index: i32), | |
296 107 => fn SetDeathFlags(death_flags: u32), | |
297 108 => fn SetDeathCallback(sub: i32), | |
298 109 => fn MemoryWriteInt(value: i32, index: i32), | |
299 111 => fn SetLife(life: i32), | |
300 112 => fn SetElapsedTime(frame: i32), | |
301 113 => fn SetLowLifeTrigger(trigger: i32), | |
302 114 => fn SetLowLifeCallback(sub: i32), | |
303 115 => fn SetTimeout(timeout: i32), | |
304 116 => fn SetTimeoutCallback(sub: i32), | |
305 117 => fn SetTouchable(touchable: i32), | |
306 118 => fn DropParticles(anim: i32, number: u32, r: u8, g: u8, b: u8, a: u8), | |
307 119 => fn DropBonus(number: i32), | |
308 120 => fn SetAutomaticOrientation(automatic: i32), | |
309 121 => fn CallSpecialFunction(function: i32, argument: i32), | |
310 122 => fn SetSpecialFunctionCallback(function: i32), | |
311 123 => fn SkipFrames(frames: i32), | |
312 124 => fn DropSpecificBonus(type_: i32), | |
313 // TODO: Found in stage 3. | |
314 125 => fn UNK_ins125(), | |
315 126 => fn SetRemainingLives(lives: i32), | |
316 // TODO: Found in stage 4. | |
317 127 => fn UNK_ins127(UNK1: i32), | |
318 128 => fn Interrupt(event: i32), | |
319 129 => fn InterruptAux(number: i32, event: i32), | |
320 // TODO: Found in stage 4. | |
321 130 => fn UNK_ins130(UNK1: i32), | |
322 131 => fn SetDifficultyCoeffs(speed_a: f32, speed_b: f32, nb_a: i32, nb_b: i32, shots_a: i32, shots_b: i32), | |
323 132 => fn SetInvisible(invisible: i32), | |
324 133 => fn CopyCallbacks(), | |
325 // TODO: Found in stage 4. | |
326 134 => fn UNK_ins134(), | |
327 135 => fn EnableSpellcardBonus(UNK1: i32), | |
328 } | |
329 | |
330 fn parse_sub_instruction(input: &[u8]) -> IResult<&[u8], CallSub> { | |
331 let i = &input[..]; | |
332 let (i, (time, opcode)) = tuple((le_i32, le_u16))(i)?; | |
333 if time == -1 || opcode == 0xffff { | |
334 return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof))); | |
335 } | |
336 | |
337 let (i, (size, rank_mask, param_mask)) = tuple((le_u16, le_u16, le_u16))(i)?; | |
338 let rank_mask = Rank::from_bits(rank_mask).unwrap(); | |
339 let (i, instr) = parse_sub_instruction_args(i, opcode)?; | |
340 assert_eq!(input.len() - i.len(), size as usize); | |
341 let call = CallSub { time, rank_mask, param_mask, instr }; | |
342 Ok((i, call)) | |
343 } | |
344 | |
345 fn parse_sub(i: &[u8]) -> IResult<&[u8], Sub> { | |
346 let (i, instructions) = many0(parse_sub_instruction)(i)?; | |
347 let sub = Sub { instructions }; | |
348 Ok((i, sub)) | |
349 } | |
350 | |
351 fn parse_main_instruction(input: &[u8]) -> IResult<&[u8], CallMain> { | |
352 let i = &input[..]; | |
353 let (i, (time, sub)) = tuple((le_u16, le_u16))(i)?; | |
354 if time == 0xffff && sub == 4 { | |
355 return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof))); | |
356 } | |
357 | |
358 let (i, (opcode, size)) = tuple((le_u16, le_u16))(i)?; | |
359 let size = size as usize; | |
360 let (i, instr) = parse_main_instruction_args(i, opcode)?; | |
361 assert_eq!(input.len() - i.len(), size as usize); | |
362 let call = CallMain { time, sub, instr }; | |
363 Ok((i, call)) | |
364 } | |
365 | |
366 fn parse_main(i: &[u8]) -> IResult<&[u8], Main> { | |
367 let (i, instructions) = many0(parse_main_instruction)(i)?; | |
368 let main = Main { instructions }; | |
369 Ok((i, main)) | |
370 } | |
371 | |
372 fn parse_ecl(input: &[u8]) -> IResult<&[u8], Ecl> { | |
373 let i = input; | |
374 | |
375 let (i, (sub_count, main_count)) = tuple((le_u16, le_u16))(i)?; | |
376 let sub_count = sub_count as usize; | |
377 | |
378 if main_count != 0 { | |
379 // TODO: use a better error. | |
380 return Err(Err::Error(nom::error::Error::new(i, ErrorKind::Eof))); | |
381 } | |
382 | |
383 let (_, (main_offsets, sub_offsets)) = tuple(( | |
384 count(le_u32, 3), | |
385 count(le_u32, sub_count), | |
386 ))(i)?; | |
387 | |
388 // Read all subs. | |
389 let mut subs = Vec::new(); | |
390 for offset in sub_offsets.into_iter().map(|offset| offset as usize) { | |
391 let (_, sub) = parse_sub(&input[offset..])?; | |
392 subs.push(sub); | |
393 } | |
394 | |
395 // Read all mains (always a single one atm). | |
396 let mut mains = Vec::new(); | |
397 for offset in main_offsets.into_iter().map(|offset| offset as usize) { | |
398 if offset == 0 { | |
399 break; | |
400 } | |
401 let (_, main) = parse_main(&input[offset..])?; | |
402 mains.push(main); | |
403 } | |
404 | |
405 let ecl = Ecl { | |
406 subs, | |
407 mains, | |
408 }; | |
409 Ok((b"", ecl)) | |
410 } | |
411 | |
412 #[cfg(test)] | |
413 mod tests { | |
414 use super::*; | |
415 use std::io::{self, Read}; | |
416 use std::fs::File; | |
417 | |
418 #[test] | |
419 fn ecl() { | |
420 let file = File::open("EoSD/ST/ecldata1.ecl").unwrap(); | |
421 let mut file = io::BufReader::new(file); | |
422 let mut buf = vec![]; | |
423 file.read_to_end(&mut buf).unwrap(); | |
424 let (_, ecl) = Ecl::from_slice(&buf).unwrap(); | |
425 assert_eq!(ecl.subs.len(), 24); | |
426 assert_eq!(ecl.mains.len(), 1); | |
427 } | |
428 } |