Repository navigation
Expand file tree
/
Copy pathcd_player.zig
More file actions
779 lines (705 loc) · 35.4 KB
/
Copy pathcd_player.zig
File metadata and controls
779 lines (705 loc) · 35.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
//! `cd_player` (`0x00437FC0`) and its drawing (`0x00438890`): the CD player, which Use CD player
//! opens in the Reliant's rooms and the Yamato's (`rooms.Place.cd_player`). It runs in a loop of its
//! own over a picture of the player, and plays the game's music from a list of twelve pieces, each
//! carrier its own: a click on a row chooses a piece and a second plays it, and the buttons play,
//! pause, stop and step through the list, repeat a piece or play the list at random, turn the
//! volume up and down, and leave. The music goes on in the rooms after it, without their reverb, as
//! any music plays.
//!
//! **Fix:** every piece comes from the list shown. The game takes the Yamato's list for PLAY
//! SELECTED TRACK alone, and the Reliant's for the rows, the next and previous tracks and the piece
//! that follows one's end, so that on the Yamato these play the Reliant's piece in the row.
//!
//! **Improvement:** mods change the lists through the records' `cd_player`, which OpenReliant
//! installs (`install`), and add tracks after them; the original's are fixed. The lists are pages
//! of the records' tracks, twelve to a page: the Reliant's, the Yamato's, then the tracks mods add.
//! Scripts can turn the page (`CdPlayer.turnTo`).
//!
//! **Fix:** the CD player's volume is the level its music plays at, which the music volume and the
//! master volume scale as they scale any music. The game plays each piece at full level and sets
//! the stream to the CD player's volume itself, past the two volumes, so that a press of either
//! button made the music louder at the music volume's default.
const std = @import("std");
const assert = std.debug.assert;
const files = @import("../../files.zig");
const hog = @import("../../../formats/hog.zig");
const hog_snd = @import("../hog_snd.zig");
const hud = @import("../hud.zig");
const input = @import("../../input.zig");
const itac = @import("../itac.zig");
const language = @import("../language.zig");
const matmanager = @import("../matmanager.zig");
const canvas_module = @import("canvas.zig");
const rooms = @import("rooms.zig");
const Canvas = canvas_module.Canvas;
const Rect = canvas_module.Rect;
/// The pictures behind it (`0x00438363`): the Reliant's bunk's player up to mission 18
/// (`0x004E8B34`), and the Yamato's after it (`0x004E8B50`).
const reliant_picture = "\\interface\\rel_bunk2cd.tga";
const yamato_picture = "\\interface\\brd2cd.tga";
/// What it reads from `resource.hog` as it opens: its shapes (`0x004E8B28`), and the ITAC's large
/// font, which its title is written in (`0x004E8320`).
pub const shapes_name = "cdplay.spr";
const title_font_name = itac.large_font_name;
/// The CD player's volume (`0x004E5B88`), from 0 to 127, which lasts while the game runs: full as
/// it starts, and turned a step each pass its buttons are held.
pub const Volume = u7;
pub const full_volume: Volume = hog_snd.loudest;
/// The path of a piece's file, with the piece's name in it (`0x004E8B18`).
const music_path = "music\\{s}.wav";
/// The longest name a piece can have, for its path to fit in `files.max_path`.
pub const longest_piece = files.max_path - (music_path.len - "{s}".len);
/// A piece of the list: the music it plays, by the name of its file in `music` without the `.wav`,
/// and the words that name it. Load scripts change them through the records
/// (`records.cd_player`).
pub const Track = struct {
piece: []const u8,
name: language.Words,
/// The file it plays, written into `buffer`; null for a track without a piece, such as one a
/// script adds before it sets its piece, and for a piece whose name is longer than
/// `longest_piece`.
pub fn path(listed: Track, buffer: *[files.max_path]u8) ?[]const u8 {
if (listed.piece.len == 0) return null;
return std.mem.print(buffer, music_path, .{listed.piece}) catch null;
}
};
fn track(piece: []const u8, name: u16) Track {
return .{ .piece = piece, .name = .{ .string = name } };
}
/// The pieces of each list, which the player shows a page at a time.
pub const track_count = 12;
/// The highest number with the two digits the game writes for a row (`%02d`).
const highest_number = 99;
/// The most tracks there can be: as many whole pages as keep every track's number to two digits.
pub const most = highest_number / track_count * track_count;
/// The page of `carrier`'s list.
fn pageOf(carrier: rooms.Carrier) usize {
return @backingInt(carrier);
}
// The Reliant's list is the first page, as the records number its tracks first.
comptime {
assert(std.mem.eql(rooms.Carrier, std.enums.values(rooms.Carrier), &.{ .reliant, .yamato }));
}
/// The original's lists: the files (`0x00438261` on, `0x004382E1` on), and their names, Spirit of
/// War to The Catalyst on the Reliant (`0x319` on, `0x004389CD` on) and Final Word to The Rise on
/// the Yamato (`0x531` on, `0x00438A51` on). The Yamato's last plays the Reliant's first piece.
pub const original = [std.enums.values(rooms.Carrier).len * track_count]Track{
// The Reliant's.
track("new_mission01", 0x319),
track("new_mission02", 0x31A),
track("new_mission03", 0x31B),
track("new_mission04", 0x31C),
track("new_mission05", 0x31D),
track("new_mission06", 0x31E),
track("new_mission10", 0x31F),
track("New_Searching Mission 01", 0x320),
track("New_Searching Mission 03", 0x321),
track("New_Searching Mission 05", 0x322),
track("New_Searching Mission 06", 0x323),
track("New_Searching Mission 10", 0x324),
// The Yamato's.
track("New_Sim01", 0x531),
track("New_Sim02", 0x532),
track("New_Sim04", 0x533),
track("New_Sim05", 0x534),
track("New_Sim07", 0x535),
track("New_Sim08", 0x536),
track("New_Sim10", 0x537),
track("new_launch", 0x538),
track("New_Takeoff - Music", 0x539),
track("new_victory", 0x53A),
track("new_defeat", 0x53B),
track("new_mission01", 0x53C),
};
/// The pieces the CD player shows: the original's until OpenReliant installs the records'
/// (`install`).
var installed: []const Track = &original;
/// Uses `tracks` as the CD player's pieces: the original's lists, changed or not, and then the
/// tracks mods add, up to `most`. OpenReliant installs the records' once the load scripts have run.
pub fn install(tracks: []const Track) void {
assert(tracks.len >= original.len and tracks.len <= most);
installed = tracks;
}
/// How many pages the pieces fill, twelve to a page. The last page may hold fewer.
pub fn pageCount() usize {
return (installed.len + track_count - 1) / track_count;
}
/// The pieces on page `index`: twelve, or fewer on the last page.
fn piecesOn(index: usize) []const Track {
const first = index * track_count;
return installed[first..@min(first + track_count, installed.len)];
}
/// Whether nothing follows the piece at `place` on its page: the page's last.
fn lastOnPage(place: usize) bool {
return place % track_count + 1 == piecesOn(place / track_count).len;
}
/// The buttons, in the order of the rectangles `interface_hit` reads.
pub const Button = enum {
play,
pause,
stop,
next,
previous,
repeat,
shuffle,
leave,
volume_up,
volume_down,
/// Whether the pointer over it lights it (`0x00438B0C`): every button but REPEAT TRACK MODE
/// and RANDOM TRACK MODE, which light while they are on.
fn lightsUnderPointer(button: Button) bool {
return switch (button) {
.repeat, .shuffle => false,
.play, .pause, .stop, .next, .previous, .leave, .volume_up, .volume_down => true,
};
}
};
/// A button: where the pointer finds it, inside its rectangle, its edges left out (`0x00437FD1`
/// on); the shape of `cdplay.spr` that lights it and where it is drawn (`0x004388B9` on); and the
/// string that names it, PLAY SELECTED TRACK to CD VOLUME DOWN (`0x00438987` on).
pub const Item = struct {
rect: Rect,
lit: u8,
lit_at: [2]i32,
name: u16,
};
pub const buttons = std.EnumArray(Button, Item).init(.{
.play = .{ .rect = .{ .x = 564, .y = 124, .width = 50, .height = 43 }, .lit = 3, .lit_at = .{ 571, 134 }, .name = 0x2F6 },
.pause = .{ .rect = .{ .x = 562, .y = 170, .width = 56, .height = 27 }, .lit = 4, .lit_at = .{ 571, 174 }, .name = 0x2F7 },
.stop = .{ .rect = .{ .x = 562, .y = 199, .width = 55, .height = 26 }, .lit = 5, .lit_at = .{ 571, 204 }, .name = 0x2F8 },
.next = .{ .rect = .{ .x = 562, .y = 227, .width = 55, .height = 27 }, .lit = 6, .lit_at = .{ 571, 231 }, .name = 0x2F9 },
.previous = .{ .rect = .{ .x = 562, .y = 256, .width = 56, .height = 27 }, .lit = 7, .lit_at = .{ 571, 259 }, .name = 0x2FA },
.repeat = .{ .rect = .{ .x = 562, .y = 285, .width = 55, .height = 26 }, .lit = 8, .lit_at = .{ 571, 288 }, .name = 0x2FB },
.shuffle = .{ .rect = .{ .x = 562, .y = 314, .width = 56, .height = 26 }, .lit = 9, .lit_at = .{ 571, 316 }, .name = 0x2FC },
.leave = .{ .rect = .{ .x = 562, .y = 343, .width = 54, .height = 38 }, .lit = 10, .lit_at = .{ 571, 354 }, .name = 0x2FD },
.volume_up = .{ .rect = .{ .x = 21, .y = 341, .width = 56, .height = 22 }, .lit = 1, .lit_at = .{ 29, 345 }, .name = 0x2FE },
.volume_down = .{ .rect = .{ .x = 21, .y = 363, .width = 56, .height = 22 }, .lit = 2, .lit_at = .{ 29, 368 }, .name = 0x2FF },
});
/// The list's rows, where the pointer finds a piece after the buttons (`0x004380E5` on): 400 by 14
/// from (117, 176), each 16 below the last.
const row_corner: [2]i16 = .{ 0x75, 0xB0 };
const row_size: [2]i16 = .{ 0x190, 0xE };
const row_spacing = 0x10;
/// What lies under the pointer (`0x0051D564`): a button, or a row of the list.
pub const Hotspot = union(enum) {
button: Button,
row: usize,
};
/// The rectangles `interface_hit` reads, the buttons' and then the rows'.
const hotspots = blk: {
var all: [buttons.values.len + track_count]Rect = undefined;
for (all[0..buttons.values.len], buttons.values) |*rect, item| rect.* = item.rect;
for (all[buttons.values.len..], 0..) |*rect, row| rect.* = .{
.x = row_corner[0],
.y = row_corner[1] + @as(i16, row) * row_spacing,
.width = row_size[0],
.height = row_size[1],
};
break :blk all;
};
/// What lies under the point `at`, the first of `hotspots` that holds it (`interface_hit`).
pub fn hotspotAt(at: [2]i32) ?Hotspot {
const index = canvas_module.hit(&hotspots, at) orelse return null;
if (index < buttons.values.len) return .{ .button = std.enums.values(Button)[index] };
return .{ .row = index - buttons.values.len };
}
/// The title, CD PLAYER, in the ITAC's large font, and the list, in the front end's small font: the
/// rows' numbers and names, in blue, the piece chosen in white (`0x00438B91` on).
const title = 0x3B4;
const title_at: [2]i32 = .{ 0x75, 0x81 };
const list_colour = hud.rgb(0x0079FE);
const number_x = 0x75;
const name_x = 0x89;
const first_line = 0xAB;
/// The rows' numbers, `%02d` of each from 1 (`0x004E8CF0`), on to the last page's: two digits each.
const numbers = blk: {
assert(most <= highest_number);
var all: [most][2]u8 = undefined;
for (&all, 1..) |*number, n| number.* = .{ '0' + n / 10, '0' + n % 10 };
break :blk all;
};
/// The pointer, shape 12 of `cdplay.spr` (`0x00438D1D`).
const pointer_shape = 0xC;
/// What the CD player reads and plays with.
pub const Context = struct {
rooms: rooms.Context,
/// The rooms' steps and doors, which a press sounds from.
steps: rooms.Steps = .{},
/// The CD player's volume, which outlasts it.
volume: *Volume,
};
/// A pass's input.
pub const Input = struct {
keyboard: *input.Keyboard,
pointer: canvas_module.Pointer,
};
pub const CdPlayer = struct {
context: Context,
/// The carrier it stands on, whose picture it shows.
carrier: rooms.Carrier,
/// The page it shows: its carrier's list, unless a script turns to another (`turnTo`).
page: usize,
/// Whether its rows are numbered as the records number the pieces, as they are once a script
/// turns a page. The game numbers each list from 01.
numbered: bool = false,
picture: matmanager.Background = .{},
/// Its shapes, each drawn with the palette before it in the set, as the drawing makes VFX's
/// palette of block 0 for the buttons and of block 11 for the pointer (`0x00438AFB`,
/// `0x00438CF8`).
shapes: ?canvas_module.Shapes = null,
title_font: ?hud.FontFile = null,
/// The pointer as the pass read it, and what lies under it.
pointer: canvas_module.Pointer = .{},
under: ?Hotspot = null,
/// Whether the left button was down on the last pass (`0x0051DB40`), so that a press counts
/// on the pass it goes down.
held: bool = false,
/// Set by LEAVE CD PLAYER, which closes it on the next pass (`0x0043864B`).
leaving: bool = false,
/// The piece chosen, by its place among the pages' pieces (`0x0051D9D0`, its row); null for
/// none.
selected: ?usize = null,
/// The buttons' modes (`0x0051DA18`, `0x0051DA08`, `0x005202C8`), and whether a piece plays
/// that the next follows (`0x0051D7AC`).
paused: bool = false,
repeat: bool = false,
shuffle: bool = false,
playing: bool = false,
random: std.Random.DefaultPrng,
/// Opens it before mission `mission` (`0x00438363` on), at `now`, with its picture, shapes and
/// font from `resource.hog`, nothing chosen; what is missing is left out, which the log says.
pub fn open(context: Context, mission: u16, now: u64) CdPlayer {
const gpa = context.rooms.gpa;
const resources = context.rooms.resources;
const carrier: rooms.Carrier = .of(mission);
var player: CdPlayer = .{ .context = context, .carrier = carrier, .page = pageOf(carrier), .random = .init(now) };
const picture = switch (player.carrier) {
.reliant => reliant_picture,
.yamato => yamato_picture,
};
player.picture.show(gpa, resources.*, picture);
player.shapes = .read(gpa, resources, shapes_name);
player.title_font = .read(gpa, resources.*, title_font_name, context.rooms.outlines);
return player;
}
/// Lets go of what it read (`0x0043881F` on). The music plays on.
pub fn deinit(player: *CdPlayer) void {
const gpa = player.context.rooms.gpa;
player.picture.deinit(gpa);
if (player.shapes) |*shapes| shapes.deinit(gpa);
if (player.title_font) |*file| file.deinit(gpa);
player.* = undefined;
}
/// The pieces of the page it shows: twelve, or fewer on the last page.
pub fn list(player: CdPlayer) []const Track {
return piecesOn(player.page);
}
/// Turns to page `page`, one of `pageCount`, its rows numbered as the records number the
/// pieces: 01 to 12 on the Reliant's list, 13 to 24 on the Yamato's, and on from 25 on the
/// pages of the tracks mods add. The piece chosen stays chosen, and the pieces that follow it
/// come from its own page.
pub fn turnTo(player: *CdPlayer, page: usize) void {
assert(page < pageCount());
player.page = page;
player.numbered = true;
}
/// The place among the pages' pieces of row `row` of the page shown.
fn placeOf(player: CdPlayer, row: usize) usize {
return player.page * track_count + row;
}
/// The number row `row` shows.
fn number(player: CdPlayer, row: usize) []const u8 {
return &numbers[if (player.numbered) player.placeOf(row) else row];
}
/// A pass of its loop (`0x00438407` on): whether it stays open. Escape closes it, and so does
/// the pass after LEAVE CD PLAYER. The left button going down over a button or a row sounds
/// and does what it does; the volume's buttons turn it a step each pass they are held. Then a
/// piece that has ended is followed (`follow`).
pub fn pass(player: *CdPlayer, in: Input) bool {
player.pointer = in.pointer;
if (in.keyboard.pressed(input.scan.escape, .none, true) or player.leaving) return false;
player.under = player.hotspotOn(in.pointer.at);
const pressed = in.pointer.down and !player.held;
player.held = in.pointer.down;
if (pressed and player.under != null) player.playPress();
if (player.under) |under| switch (under) {
.button => |button| switch (button) {
.play => if (pressed and player.selected != null) {
player.start();
player.paused = false;
player.playing = true;
},
.pause => if (pressed) {
player.context.rooms.sound.pauseMusic(!player.paused);
player.paused = !player.paused;
},
.stop => if (pressed) {
player.selected = null;
player.playing = false;
player.context.rooms.sound.closeMusic();
},
.next => if (pressed) {
if (player.selected) |chosen| {
if (!lastOnPage(chosen)) player.skipTo(chosen + 1);
} else player.skipTo(player.placeOf(0));
},
.previous => if (pressed) if (player.selected) |chosen| if (chosen % track_count > 0) player.skipTo(chosen - 1),
.repeat => if (pressed) {
player.shuffle = false;
player.repeat = !player.repeat;
},
.shuffle => if (pressed) {
player.repeat = false;
player.shuffle = !player.shuffle;
},
.leave => if (pressed) {
player.leaving = true;
},
.volume_up => if (in.pointer.down) player.turn(player.context.volume.* +| 1),
.volume_down => if (in.pointer.down) player.turn(player.context.volume.* -| 1),
},
// A row chooses its piece, and the piece chosen plays.
.row => |row| if (pressed) {
const place = player.placeOf(row);
if (player.selected == place) {
player.start();
player.playing = true;
} else player.selected = place;
},
};
player.follow();
return true;
}
/// The next or the previous track, at `place`, played (`0x004385B9` on).
fn skipTo(player: *CdPlayer, place: usize) void {
player.selected = place;
player.start();
player.paused = false;
}
/// What follows a piece as it ends (`0x00438735` on): with REPEAT TRACK MODE off, once the
/// music has stopped while a piece played, not paused, the next on its page, or in RANDOM TRACK
/// MODE another of the page's pieces. The page's last piece ends it.
fn follow(player: *CdPlayer) void {
if (player.repeat or player.paused or !player.playing) return;
if (player.context.rooms.sound.musicPlaying()) return;
const chosen = player.selected orelse return;
if (player.shuffle) {
const row = chosen % track_count;
player.selected = chosen - row + other(player.random.random(), row, piecesOn(chosen / track_count).len);
} else {
if (lastOnPage(chosen)) return;
player.selected = chosen + 1;
}
player.start();
}
/// `music_play` of the piece chosen, at once: over and over in REPEAT TRACK MODE, else once.
fn start(player: *CdPlayer) void {
const chosen = player.selected orelse return;
const loops: u32 = if (player.repeat) hog_snd.forever else hog_snd.once;
var buffer: [files.max_path]u8 = undefined;
const path = installed[chosen].path(&buffer) orelse return;
player.context.rooms.sound.playMusic(path, loops, player.context.volume.*, .now);
}
/// The volume turned to `volume`, and the music with it (`0x0043866D` on, `0x004386AA` on).
fn turn(player: *CdPlayer, volume: Volume) void {
player.context.volume.* = volume;
player.context.rooms.sound.setMusicLevel(volume);
}
/// The sound of a press over a button or a row.
fn playPress(player: CdPlayer) void {
player.context.steps.play(player.context.rooms.sound, .cd_press);
}
/// The frame (`0x00438890`): the picture; the button under the pointer lit, and REPEAT TRACK
/// MODE and RANDOM TRACK MODE while they are on; the title and the list; and the name of the
/// button under the pointer. The pointer comes last, after what scripts draw (`drawPointer`).
pub fn draw(player: *CdPlayer, canvas: Canvas) canvas_module.Error!void {
if (player.picture.image) |*image| canvas.fill(image);
const button = player.buttonUnder();
if (player.shapes) |*shapes| {
if (button) |over| if (over.lightsUnderPointer()) try light(canvas, shapes, over);
if (player.repeat) try light(canvas, shapes, .repeat);
if (player.shuffle) try light(canvas, shapes, .shuffle);
}
if (player.title_font) |*file| try canvas.string(&file.font, title_at, title, list_colour, .left);
var y: i32 = first_line;
for (player.list(), 0..) |piece, row| {
defer y += row_spacing;
const colour = if (player.selected == player.placeOf(row)) canvas_module.white else list_colour;
try canvas.text(canvas.fonts.small, .{ number_x, y }, player.number(row), colour, .left);
try canvas.text(canvas.fonts.small, .{ name_x, y }, piece.name.in(canvas.strings.*), colour, .left);
}
if (button) |over| try canvas.label(buttons.get(over).name);
}
/// The pointer (`0x00438D1D`).
pub fn drawPointer(player: *CdPlayer, canvas: Canvas) canvas_module.Error!void {
if (player.shapes) |*shapes| try canvas.onScreen().shape(&shapes.art, pointer_shape, player.pointer.at);
}
/// What lies under the point `at` (`hotspotAt`), leaving out the rows the page shown has no
/// piece for.
fn hotspotOn(player: CdPlayer, at: [2]i32) ?Hotspot {
const under = hotspotAt(at) orelse return null;
return switch (under) {
.button => under,
.row => |row| if (row < player.list().len) under else null,
};
}
/// The button under the pointer; null over a row, or over nothing.
fn buttonUnder(player: CdPlayer) ?Button {
const under = player.under orelse return null;
return switch (under) {
.button => |button| button,
.row => null,
};
}
};
/// Draws the shape that lights `button`.
fn light(canvas: Canvas, shapes: *canvas_module.Shapes, button: Button) canvas_module.Error!void {
const item = buttons.get(button);
try canvas.shape(&shapes.art, item.lit, item.lit_at);
}
/// A piece of a page of `count` other than `row`, at random; `row` again where it's the page's
/// only piece.
///
/// **Improvement:** it is drawn from `std.Random` among the page's other pieces. The game draws
/// `rand() % 12` up to 20 times for one other than `row` (`0x004387AE` on), and plays `row` again
/// where none is.
fn other(random: std.Random, row: usize, count: usize) usize {
if (count < 2) return row;
const drawn = random.uintLessThan(usize, count - 1);
return if (drawn >= row) drawn + 1 else drawn;
}
test hotspotAt {
try std.testing.expectEqual(Hotspot{ .button = .play }, hotspotAt(.{ 590, 140 }).?);
try std.testing.expectEqual(Hotspot{ .button = .leave }, hotspotAt(.{ 590, 360 }).?);
try std.testing.expectEqual(Hotspot{ .button = .volume_down }, hotspotAt(.{ 40, 370 }).?);
// The rows, 16 apart from (117, 176), their edges left out.
try std.testing.expectEqual(Hotspot{ .row = 0 }, hotspotAt(.{ 200, 180 }).?);
try std.testing.expectEqual(Hotspot{ .row = 11 }, hotspotAt(.{ 200, 360 }).?);
try std.testing.expectEqual(null, hotspotAt(.{ 200, 176 }));
try std.testing.expectEqual(null, hotspotAt(.{ 320, 440 }));
}
test other {
var prng: std.Random.DefaultPrng = .init(0);
for (0..200) |n| {
const row = n % track_count;
const drawn = other(prng.random(), row, track_count);
try std.testing.expect(drawn != row and drawn < track_count);
}
// On a page of three, among its three; on a page of one, the same piece again.
for (0..20) |_| try std.testing.expect(other(prng.random(), 1, 3) < 3);
try std.testing.expectEqual(0, other(prng.random(), 0, 1));
}
test original {
var buffer: [files.max_path]u8 = undefined;
try std.testing.expectEqualStrings("music\\new_mission01.wav", original[0].path(&buffer).?);
try std.testing.expectEqualStrings("music\\New_Takeoff - Music.wav", original[20].path(&buffer).?);
try std.testing.expectEqual(0x53C, original[23].name.string);
try std.testing.expectEqualStrings("12", &numbers[11]);
// A piece whose path wouldn't fit has none, and so does a track without a piece.
const long_piece: [files.max_path]u8 = @splat('x');
const long: Track = .{ .piece = &long_piece, .name = .{ .string = 0x319 } };
try std.testing.expectEqual(null, long.path(&buffer));
const empty: Track = .{ .piece = "", .name = .{ .text = "" } };
try std.testing.expectEqual(null, empty.path(&buffer));
}
/// Its files in the tests: there, but unreadable, and left out.
const stand_ins = [_]hog.Member{
.{ .name = "rel_bunk2cd.tga", .data = "x" },
.{ .name = "brd2cd.tga", .data = "x" },
.{ .name = "cdplay.spr", .data = "x" },
.{ .name = "itacbig.fnt", .data = "x" },
};
/// The rooms in the tests, with the music of `carrier`'s list as installed.
fn testedWith(tested: *rooms.testing.Tested, carrier: rooms.Carrier) !void {
try tested.init(&.{}, &.{}, &stand_ins);
errdefer tested.deinit();
var buffers: [track_count][files.max_path]u8 = undefined;
var paths: [track_count][]const u8 = undefined;
const list = installed[pageOf(carrier) * track_count ..][0..track_count];
for (&paths, &buffers, list) |*path, *buffer, piece| path.* = piece.path(buffer).?;
try tested.giveMusic(&paths);
}
/// A pass with the pointer at `at`, the left button down where `down` has it.
fn passAt(player: *CdPlayer, keyboard: *input.Keyboard, at: [2]i32, down: bool) bool {
return player.pass(.{ .keyboard = keyboard, .pointer = .{ .at = at, .down = down } });
}
/// A click at `at`: the left button down for a pass, then up.
fn click(player: *CdPlayer, keyboard: *input.Keyboard, at: [2]i32) !void {
try std.testing.expect(passAt(player, keyboard, at, true));
try std.testing.expect(passAt(player, keyboard, at, false));
}
/// Where the pointer finds `button`, and row `row` of the list; and a point over nothing.
fn buttonAt(button: Button) [2]i32 {
const rect = buttons.get(button).rect;
return .{ @as(i32, rect.x) + 5, @as(i32, rect.y) + 5 };
}
fn rowAt(row: i32) [2]i32 {
return .{ 200, row_corner[1] + row * row_spacing + 5 };
}
const nowhere: [2]i32 = .{ 320, 440 };
test CdPlayer {
var tested: rooms.testing.Tested = undefined;
try testedWith(&tested, .reliant);
defer tested.deinit();
const sound = &tested.sound;
var keyboard: input.Keyboard = .{};
var volume: Volume = full_volume;
var player: CdPlayer = .open(.{ .rooms = tested.context(), .volume = &volume }, 18, 0);
defer player.deinit();
try std.testing.expectEqual(rooms.Carrier.reliant, player.carrier);
// PLAY SELECTED TRACK with none chosen plays nothing. A press counts on the pass the button
// goes down: a row held chooses its piece, which plays once the row is clicked again.
try click(&player, &keyboard, buttonAt(.play));
try std.testing.expect(!sound.musicPlaying());
try std.testing.expect(passAt(&player, &keyboard, rowAt(1), true));
try std.testing.expect(passAt(&player, &keyboard, rowAt(1), true));
try std.testing.expectEqual(1, player.selected.?);
try std.testing.expect(!sound.musicPlaying());
try std.testing.expect(passAt(&player, &keyboard, rowAt(1), false));
try click(&player, &keyboard, rowAt(1));
try std.testing.expect(sound.musicPlaying() and player.playing);
try std.testing.expectEqual(hog_snd.loudest, sound.music.level);
// The volume turns a step each pass its button is held, and the music with it.
try std.testing.expect(passAt(&player, &keyboard, buttonAt(.volume_down), true));
try std.testing.expect(passAt(&player, &keyboard, buttonAt(.volume_down), true));
try std.testing.expectEqual(full_volume - 2, volume);
try std.testing.expectEqual(full_volume - 2, sound.music.level);
for (0..3) |_| try std.testing.expect(passAt(&player, &keyboard, buttonAt(.volume_up), true));
try std.testing.expect(passAt(&player, &keyboard, buttonAt(.volume_up), false));
try std.testing.expectEqual(full_volume, volume);
// Paused, a piece's end is not followed; going on, it is, by the list's next.
try click(&player, &keyboard, buttonAt(.pause));
try std.testing.expect(player.paused and !sound.musicPlaying());
try std.testing.expect(passAt(&player, &keyboard, nowhere, false));
try std.testing.expectEqual(1, player.selected.?);
try click(&player, &keyboard, buttonAt(.pause));
try std.testing.expect(sound.musicPlaying());
sound.closeMusic();
try std.testing.expect(passAt(&player, &keyboard, nowhere, false));
try std.testing.expectEqual(2, player.selected.?);
try std.testing.expect(sound.musicPlaying());
// While the window is away, the piece paused for it hasn't ended, and nothing follows it.
sound.holdMusic(true);
try std.testing.expect(passAt(&player, &keyboard, nowhere, false));
try std.testing.expectEqual(2, player.selected.?);
sound.holdMusic(false);
// The previous track; and in RANDOM TRACK MODE, another piece follows one's end.
try click(&player, &keyboard, buttonAt(.previous));
try std.testing.expectEqual(1, player.selected.?);
try click(&player, &keyboard, buttonAt(.shuffle));
sound.closeMusic();
try std.testing.expect(passAt(&player, &keyboard, nowhere, false));
try std.testing.expect(player.selected.? != 1 and sound.musicPlaying());
// REPEAT TRACK MODE turns RANDOM TRACK MODE off, and nothing follows a piece's end.
try click(&player, &keyboard, buttonAt(.repeat));
try std.testing.expect(player.repeat and !player.shuffle);
sound.closeMusic();
try std.testing.expect(passAt(&player, &keyboard, nowhere, false));
try std.testing.expect(!sound.musicPlaying());
// STOP THE CURRENT TRACK ends the music and the choice; the next track is then the first.
try click(&player, &keyboard, buttonAt(.stop));
try std.testing.expectEqual(null, player.selected);
try click(&player, &keyboard, buttonAt(.next));
try std.testing.expectEqual(0, player.selected.?);
try std.testing.expect(sound.musicPlaying());
// LEAVE CD PLAYER closes it on the next pass, the music playing on.
try std.testing.expect(passAt(&player, &keyboard, buttonAt(.leave), true));
try std.testing.expect(!passAt(&player, &keyboard, buttonAt(.leave), false));
try std.testing.expect(sound.musicPlaying());
}
test "the CD player plays the lists the records install" {
// A mod puts a piece of its own in The Rise's row, the Yamato's last, with a name of its own.
var tracks = original;
tracks[23] = .{ .piece = "New_Pensive", .name = .{ .text = "OpenReliant Theme" } };
install(&tracks);
defer install(&original);
var tested: rooms.testing.Tested = undefined;
try testedWith(&tested, .yamato);
defer tested.deinit();
var keyboard: input.Keyboard = .{};
var volume: Volume = full_volume;
var player: CdPlayer = .open(.{ .rooms = tested.context(), .volume = &volume }, 19, 0);
defer player.deinit();
try std.testing.expectEqualStrings("OpenReliant Theme", player.list()[11].name.text);
// The rooms hold the installed list's music alone, so the row plays the mod's piece.
try click(&player, &keyboard, rowAt(11));
try click(&player, &keyboard, rowAt(11));
try std.testing.expect(tested.sound.musicPlaying());
}
test "the pages of the tracks mods add" {
// A mod adds three tracks after the Yamato's list, on a page of their own.
var tracks = original ++ [_]Track{
.{ .piece = "Theme_A", .name = .{ .text = "Theme A" } },
.{ .piece = "Theme_B", .name = .{ .text = "Theme B" } },
.{ .piece = "Theme_C", .name = .{ .text = "Theme C" } },
};
install(&tracks);
defer install(&original);
try std.testing.expectEqual(3, pageCount());
var tested: rooms.testing.Tested = undefined;
try testedWith(&tested, .reliant);
defer tested.deinit();
try tested.giveMusic(&.{ "music\\Theme_A.wav", "music\\Theme_B.wav", "music\\Theme_C.wav" });
var keyboard: input.Keyboard = .{};
var volume: Volume = full_volume;
var player: CdPlayer = .open(.{ .rooms = tested.context(), .volume = &volume }, 18, 0);
defer player.deinit();
// Its three rows, numbered on from the Yamato's.
player.turnTo(2);
try std.testing.expectEqual(3, player.list().len);
try std.testing.expectEqualStrings("25", player.number(0));
// The rows past its last piece hold nothing to choose.
try click(&player, &keyboard, rowAt(5));
try std.testing.expectEqual(null, player.selected);
// Its second piece plays, the next track is its third, and nothing follows the third.
try click(&player, &keyboard, rowAt(1));
try click(&player, &keyboard, rowAt(1));
try std.testing.expect(tested.sound.musicPlaying());
try click(&player, &keyboard, buttonAt(.next));
try std.testing.expectEqual(26, player.selected.?);
try click(&player, &keyboard, buttonAt(.next));
try std.testing.expectEqual(26, player.selected.?);
}
test "the Yamato's CD player plays its own list" {
var tested: rooms.testing.Tested = undefined;
try testedWith(&tested, .yamato);
defer tested.deinit();
var keyboard: input.Keyboard = .{};
var volume: Volume = 100;
var player: CdPlayer = .open(.{ .rooms = tested.context(), .volume = &volume }, 19, 0);
defer player.deinit();
try std.testing.expectEqual(rooms.Carrier.yamato, player.carrier);
// Its rows are numbered from 01, as on the Reliant.
try std.testing.expectEqualStrings("01", player.number(0));
// The next track from the first is the Yamato's second, played at the CD player's volume.
try click(&player, &keyboard, rowAt(0));
try click(&player, &keyboard, buttonAt(.next));
try std.testing.expect(tested.sound.musicPlaying());
try std.testing.expectEqual(100, tested.sound.music.level);
// Escape closes it.
keyboard.down[input.scan.escape] = true;
try std.testing.expect(!passAt(&player, &keyboard, nowhere, false));
}
test "a script turns the page" {
var tested: rooms.testing.Tested = undefined;
try testedWith(&tested, .reliant);
defer tested.deinit();
var keyboard: input.Keyboard = .{};
var volume: Volume = full_volume;
var player: CdPlayer = .open(.{ .rooms = tested.context(), .volume = &volume }, 18, 0);
defer player.deinit();
// The Reliant's second piece plays. Turned to the Yamato's list, its rows are numbered as the
// records number its pieces.
try click(&player, &keyboard, rowAt(1));
try click(&player, &keyboard, rowAt(1));
player.turnTo(1);
try std.testing.expectEqual(0x531, player.list()[0].name.string);
try std.testing.expectEqualStrings("13", player.number(0));
try std.testing.expectEqualStrings("24", player.number(11));
// The next track comes from the Reliant's page, where the piece was chosen.
try click(&player, &keyboard, buttonAt(.next));
try std.testing.expectEqual(2, player.selected.?);
// A row of the page shown chooses the Yamato's piece.
try click(&player, &keyboard, rowAt(3));
try std.testing.expectEqual(15, player.selected.?);
}