source: Readingame/main.cpp@ 3d27dc6

main
Last change on this file since 3d27dc6 was 3d27dc6, checked in by PulkoMandy <pulkomandy@…>, 10 months ago

Start making the game run

Implemented text rendering, buttons and hyperlinks.

Several opcodes not interpreted yet.

Global buttons are missing.

  • Property mode set to 100644
File size: 15.9 KB
Line 
1/*
2 * Copyright (C) 2023 PulkoMandy <pulkomandy@pullkomandy.tk>
3 *
4 * Distributed under terms of the MIT license.
5 */
6
7#include "HyperTextView.h"
8
9#include <Application.h>
10#include <Button.h>
11#include <ByteOrder.h>
12#include <Directory.h>
13#include <Entry.h>
14#include <File.h>
15#include <GroupLayout.h>
16#include <LayoutBuilder.h>
17#include <String.h>
18#include <TextEncoding.h>
19#include <TextView.h>
20#include <Window.h>
21
22#include <errno.h>
23#include <stdio.h>
24
25#include <string>
26#include <vector>
27
28#define BOLD "\x1B[34m"
29#define NORMAL "\x1B[0m"
30
31void disassemble(const std::vector<uint8_t>& script)
32{
33 size_t pc = 0;
34
35 while (pc < script.size()) {
36 uint8_t op = script[pc];
37 switch(op) {
38 case 0x00:
39 printf(BOLD "Return ");
40 pc += 1;
41 break;
42 case 0x01:
43 printf(BOLD "CheckFlag(%02x): ", script[pc + 1]);
44 pc += 2;
45 break;
46 case 0x03:
47 printf(BOLD "HaveItem(%02x): ", script[pc + 1]);
48 pc += 2;
49 break;
50 case 0x09:
51 printf(BOLD "IfEqual(%02x, %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
52 pc += 4;
53 break;
54 case 0x0A:
55 printf(BOLD "IfLess(%02x, %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
56 pc += 4;
57 break;
58 case 0x0D:
59 printf(BOLD "Hook(%02x%02x): ", script[pc + 2], script[pc + 1]);
60 pc += 3;
61 break;
62 case 0x0E:
63 printf(BOLD "GlobalHook(%02x): ", script[pc + 1]);
64 pc += 2;
65 break;
66 case 0x7E:
67 printf(BOLD "Or ");
68 pc += 1;
69 break;
70 case 0x7F:
71 printf(BOLD "Not "); // is it And?
72 pc += 1;
73 break;
74 case 0x80:
75 printf(NORMAL "80(%02x) ", script[pc + 1]);
76 pc += 2;
77 break;
78 case 0x81:
79 printf(BOLD "SetFlag(%02x) ", script[pc + 1]);
80 pc += 2;
81 break;
82 case 0x82:
83 printf(BOLD "ClearFlag(%02x) ", script[pc + 1]);
84 pc += 2;
85 break;
86 case 0x83:
87 printf(BOLD "GetItem(%02x) ", script[pc + 1]);
88 pc += 2;
89 break;
90 case 0x85:
91 printf(BOLD "LoseItem(%02x %02x) ", script[pc + 1], script[pc + 2]);
92 pc += 3;
93 break;
94 case 0x87:
95 printf(BOLD "SetCounter(%02x, %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
96 pc += 4;
97 break;
98 case 0x89:
99 printf(BOLD "Add(%02x, %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
100 pc += 4;
101 break;
102 case 0x8A:
103 printf(BOLD "Sub(%02x, %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
104 pc += 4;
105 break;
106 case 0x8F:
107 printf(BOLD "GoRoom(%02x) ", script[pc + 1]);
108 pc += 2;
109 break;
110 case 0x90:
111 printf(BOLD "GoScreen(%02x) ", script[pc + 1]);
112 pc += 2;
113 break;
114 case 0x91:
115 printf(BOLD "ShowGlobalScreen(%02x) ", script[pc + 1]);
116 pc += 2;
117 break;
118 case 0x93:
119 // Together with a GoRoom, allows to enter a room in a specific screen
120 printf(BOLD "WithScreen(%02x) ", script[pc + 1]);
121 pc += 2;
122 break;
123 case 0x95:
124 printf(BOLD "InitFlag(%02x, %02x) ", script[pc + 1], script[pc + 2]);
125 pc += 3;
126 break;
127 case 0xA0:
128 printf(BOLD "Compare(%02x, %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
129 pc += 4;
130 break;
131 case 0xFA:
132 printf(BOLD "End.");
133 pc++;
134 break;
135 case 0xFC:
136 printf(BOLD "Game Over ");
137 pc++;
138 break;
139 case 0xFD:
140 printf(BOLD "Next Script ");
141 pc++;
142 break;
143 case 0xFE:
144 printf(BOLD "Goto(%02x)", script[pc + 1]);
145 pc += 2;
146 break;
147 case 0xFF:
148 printf(BOLD "You won! ");
149 pc++;
150 break;
151 default:
152 printf(NORMAL "%02x ", op);
153 pc++;
154 break;
155 }
156 }
157 puts(NORMAL);
158}
159
160const char* cp850_lookup[0x80] = {
161 "Ç", "ü", "é", "â", "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", "É", "æ", "Æ",
162 "ô", "ö", "ò", "û", "ù", "ÿ", "Ö", "Ü", "ø", "£", "Ø", "×", "ƒ", "á", "í", "ó", "ú", "ñ", "Ñ",
163 "ª", "º", "¿", "®", "¬", "½", "¼", "¡", "«", "»", "░", "▒", "▓", "│", "┤", "Á", "Â", "À", "©",
164 "╣", "║", "╗", "╝", "¢", "¥", "┐", "└", "┴", "┬", "├", "─", "┼", "ã", "Ã", "╚", "╔", "╩", "╦",
165 "╠", "═", "╬", "¤", "ð", "Ð", "Ê", "Ë", "È", "ı", "Í", "Î", "Ï", "┘", "┌", "█", "▄", "¦", "Ì",
166 "▀", "Ó", "ß", "Ô", "Ò", "õ", "Õ", "µ", "þ", "Þ", "Ú", "Û", "Ù", "ý", "Ý", "¯", "´", "\u00AD",
167 "±", "‗", "¾", "¶", "§", "÷", "¸", "°", "¨", "·", "¹", "³", "²", "■", "\u00A0"
168};
169
170class Room {
171 public:
172 std::vector<std::string> fMessages;
173 std::vector<std::vector<uint8_t>> fScripts;
174};
175
176std::vector<Room> gRooms;
177std::vector<uint8_t> gVar8;
178std::vector<uint16_t> gVar16;
179
180uint8_t gCurrentRoom;
181uint8_t gCurrentScreen;
182uint8_t gTrigger;
183
184// TODO objects
185// TODO actions
186// TODO variables (c and f) and their init
187// TODO global buttons
188// TODO initial room
189// TODO other things from the init script
190
191class GameWindow: public BWindow
192{
193 public:
194 GameWindow()
195 : BWindow(BRect(40, 40, 500, 400), "Readingame", B_DOCUMENT_WINDOW,
196 B_AUTO_UPDATE_SIZE_LIMITS | B_QUIT_ON_WINDOW_CLOSE)
197 , fDecoder("cp-850")
198 {
199 fMainText = new HyperTextView("main text", B_WILL_DRAW);
200
201 // TODO making it non-editable while having custom insets breaks the layout somehow?
202 fMainText->MakeEditable(false);
203 fMainText->MakeSelectable(false);
204 //fMainText->SetInsets(10, 10, 10, 10);
205
206 fInlineButton = new BButton("button", NULL);
207
208 BLayoutBuilder::Group<>(this, B_VERTICAL)
209 .SetInsets(0, 0, 0, B_USE_WINDOW_SPACING)
210 .Add(fMainText, 999)
211 .AddGroup(B_HORIZONTAL)
212 .AddGlue()
213 .Add(fInlineButton)
214 .AddGlue()
215 .End()
216 .AddGlue(0)
217 .End();
218 }
219
220 void Show() override
221 {
222 BWindow::Show();
223 Lock();
224 fInlineButton->SetLowColor(make_color(255, 128, 255));
225 fMainText->SetViewColor(make_color(196, 255, 255));
226 fInlineButton->SetTarget(be_app);
227 Unlock();
228 }
229
230 void SetMainText(const std::string& text)
231 {
232 Lock();
233 fInlineButton->Hide();
234 fMainText->SetText("");
235
236 BString decoded;
237 PrettyPrint(text, decoded);
238
239 text_run_array runs;
240 runs.count = 1;
241 runs.runs->font = be_plain_font;
242 runs.runs->offset = 0;
243 runs.runs->color = make_color(0, 0, 0);
244 fMainText->Insert(decoded, &runs);
245 fMainText->Invalidate();
246 Unlock();
247 }
248
249 void PrettyPrint(const std::string& message, BString& decoded)
250 {
251 size_t cursor = 0;
252 while (cursor < message.size()) {
253 switch(message[cursor]) {
254 case '\\':
255 cursor += HandleEscape(message, cursor + 1);
256 break;
257 case '[':
258 {
259 text_run_array runs;
260 runs.count = 1;
261 runs.runs->font = be_plain_font;
262 runs.runs->offset = 0;
263 runs.runs->color = make_color(0, 0, 0);
264 fMainText->Insert(decoded, &runs);
265 decoded = "";
266 cursor += HandleHyperlink(message, cursor);
267 break;
268 }
269 case '@':
270 case '$':
271 // @ Gender mark (print an e if the player identifies as female)
272 // $ Plural mark (print an s if the counter is plural)
273 printf(BOLD "%c" NORMAL, message[cursor]);
274 cursor++;
275 break;
276 case '_':
277 // Used as a non-breakable space
278 decoded += "\u00A0";
279 cursor++;
280 break;
281 default:
282 if (message[cursor] >= 0)
283 decoded += message[cursor];
284 else
285 decoded += cp850_lookup[(message[cursor] - 0x80) & 0xFF];
286 cursor++;
287 break;
288 }
289 }
290 decoded += '\n';
291 }
292
293 int HandleHyperlink(const std::string& message, int cursor)
294 {
295 BString linkText;
296
297 int v = 0;
298 int i = 1;
299 while (isdigit(message[cursor + i])) {
300 v = v * 10 + message[cursor + i] - '0';
301 i++;
302 }
303
304 while (message[cursor + i] != ']') {
305 if (message[cursor + i] >= 0)
306 linkText += message[cursor + i];
307 else
308 linkText += cp850_lookup[(message[cursor + i] - 0x80) & 0xFF];
309 i++;
310 }
311
312 text_run_array single_run;
313 single_run.count = 1;
314 single_run.runs->offset = 0;
315 single_run.runs->font = be_plain_font;
316 single_run.runs->color = make_color(0, 0, 196);
317 fMainText->InsertHyperText(linkText, new HyperTextAction(v), &single_run);
318
319 return i + 1;
320 }
321
322 int HandleEscape(const std::string& message, int cursor)
323 {
324 if (message[cursor] == 'o') {
325 fInlineButton->SetLabel("OK");
326 fInlineButton->Show();
327 int v = 0;
328 int i = 1;
329 while (isdigit(message[cursor + i])) {
330 v = v * 10 + message[cursor + i] - '0';
331 i++;
332 }
333 fInlineButton->SetMessage(new BMessage(v));
334 return i + 1;
335 } else if (message[cursor] == '!') {
336 printf(BOLD "APPEND" NORMAL "\n");
337 return 2;
338 } else {
339 // TODO unknown escape sequence
340 fprintf(stderr, "ERROR: unknown escape sequence %c", message[cursor]);
341 return 2;
342 }
343 }
344
345 private:
346 HyperTextView* fMainText;
347 BButton* fInlineButton;
348 BPrivate::BTextEncoding fDecoder;
349};
350
351class Readingame: public BApplication
352{
353 public:
354 Readingame()
355 : BApplication("application/x-vnd.PulkoMandy.Readingame")
356 {
357 }
358
359 void ReadyToRun() override
360 {
361 // TODO check if we have a game loaded
362 fMainWindow = new GameWindow();
363
364 // FIXME add the global buttons to the main window
365
366 BWindow* debugWindow = new BWindow(BRect(600, 40, 900, 200), "Readingame debugger",
367 B_DOCUMENT_WINDOW, B_AUTO_UPDATE_SIZE_LIMITS | B_QUIT_ON_WINDOW_CLOSE);
368
369 fMainWindow->Show();
370 fMainWindow->CenterOnScreen();
371
372 // TODO: only if asked from the command line
373 debugWindow->Show();
374
375 bool runNextLine = true;
376 int line = 0;
377 while (runNextLine) {
378 runNextLine = RunScriptLine(gRooms[0].fScripts[line]);
379 line++;
380 }
381 fMainWindow->SetMainText(gRooms[gCurrentRoom].fMessages[gCurrentScreen - 1]);
382 }
383
384 void MessageReceived(BMessage* message) override
385 {
386 if (message->what < 256) {
387 gTrigger = message->what;
388 int line = 0;
389 bool runNextLine = true;
390 while (runNextLine) {
391 runNextLine = RunScriptLine(gRooms[0].fScripts[line]);
392 line++;
393 }
394 line = 0;
395 while (runNextLine) {
396 runNextLine = RunScriptLine(gRooms[gCurrentRoom].fScripts[line]);
397 line++;
398 }
399 fMainWindow->SetMainText(gRooms[gCurrentRoom].fMessages[gCurrentScreen - 1]);
400 } else {
401 BApplication::MessageReceived(message);
402 }
403 }
404
405 bool RunScriptLine(const std::vector<uint8_t>& script)
406 {
407 int pc = 0;
408 bool done = false;
409 bool runNextLine = false;
410
411 while (!done) {
412 switch(script[pc++]) {
413 case 0x00:
414 {
415 done = true;
416 break;
417 }
418 case 0x09:
419 {
420 uint8_t varAddress = script[pc++];
421 uint16_t varValue = script[pc++];
422 varValue = varValue | (script[pc++] << 8);
423 if (gVar16[varAddress] == varValue) {
424 continue;
425 } else {
426 done = true;
427 runNextLine = true;
428 }
429 }
430 case 0x0D:
431 {
432 uint8_t eventToCheck = script[pc++];
433 if (gTrigger == eventToCheck) {
434 fprintf(stderr, "unknown param to 0D: %02x\n", script[pc++]);
435 disassemble(script);
436 continue;
437 } else {
438 done = true;
439 runNextLine = true;
440 }
441 break;
442 }
443 case 0x0E:
444 {
445 uint8_t eventToCheck = script[pc++];
446 // FIXME is it really gTrigger here, or should it be something else?
447 if (gTrigger == eventToCheck)
448 continue;
449 else {
450 done = true;
451 runNextLine = true;
452 }
453 break;
454 }
455 case 0x7F:
456 {
457 // FIXME figure this out
458 pc++;
459 break;
460 }
461 case 0x87:
462 {
463 uint8_t varAddress = script[pc++];
464 uint16_t varValue = script[pc++];
465 varValue = varValue | (script[pc++] << 8);
466 gVar16[varAddress] = varValue;
467 break;
468 }
469 case 0x8F:
470 {
471 gCurrentRoom = script[pc++];
472 break;
473 }
474 case 0x90:
475 {
476 gCurrentScreen = script[pc++];
477 break;
478 }
479 case 0x93:
480 {
481 gCurrentScreen = script[pc++];
482 break;
483 }
484 case 0x95:
485 {
486 uint8_t varAddress = script[pc++];
487 uint8_t varValue = script[pc++];
488 gVar8[varAddress] = varValue;
489 break;
490 }
491 default:
492 fprintf(stderr, "ERROR: unknown opcode %02x at %04x\n", script[pc - 1], pc - 1);
493 disassemble(script);
494 done = true;
495 break;
496 }
497 }
498
499 return runNextLine;
500 }
501
502private:
503 GameWindow* fMainWindow;
504};
505
506void usage()
507{
508 fprintf(stderr,
509 "An engine for playing game originally designed for the Lectures Enjeu application\n\n"
510 "Usage: readingame path/to/gamedir/\n"
511 );
512}
513
514void ReadRoomsScripts(BFile& f)
515{
516 static int roomNumber = 0;
517
518 off_t fileSize;
519 f.GetSize(&fileSize);
520 while (fileSize > 0) {
521 uint16_t roomSize;
522 f.Read(&roomSize, 2);
523 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
524 fileSize -= roomSize;
525
526 if (roomSize == 0) {
527 fileSize -= 2;
528 break;
529 }
530
531 if (gRooms.size() < roomNumber + 1)
532 gRooms.resize(roomNumber + 1);
533
534 int i = 0;
535 while (roomSize > 0) {
536 uint16_t scriptSize;
537
538 if (f.Read(&scriptSize, 2) != 2) {
539 fprintf(stderr, "ERROR\n");
540 return;
541 }
542
543 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
544 if (scriptSize == 0) {
545 roomSize -= 2;
546 break;
547 }
548
549 if (gRooms[roomNumber].fScripts.size() < i + 1)
550 gRooms[roomNumber].fScripts.resize(i + 1);
551 gRooms[roomNumber].fScripts[i].resize(scriptSize - 2);
552 f.Read(gRooms[roomNumber].fScripts[i].data(), scriptSize - 2);
553
554 roomSize -= scriptSize;
555 i++;
556 }
557 roomNumber++;
558 }
559}
560
561void ReadRoomsMessages(BFile& f)
562{
563 static int roomNumber = 0;
564
565 off_t fileSize;
566 f.GetSize(&fileSize);
567 while (fileSize > 0) {
568 uint16_t roomSize;
569 f.Read(&roomSize, 2);
570 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
571 fileSize -= roomSize;
572
573 if (roomSize == 0) {
574 fileSize -= 2;
575 break;
576 }
577
578 roomSize -= 2;
579
580 if (gRooms.size() < roomNumber + 1)
581 gRooms.resize(roomNumber + 1);
582
583 int i = 0;
584 while (roomSize > 0) {
585 uint16_t scriptSize;
586
587 ssize_t r = f.Read(&scriptSize, 2);
588 if (r != 2) {
589 fprintf(stderr, "ERROR %d (%s)\n", r, strerror(errno));
590 return;
591 }
592 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
593 if (scriptSize == 0) {
594 roomSize -= 2;
595 break;
596 }
597
598 if (gRooms[roomNumber].fMessages.size() < i + 1)
599 gRooms[roomNumber].fMessages.resize(i + 1);
600 gRooms[roomNumber].fMessages[i].resize(scriptSize - 2);
601 f.Read(gRooms[roomNumber].fMessages[i].data(), scriptSize - 2);
602
603 roomSize -= scriptSize;
604 i++;
605 }
606 roomNumber++;
607 }
608}
609
610int main(int argc, char** argv)
611{
612 if (argc != 2) {
613 // TODO open a filepanel instead to load a game
614 // TODO or just look for the TITRES.DAT file
615 usage();
616 return 1;
617 }
618
619 BDirectory gameDir(argv[1]);
620
621 if (gameDir.InitCheck() != B_OK) {
622 fprintf(stderr, "Game directory %s not found.\n", argv[1]);
623 usage();
624 return 1;
625 }
626
627 BEntry entry;
628 BFile initFile;
629 BFile msgRoomsFile;
630 BFile msgGeneralFile;
631 BFile condRoomsFile;
632 BFile condGenFile;
633 BFile condUltiFile;
634
635 if (gameDir.FindEntry("INIT.DAT", &entry) == B_OK) {
636 initFile.SetTo(&entry, B_READ_ONLY);
637 } else {
638 fprintf(stderr, "Game does not contain an INIT.DAT file.\n");
639 return 2;
640 }
641
642 if (gameDir.FindEntry("MSG.DAT", &entry) == B_OK) {
643 msgRoomsFile.SetTo(&entry, B_READ_ONLY);
644 } else {
645 fprintf(stderr, "Game does not contain an MSG.DAT file.\n");
646 return 2;
647 }
648
649 if (gameDir.FindEntry("MSGGEN.DAT", &entry) == B_OK) {
650 msgGeneralFile.SetTo(&entry, B_READ_ONLY);
651 } else {
652 fprintf(stderr, "Game does not contain an MSGGEN.DAT file.\n");
653 return 2;
654 }
655
656 if (gameDir.FindEntry("CONDSAL.DAT", &entry) == B_OK) {
657 condRoomsFile.SetTo(&entry, B_READ_ONLY);
658 } else {
659 fprintf(stderr, "Game does not contain a CONDSAL.DAT file.\n");
660 return 2;
661 }
662
663 if (gameDir.FindEntry("CONDGALE.DAT", &entry) == B_OK) {
664 condGenFile.SetTo(&entry, B_READ_ONLY);
665 } else {
666 fprintf(stderr, "Game does not contain a CONDGALE.DAT file.\n");
667 return 2;
668 }
669
670 if (gameDir.FindEntry("CONDULTI.DAT", &entry) == B_OK) {
671 condUltiFile.SetTo(&entry, B_READ_ONLY);
672 } else {
673 fprintf(stderr, "Game does not contain a CONDULTI.DAT file.\n");
674 return 2;
675 }
676
677 ReadRoomsScripts(condGenFile);
678 ReadRoomsScripts(condRoomsFile);
679 ReadRoomsScripts(condUltiFile);
680
681 ReadRoomsMessages(msgGeneralFile);
682 ReadRoomsMessages(msgRoomsFile);
683
684 // TODO how many variables are there?
685 gVar8.resize(16);
686 gVar16.resize(16);
687
688 /*
689 int i = 0;
690 for (const auto& room: gRooms) {
691 printf("========= ROOM %d =========\n", i);
692
693 int j = 0;
694 for (auto text: room.fMessages) {
695 printf("------ SCREEN %d -------\n", j + 1);
696 prettyPrint(text);
697 j++;
698 }
699
700 j = 0;
701 for (auto script: room.fScripts) {
702 printf("%x:\t", j + 1);
703 disassemble(script);
704 j++;
705 }
706 i++;
707 }
708 */
709
710 Readingame app;
711
712 app.Run();
713}
Note: See TracBrowser for help on using the repository browser.