source: Readingame/main.cpp@ 4b1449d

main
Last change on this file since 4b1449d was 4b1449d, checked in by PulkoMandy <pulkomandy@…>, 2 months ago

Basic implementation of percent-escape to print extra messages

  • Property mode set to 100644
File size: 31.8 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 <Box.h>
11#include <Button.h>
12#include <ByteOrder.h>
13#include <Directory.h>
14#include <Entry.h>
15#include <File.h>
16#include <GroupLayout.h>
17#include <LayoutBuilder.h>
18#include <ListView.h>
19#include <OptionPopUp.h>
20#include <SeparatorView.h>
21#include <String.h>
22#include <StringView.h>
23#include <TextEncoding.h>
24#include <Window.h>
25
26#include <errno.h>
27#include <stdio.h>
28
29#include <string>
30#include <vector>
31
32#define COLOR "\x1B[33m"
33#define NORMAL "\x1B[0m"
34
35void disassemble(const std::vector<uint8_t>& script, BString& output)
36{
37 size_t pc = 0;
38 BString buffer;
39
40 while (pc < script.size()) {
41 uint8_t op = script[pc];
42 switch(op) {
43 case 0x00:
44 output += "Return ";
45 pc += 1;
46 break;
47 case 0x01:
48 buffer.SetToFormat("IsZero(%02x): ", script[pc + 1]);
49 output += buffer;
50 pc += 2;
51 break;
52 case 0x03:
53 buffer.SetToFormat("HaveItem(%02x): ", script[pc + 1]);
54 output += buffer;
55 pc += 2;
56 break;
57 case 0x09:
58 buffer.SetToFormat("IfEqual(WORDS[%02x], %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
59 output += buffer;
60 pc += 4;
61 break;
62 case 0x0A:
63 buffer.SetToFormat("IfLess(WORDS[%02x], %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
64 output += buffer;
65 pc += 4;
66 break;
67 case 0x0C:
68 buffer.SetToFormat("Random(BYTES[%02x], %02x): ", script[pc + 2], script[pc + 1]);
69 output += buffer;
70 pc += 2;
71 case 0x0D:
72 buffer.SetToFormat("Hook(%02x%02x): ", script[pc + 2], script[pc + 1]);
73 output += buffer;
74 pc += 3;
75 break;
76 case 0x0E:
77 buffer.SetToFormat("GlobalHook(%02x): ", script[pc + 1]);
78 output += buffer;
79 pc += 2;
80 break;
81 case 0x7E:
82 output += "OR ";
83 pc += 1;
84 break;
85 case 0x7F:
86 // There is at least one script with Hook immediately followed by HaveItem which
87 // seems to mean this is not required to form an AND condition. So it must be a NOT
88 // This breaks all scripts that I already got working however, so the meaning of
89 // all conditionals must be reversed? Which is strange, because HaveItem made more
90 // sense than the opposite for example.
91 output += "NOT ";
92 pc += 1;
93 break;
94 case 0x80:
95 buffer.SetToFormat("80(%02x) ", script[pc + 1]);
96 output += buffer;
97 pc += 2;
98 break;
99 case 0x81:
100 buffer.SetToFormat("NEG(BYTES[%02x]) ", script[pc + 1]);
101 output += buffer;
102 pc += 2;
103 break;
104 case 0x82:
105 buffer.SetToFormat("Increment(BYTES[%02x]) ", script[pc + 1]);
106 output += buffer;
107 pc += 2;
108 break;
109 case 0x83:
110 buffer.SetToFormat("GetItem(%02x) ", script[pc + 1]);
111 output += buffer;
112 pc += 2;
113 break;
114 case 0x85:
115 // What is the second parameter?
116 buffer.SetToFormat("LoseItem(%02x %02x) ", script[pc + 1], script[pc + 2]);
117 output += buffer;
118 pc += 3;
119 break;
120 case 0x87:
121 buffer.SetToFormat("Set(WORDS[%02x], %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
122 output += buffer;
123 pc += 4;
124 break;
125 case 0x89:
126 buffer.SetToFormat("Add(WORDS[%02x], %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
127 output += buffer;
128 pc += 4;
129 break;
130 case 0x8A:
131 buffer.SetToFormat("Sub(WORDS[%02x], %02x%02x) ", script[pc + 1], script[pc + 3], script[pc + 2]);
132 output += buffer;
133 pc += 4;
134 break;
135 case 0x8F:
136 buffer.SetToFormat("GoRoom(%02x) ", script[pc + 1]);
137 output += buffer;
138 pc += 2;
139 break;
140 case 0x90:
141 buffer.SetToFormat("GoScreen(%02x) ", script[pc + 1]);
142 output += buffer;
143 pc += 2;
144 break;
145 case 0x91:
146 buffer.SetToFormat("ShowGlobalScreen(%02x) ", script[pc + 1]);
147 output += buffer;
148 pc += 2;
149 break;
150 case 0x93:
151 // Sets the "home screen" for a room, that will be displayed if no hook sets a
152 // specific screen
153 buffer.SetToFormat("HomeScreen(%02x) ", script[pc + 1]);
154 output += buffer;
155 pc += 2;
156 break;
157 case 0x95:
158 buffer.SetToFormat("Set(BYTES[%02x], %02x) ", script[pc + 1], script[pc + 2]);
159 output += buffer;
160 pc += 3;
161 break;
162 case 0x97:
163 output += "WaitInput ";
164 pc++;
165 break;
166 // I guess 98 is BYTES+= and 99 is BYTES-= ?
167 case 0xA0:
168 buffer.SetToFormat("Compare(WORDS[%02x], %02x%02x) ", script[pc + 1],
169 script[pc + 3], script[pc + 2]);
170 output += buffer;
171 pc += 4;
172 break;
173 case 0xFA:
174 output += "End.";
175 pc++;
176 break;
177 case 0xFC:
178 output += "Game Over ";
179 pc++;
180 break;
181 case 0xFD:
182 output += "Next Script ";
183 pc++;
184 break;
185 case 0xFE:
186 buffer.SetToFormat("Goto(%02x)", script[pc + 1]);
187 pc += 2;
188 break;
189 case 0xFF:
190 output += "You won! ";
191 pc++;
192 break;
193 default:
194 buffer.SetToFormat("%02x ", op);
195 output += buffer;
196 pc++;
197 break;
198 }
199 }
200}
201
202const char* cp850_lookup[0x80] = {
203 "Ç", "ü", "é", "â", "ä", "à", "å", "ç", "ê", "ë", "è", "ï", "î", "ì", "Ä", "Å", "É", "æ", "Æ",
204 "ô", "ö", "ò", "û", "ù", "ÿ", "Ö", "Ü", "ø", "£", "Ø", "×", "ƒ", "á", "í", "ó", "ú", "ñ", "Ñ",
205 "ª", "º", "¿", "®", "¬", "½", "¼", "¡", "«", "»", "░", "▒", "▓", "│", "┤", "Á", "Â", "À", "©",
206 "╣", "║", "╗", "╝", "¢", "¥", "┐", "└", "┴", "┬", "├", "─", "┼", "ã", "Ã", "╚", "╔", "╩", "╦",
207 "╠", "═", "╬", "¤", "ð", "Ð", "Ê", "Ë", "È", "ı", "Í", "Î", "Ï", "┘", "┌", "█", "▄", "¦", "Ì",
208 "▀", "Ó", "ß", "Ô", "Ò", "õ", "Õ", "µ", "þ", "Þ", "Ú", "Û", "Ù", "ý", "Ý", "¯", "´", "\u00AD",
209 "±", "‗", "¾", "¶", "§", "÷", "¸", "°", "¨", "·", "¹", "³", "²", "■", "\u00A0"
210};
211
212const rgb_color EGA[] = {
213 {0, 0, 0},
214 {0, 0, 196},
215 {128, 255, 128},
216 {128, 255, 255},
217 {255, 128, 128},
218 {255, 128, 255},
219 {255, 255, 128},
220 {255, 255, 255},
221};
222
223class Room {
224 public:
225 std::vector<std::string> fMessages;
226 std::vector<std::vector<uint8_t>> fScripts;
227};
228
229struct ButtonDescriptor {
230 uint16_t x;
231 uint16_t y;
232 uint16_t color;
233 uint16_t reserved[3];
234 uint16_t trigger;
235 BString label;
236 BString shortcut;
237};
238
239
240struct Object {
241 uint8_t reserved[8];
242 BString name;
243 BString description;
244
245 uint8_t roomActions[3];
246 uint8_t inventoryActions[3];
247
248 bool inInventory;
249};
250
251
252std::vector<Room> gRooms;
253std::vector<uint8_t> gVar8;
254std::vector<uint16_t> gVar16;
255std::vector<Object> gObjects;
256
257std::vector<ButtonDescriptor> gButtons;
258
259uint8_t gCurrentRoom;
260uint8_t gCurrentScreen;
261uint8_t gLine;
262uint8_t gHomeScreen;
263uint8_t gTrigger;
264uint8_t gGlobalTrigger;
265uint8_t gSavePC;
266
267uint32_t gLastRunScript;
268
269// TODO global buttons
270
271class DebugWindow: public BWindow
272{
273 public:
274 DebugWindow()
275 : BWindow(BRect(40, 40, 750, 800), "Readingame debugger", B_DOCUMENT_WINDOW,
276 B_AUTO_UPDATE_SIZE_LIMITS | B_QUIT_ON_WINDOW_CLOSE)
277 , fDecoder("cp-850")
278 {
279 BBox* roomBox = new BBox("room");
280 fRoomList = new BOptionPopUp("room popup", "Room", new BMessage('ROOM'));
281 roomBox->SetLabel(fRoomList);
282
283 fScreenDump = new BTextView("screen text", B_WILL_DRAW);
284
285 fDisassembly = new BListView("script disassemly");
286 fDisassembly->SetFont(be_fixed_font);
287
288 fScreenList = new BOptionPopUp("screens popup", "Screen", new BMessage('SCRN'));
289
290 BGroupView* roomView = new BGroupView(B_VERTICAL);
291
292 BLayoutBuilder::Group<>(roomView)
293 .SetInsets(B_USE_DEFAULT_SPACING)
294 .AddGroup(B_HORIZONTAL)
295 .Add(fScreenList)
296 .AddGlue()
297 .End()
298 .Add(fScreenDump)
299 .Add(fDisassembly)
300 .End();
301
302 roomBox->AddChild(roomView);
303
304 for (int i = 0; i < gRooms.size(); i++) {
305 BString name;
306 if (i == 0)
307 name = "General";
308 else
309 name.SetToFormat("%02d", i);
310 fRoomList->AddOption(name.String(), i);
311 }
312
313 // TODO room messages
314 // TODO room scripts
315 // TODO current room/screen (make them bold+button to quickly reach them)
316
317 fGlobalTrigger = new BStringView("global trigger", "Global Trigger: ");
318 fRoomTrigger = new BStringView("room trigger", "Room Trigger: ");
319
320 fFlagsList = new BListView("flags");
321 fByteList = new BListView("flags");
322 fWordList = new BListView("flags");
323
324 for (int i = 0; i < gObjects.size(); i++)
325 fFlagsList->AddItem(new BStringItem(""));
326 for (int i = 0; i < gVar8.size(); i++)
327 fByteList->AddItem(new BStringItem(""));
328 for (int i = 0; i < gVar16.size(); i++)
329 fWordList->AddItem(new BStringItem(""));
330
331 BLayoutBuilder::Group<>(this, B_HORIZONTAL)
332 .SetInsets(B_USE_WINDOW_SPACING)
333 .Add(roomBox)
334 .AddGrid(B_VERTICAL)
335 .Add(fGlobalTrigger, 0, 0)
336 .Add(fRoomTrigger, 1, 0)
337 .Add(fWordList, 0, 1)
338 .Add(fByteList, 1, 1)
339 .Add(fFlagsList, 0, 2, 2)
340 .End()
341 .End();
342 }
343
344 // TODO allow to show different rooms and screens
345
346 void Update()
347 {
348 Lock();
349 fRoomList->SetValue(gCurrentRoom);
350
351 BString label;
352 label.SetToFormat("Global Trigger: %d", gGlobalTrigger);
353 fGlobalTrigger->SetText(label);
354 label.SetToFormat("Room Trigger: %d", gTrigger);
355 fRoomTrigger->SetText(label);
356
357 ShowRoom(gCurrentRoom);
358
359 for (int i = 0; i < gObjects.size(); i++)
360 {
361 BString formatted;
362 // TODO show the object names as well
363 char buffer[25];
364 size_t out = sizeof(buffer);
365 BString& name = gObjects[i].name;
366 size_t in = name.Length();
367 fDecoder.Decode(name.String(), in, buffer, out);
368 formatted.SetToFormat("%02x: %s %s", i, gObjects[i].inInventory ? "✓" : " ", buffer);
369 BStringItem* item = (BStringItem*)fFlagsList->ItemAt(i);
370 if (item->Text() == formatted)
371 item->SetEnabled(false);
372 else {
373 item->SetEnabled(true);
374 item->SetText(formatted);
375 }
376 }
377 fFlagsList->Invalidate();
378 for (int i = 0; i < gVar8.size(); i++)
379 {
380 BString formatted;
381 formatted.SetToFormat("%02x: %02x", i, gVar8[i]);
382 BStringItem* item = (BStringItem*)fByteList->ItemAt(i);
383 if (item->Text() == formatted)
384 item->SetEnabled(false);
385 else {
386 item->SetEnabled(true);
387 item->SetText(formatted);
388 }
389 }
390 fByteList->Invalidate();
391 for (int i = 0; i < gVar16.size(); i++)
392 {
393 BString formatted;
394 formatted.SetToFormat("%02x: %04x", i, gVar16[i]);
395 BStringItem* item = (BStringItem*)fWordList->ItemAt(i);
396 if (item->Text() == formatted)
397 item->SetEnabled(false);
398 else {
399 item->SetEnabled(true);
400 item->SetText(formatted);
401 }
402 }
403 fWordList->Invalidate();
404
405 Unlock();
406 }
407
408 void ShowRoom(int32 room)
409 {
410 int32 screen = 1;
411 if (room == gCurrentRoom)
412 screen = gCurrentScreen;
413
414 while (fScreenList->CountOptions() > 0)
415 fScreenList->RemoveOptionAt(0);
416 for (int i = 0; i < gRooms[room].fMessages.size(); i++)
417 {
418 BString name;
419 name.SetToFormat("%02d", i + 1);
420 fScreenList->AddOption(name, i);
421 }
422 fScreenList->SetValue(screen - 1);
423 ShowScreen(screen - 1);
424
425 // update script disassembly (and remove from stdout)
426 while (fDisassembly->CountItems() > 0)
427 fDisassembly->RemoveItem(0);
428 for (int i = 0; i < gRooms[room].fScripts.size(); i++)
429 {
430 BString output;
431 disassemble(gRooms[room].fScripts[i], output);
432 BStringItem* item = new BStringItem(output);
433 if (((room << 16) | i) == gLastRunScript)
434 item->SetEnabled(false);
435 fDisassembly->AddItem(item);
436 }
437 }
438
439 void ShowScreen(int32 screen)
440 {
441 if ((screen < 0) || (gRooms[fRoomList->Value()].fMessages.size() <= screen)) {
442 fScreenDump->SetText("<no screens>");
443 return;
444 }
445
446 char buffer[80*25] = {0};
447 size_t out = sizeof(buffer);
448 std::string& str = gRooms[fRoomList->Value()].fMessages[screen];
449 size_t in = str.length();
450 fDecoder.Decode(str.c_str(), in, buffer, out);
451 fScreenDump->SetText(buffer);
452 }
453
454 void MessageReceived(BMessage* message)
455 {
456 switch (message->what) {
457 case 'ROOM':
458 {
459 ShowRoom(message->FindInt32("be:value"));
460 break;
461 }
462 case 'SCRN':
463 {
464 ShowScreen(message->FindInt32("be:value"));
465 break;
466 }
467 default:
468 BWindow::MessageReceived(message);
469 break;
470 }
471 }
472
473 private:
474 BOptionPopUp* fRoomList;
475 BOptionPopUp* fScreenList;
476 BTextView* fScreenDump;
477 BListView* fDisassembly;
478
479 BStringView* fGlobalTrigger;
480 BStringView* fRoomTrigger;
481
482 BListView* fFlagsList;
483 BListView* fByteList;
484 BListView* fWordList;
485
486 BPrivate::BTextEncoding fDecoder;
487};
488
489class GameWindow: public BWindow
490{
491 public:
492 GameWindow()
493 : BWindow(BRect(40, 40, 500, 400), "Readingame", B_DOCUMENT_WINDOW,
494 B_AUTO_UPDATE_SIZE_LIMITS | B_QUIT_ON_WINDOW_CLOSE)
495 {
496 fMainText = new HyperTextView("main text", B_WILL_DRAW);
497
498 // TODO making it non-editable while having custom insets breaks the layout somehow?
499 fMainText->MakeEditable(false);
500 fMainText->MakeSelectable(false);
501 fMainText->SetInsets(10, 10, 10, 10);
502
503 fInlineButton = new BButton("button", NULL);
504
505 BGridView* bottomArea = new BGridView();
506 fGrid = bottomArea->GridLayout();
507 fGrid->SetSpacing(0, 0);
508
509 for (const auto& button: gButtons)
510 {
511 BString label(button.label);
512 label.RemoveAll("$");
513 BButton* widget = new BButton(label, new BMessage(button.trigger));
514 // widget->SetLowColor(EGA[button.color]);
515 // Done in Show instead because it gets overriden
516 fGrid->AddView(widget, button.x, button.y, 12, 1);
517 }
518
519 BSeparatorView* sv = new BSeparatorView(B_HORIZONTAL);
520 sv->SetExplicitMinSize(BSize(515, 16));
521 fGrid->AddView(sv, 0, 0, 80);
522
523 for (int i = 0; i < 80; i++)
524 {
525 fGrid->SetMinColumnWidth(i, 4);
526 }
527
528 BLayoutBuilder::Group<>(this, B_VERTICAL)
529 .SetInsets(0, 0, 0, B_USE_WINDOW_SPACING)
530 .Add(fMainText, 1)
531 .AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 0)
532 .GetLayout(&fLayout)
533 .AddGlue()
534 .Add(fInlineButton)
535 .AddGlue()
536 .End()
537 .Add(bottomArea)
538 .End();
539 }
540
541 void Show() override
542 {
543 BWindow::Show();
544 Lock();
545 fInlineButton->SetLowColor(make_color(255, 128, 255));
546 fMainText->SetViewColor(make_color(196, 255, 255));
547 fInlineButton->SetTarget(be_app);
548
549 for (int i = 0; i < gButtons.size(); i++)
550 {
551 BView* v = ((BLayout*)fGrid)->ItemAt(i)->View();
552 v->SetLowColor(EGA[gButtons[i].color]);
553 dynamic_cast<BControl*>(v)->SetTarget(be_app);
554 }
555
556 Unlock();
557 }
558
559 void SetButtonVisible(bool visible)
560 {
561 int32 index = fLayout->IndexOfView(fInlineButton);
562 fLayout->ItemAt(index)->SetVisible(visible);
563 }
564
565 void SetMainText(const std::string& text)
566 {
567 Lock();
568 SetButtonVisible(false);
569 fMainText->SetText("");
570
571 BString decoded;
572 PrettyPrint(text, decoded);
573
574 text_run_array runs;
575 runs.count = 1;
576 runs.runs->font = be_fixed_font;
577 runs.runs->offset = 0;
578 runs.runs->color = make_color(0, 0, 0);
579 fMainText->Insert(decoded, &runs);
580 fMainText->Invalidate();
581 Unlock();
582 }
583
584 void PrettyPrint(const std::string& message, BString& decoded)
585 {
586 size_t cursor = 0;
587 while (cursor < message.size()) {
588 switch(message[cursor]) {
589 case '\\':
590 cursor += HandleEscape(message, cursor + 1);
591 break;
592 case '%':
593 cursor += HandlePercentEscape(message, cursor + 1, decoded);
594 break;
595 case '[':
596 {
597 text_run_array runs;
598 runs.count = 1;
599 runs.runs->font = be_fixed_font;
600 runs.runs->offset = 0;
601 runs.runs->color = make_color(0, 0, 0);
602 fMainText->Insert(decoded, &runs);
603 decoded = "";
604 cursor += HandleHyperlink(message, cursor);
605 break;
606 }
607 case '{':
608 {
609 text_run_array runs;
610 runs.count = 1;
611 runs.runs->font = be_plain_font;
612 runs.runs->offset = 0;
613 runs.runs->color = make_color(0, 0, 0);
614 fMainText->Insert(decoded, &runs);
615 decoded = "";
616 cursor += HandleConditional(message, cursor);
617 break;
618 }
619 case '@':
620 {
621 // @ Gender mark (print an e if the player identifies as female)
622 // TODO we don't have a way to configure player gender yet
623 decoded += 'e';
624 cursor++;
625 break;
626 }
627 case '$':
628 // $ Plural mark (print an s if the counter is plural)
629 // TODO not implemented yet
630 printf(COLOR "%c\n" NORMAL, message[cursor]);
631 cursor++;
632 break;
633 case '_':
634 // Used as a non-breakable space
635 decoded += "\u00A0";
636 cursor++;
637 break;
638 default:
639 if (message[cursor] >= 0)
640 decoded += message[cursor];
641 else
642 decoded += cp850_lookup[(message[cursor] - 0x80) & 0xFF];
643 cursor++;
644 break;
645 }
646 }
647 decoded += '\n';
648 }
649
650 int ParseInt(const std::string& message, int& cursor)
651 {
652 int v = 0;
653 while (isdigit(message[cursor])) {
654 v = v * 10 + message[cursor] - '0';
655 cursor++;
656 }
657
658 return v;
659 }
660
661 int HandleConditional(const std::string& message, int cursor)
662 {
663 int icursor = cursor;
664 // {%f3|%m2~%m3}
665 cursor++;
666 if (message[cursor] != '%' || message[cursor + 1] != 'f') {
667 fprintf(stderr, "Unknwown message conditional\n");
668 return 1;
669 }
670
671 cursor += 2;
672
673 int flagToCheck = ParseInt(message, cursor);
674 uint8_t valueToCheck = gVar8[flagToCheck];
675
676 if (message[cursor] != '|') {
677 fprintf(stderr, "Unknwown message conditional action\n");
678 return 1;
679 }
680
681 for(;;) {
682 if (!valueToCheck && message[cursor] == '|')
683 break;
684 if (valueToCheck && message[cursor] == '~')
685 break;
686 cursor++;
687 }
688 cursor++;
689
690 if (message[cursor] != '%' || message[cursor + 1] != 'm') {
691 fprintf(stderr, "Unknwown message output %c%c\n", message[cursor], message[cursor + 1]);
692 }
693 cursor += 2;
694
695 int messageToDisplay = ParseInt(message, cursor);
696 BString decodedMessage;
697 PrettyPrint(gRooms[gCurrentRoom].fMessages[messageToDisplay - 1], decodedMessage);
698 fMainText->Insert(decodedMessage);
699
700 do {
701 cursor++;
702 } while (message[cursor] != '}');
703 cursor++;
704
705 return cursor - icursor;
706 }
707
708 int HandleHyperlink(const std::string& message, int cursor)
709 {
710 BString linkText;
711
712 int v = 0;
713 int i = 1;
714 while (isdigit(message[cursor + i])) {
715 v = v * 10 + message[cursor + i] - '0';
716 i++;
717 }
718
719 while (message[cursor + i] != ']') {
720 if (message[cursor + i] >= 0)
721 linkText += message[cursor + i];
722 else
723 linkText += cp850_lookup[(message[cursor + i] - 0x80) & 0xFF];
724 i++;
725 }
726
727 text_run_array single_run;
728 single_run.count = 1;
729 single_run.runs->offset = 0;
730 single_run.runs->font = be_plain_font;
731 single_run.runs->color = make_color(0, 0, 196);
732 fMainText->InsertHyperText(linkText, new HyperTextAction(v), &single_run);
733
734 return i + 1;
735 }
736
737 int HandlePercentEscape(const std::string& message, int cursor, BString& decoded)
738 {
739 if (message[cursor] == 'M') {
740 int value = message[cursor + 1] - '0';
741 PrettyPrint(gRooms[0].fMessages[value - 1], decoded);
742 return 3;
743 } else {
744 // TODO unknown escape sequence
745 fprintf(stderr, "ERROR: unknown percent escape sequence %c\n", message[cursor]);
746 return 2;
747 }
748 }
749
750 int HandleEscape(const std::string& message, int cursor)
751 {
752 if (message[cursor] == 'o') {
753 fInlineButton->SetLabel("OK");
754 SetButtonVisible(true);
755 int v = 0;
756 int i = 1;
757 while (isdigit(message[cursor + i])) {
758 v = v * 10 + message[cursor + i] - '0';
759 i++;
760 }
761
762 // If there is no specified value, the default is 0x64
763 if (i == 1)
764 v = 0x64;
765
766 fInlineButton->SetMessage(new BMessage(v));
767 return i + 1;
768 } else if (message[cursor] == '!') {
769 printf(COLOR "APPEND" NORMAL "\n");
770 return 2;
771 } else {
772 // TODO unknown escape sequence
773 fprintf(stderr, "ERROR: unknown escape sequence %c\n", message[cursor]);
774 return 2;
775 }
776 }
777
778 private:
779 HyperTextView* fMainText;
780 BButton* fInlineButton;
781 BGroupLayout* fLayout;
782 BGridLayout* fGrid;
783};
784
785class Readingame: public BApplication
786{
787 public:
788 Readingame()
789 : BApplication("application/x-vnd.PulkoMandy.Readingame")
790 {
791 }
792
793 void ReadyToRun() override
794 {
795 // TODO check if we have a game loaded
796 fMainWindow = new GameWindow();
797
798 // FIXME add the global buttons to the main window
799
800 fDebugWindow = new DebugWindow();
801
802 fMainWindow->Show();
803 fMainWindow->CenterOnScreen();
804
805 // TODO: only if asked from the command line
806 fDebugWindow->Show();
807
808 // Initial update so we can get the proper highlights for changed variables after running
809 // the init
810 fDebugWindow->Update();
811
812 bool runNextLine = true;
813 int line = 0;
814 while (runNextLine && (line < gRooms[0].fScripts.size())) {
815 gSavePC = 0;
816 runNextLine = RunScriptLine(gRooms[0].fScripts[line]);
817 gLastRunScript = line;
818 line++;
819 }
820 fMainWindow->SetMainText(gRooms[gCurrentRoom].fMessages[gCurrentScreen - 1]);
821 fDebugWindow->Update();
822 }
823
824 void MessageReceived(BMessage* message) override
825 {
826 if (message->what < 256) {
827 gTrigger = message->what;
828
829 bool runNextLine = true;
830
831 if (gSavePC != 0) {
832 goto resume;
833 }
834
835 gLine = 0;
836 gCurrentScreen = gHomeScreen;
837 while (runNextLine && (gLine < gRooms[0].fScripts.size())) {
838 gSavePC = 0;
839 runNextLine = RunScriptLine(gRooms[0].fScripts[gLine]);
840 gLastRunScript = gLine;
841 gLine++;
842 }
843 gLine = 0;
844 while (runNextLine && (gLine < gRooms[gCurrentRoom].fScripts.size())) {
845 gSavePC = 0;
846resume:
847 runNextLine = RunScriptLine(gRooms[gCurrentRoom].fScripts[gLine]);
848 gLastRunScript = gLine | (gCurrentRoom << 16);
849 if (gSavePC == 0)
850 gLine++;
851 }
852
853 if (gSavePC == 0) {
854 fMainWindow->SetMainText(gRooms[gCurrentRoom].fMessages[gCurrentScreen - 1]);
855 gLine = 0;
856 }
857 fDebugWindow->Update();
858 } else {
859 BApplication::MessageReceived(message);
860 }
861 }
862
863 bool RunScriptLine(const std::vector<uint8_t>& script)
864 {
865 int pc = gSavePC;
866 gSavePC = 0;
867 bool done = false;
868 bool runNextLine = false;
869 bool conditionIsInverted = false;
870
871 while (!done) {
872 switch(script[pc++]) {
873 case 0x00:
874 {
875 done = true;
876 break;
877 }
878 case 0x01:
879 {
880 uint8_t varAddress = script[pc++];
881 if (conditionIsInverted ^ (gVar8[varAddress] != 0)) {
882 done = true;
883 runNextLine = true;
884 }
885 conditionIsInverted = false;
886 break;
887 }
888 case 0x03:
889 {
890 uint8_t varAddress = script[pc++] - 1;
891 if (conditionIsInverted ^ (gObjects[varAddress].inInventory == 0)) {
892 done = true;
893 runNextLine = true;
894 }
895 conditionIsInverted = false;
896 break;
897 }
898 case 0x09:
899 {
900 uint8_t varAddress = script[pc++];
901 uint16_t varValue = script[pc++];
902 varValue = varValue | (script[pc++] << 8);
903 if (conditionIsInverted ^ (gVar16[varAddress] != varValue)) {
904 done = true;
905 runNextLine = true;
906 }
907 conditionIsInverted = false;
908 break;
909 }
910 case 0x0C:
911 {
912 uint8_t maxValue = script[pc++];
913 uint8_t varAddress = script[pc++];
914 gVar8[varAddress] = random() % maxValue;
915 break;
916 }
917 case 0x0D:
918 {
919 uint16_t eventToCheck = script[pc++];
920 eventToCheck |= script[pc++] << 8;
921 if (conditionIsInverted ^ (gTrigger != eventToCheck)) {
922 done = true;
923 runNextLine = true;
924 }
925 conditionIsInverted = false;
926 break;
927 }
928 case 0x0E:
929 {
930 uint8_t eventToCheck = script[pc++];
931 // FIXME is it really gTrigger here, or should it be something else?
932 if (gGlobalTrigger == eventToCheck) {
933 gGlobalTrigger++;
934 } else {
935 done = true;
936 runNextLine = true;
937 }
938 break;
939 }
940 case 0x7E:
941 {
942 // FIXME figure this out
943 break;
944 }
945
946 case 0x7F:
947 {
948 conditionIsInverted = true;
949 break;
950 }
951 case 0x81:
952 {
953 uint8_t varAddress = script[pc++];
954 gVar8[varAddress] = !gVar8[varAddress];
955 break;
956 }
957 case 0x83:
958 {
959 uint8_t varAddress = script[pc++];
960 gObjects[varAddress - 1].inInventory = true;
961 break;
962 }
963 case 0x87:
964 {
965 uint8_t varAddress = script[pc++];
966 uint16_t varValue = script[pc++];
967 varValue = varValue | (script[pc++] << 8);
968 gVar16[varAddress] = varValue;
969 break;
970 }
971 case 0x8A:
972 {
973 uint8_t varAddress = script[pc++];
974 uint16_t varValue = script[pc++];
975 varValue = varValue | (script[pc++] << 8);
976 gVar16[varAddress] -= varValue;
977 break;
978 }
979 case 0x8F:
980 {
981 gCurrentRoom = script[pc++];
982 break;
983 }
984 case 0x90:
985 {
986 gCurrentScreen = script[pc++];
987 break;
988 }
989 case 0x91:
990 {
991 fMainWindow->SetMainText(gRooms[0].fMessages[script[pc++]]);
992 break;
993 }
994 case 0x93:
995 {
996 gHomeScreen = script[pc++];
997 gCurrentScreen = gHomeScreen;
998 break;
999 }
1000 case 0x95:
1001 {
1002 uint8_t varAddress = script[pc++];
1003 uint8_t varValue = script[pc++];
1004 gVar8[varAddress] = varValue;
1005 break;
1006 }
1007 case 0x97:
1008 {
1009 gSavePC = pc;
1010 done = true;
1011 break;
1012 }
1013 case 0xA0:
1014 {
1015 // FIXME: the global hooks are always evaluated anyways?
1016 pc += 3;
1017 break;
1018 }
1019 case 0xFC:
1020 {
1021 printf("Game over");
1022 done = true;
1023 break;
1024 }
1025 default:
1026 fprintf(stderr, "ERROR: unknown opcode %02x at %04x (screen %d)\n", script[pc - 1], pc - 1, gHomeScreen);
1027 for (int k = 0; k < script.size(); k++) {
1028 if (k == pc - 1)
1029 printf("*");
1030 printf("%02x ", script[k]);
1031 }
1032 puts("");
1033 done = true;
1034 break;
1035 }
1036
1037 if (done && (script[pc] == 0x7E)) {
1038 done = false;
1039 }
1040 }
1041
1042 return runNextLine;
1043 }
1044
1045private:
1046 GameWindow* fMainWindow;
1047 DebugWindow* fDebugWindow;
1048};
1049
1050void usage()
1051{
1052 fprintf(stderr,
1053 "An engine for playing game originally designed for the Lectures Enjeu application\n\n"
1054 "Usage: readingame path/to/gamedir/\n"
1055 );
1056}
1057
1058void ReadString(BFile& f, BString& output)
1059{
1060 char buffer[256];
1061 int off = 0;
1062
1063 do {
1064 f.Read(buffer + off, 1);
1065 off++;
1066 } while (buffer[off - 1] != 0);
1067
1068 output = buffer;
1069}
1070
1071void ReadInit(BFile& f)
1072{
1073 uint16_t header[40];
1074 f.Read(&header, 80);
1075
1076 for (int i = 0; i < 40; i++)
1077 {
1078 header[i] = B_LENDIAN_TO_HOST_INT16(header[i]);
1079 //printf("%04x ", header[i]);
1080 }
1081 //puts("");
1082
1083 off_t fileSize;
1084 f.GetSize(&fileSize);
1085 while (fileSize > 0) {
1086 uint16_t chunkId;
1087 f.Read(&chunkId, 2);
1088 chunkId = B_LENDIAN_TO_HOST_INT16(chunkId);
1089
1090 fileSize -= 2;
1091
1092 switch (chunkId) {
1093 case 1:
1094 {
1095 // widgets
1096 ButtonDescriptor button;
1097
1098 for(;;) {
1099 f.Read(&button.x, 2);
1100 f.Read(&button.y, 2);
1101
1102 if ((button.x == 0) && (button.y == 0))
1103 break;
1104
1105 f.Read(&button.color, 2);
1106 f.Read(&button.reserved, 6);
1107 f.Read(&button.trigger, 2);
1108
1109 char buffer[256];
1110 int off = 0;
1111
1112 ReadString(f, button.label);
1113 ReadString(f, button.shortcut);
1114
1115 button.x = B_LENDIAN_TO_HOST_INT16(button.x);
1116 button.y = B_LENDIAN_TO_HOST_INT16(button.y);
1117 button.trigger = B_LENDIAN_TO_HOST_INT16(button.trigger);
1118
1119 printf("Button: x %d y %d c %d t %04x %s %s (%04x %04x %04x)\n", button.x, button.y,
1120 button.color, button.trigger, button.label.String(), button.shortcut.String(),
1121 button.reserved[0], button.reserved[1], button.reserved[2]);
1122 gButtons.push_back(button);
1123 }
1124 break;
1125 }
1126 case 2:
1127 {
1128 // actions
1129 struct Action {
1130 uint8_t id;
1131 BString label;
1132 BString shortcut;
1133 } action;
1134
1135 for(;;) {
1136 f.Read(&action.id, 1);
1137 if (action.id == 0)
1138 break;
1139 ReadString(f, action.label);
1140 ReadString(f, action.shortcut);
1141 printf("Action: %02x %s %s\n", action.id, action.shortcut.String(), action.label.String());
1142 }
1143
1144 break;
1145 }
1146 case 3:
1147 {
1148 uint16_t header;
1149 f.Read(&header, 2);
1150 header = B_LENDIAN_TO_HOST_INT16(header);
1151 printf("Object header: %d\n", header);
1152
1153 Object object;
1154
1155 for (;;) {
1156 f.Read(&object.reserved[0], 1);
1157 if (object.reserved[0] == 0)
1158 break;
1159 f.Read(&object.reserved[1], 7);
1160 ReadString(f, object.name);
1161 ReadString(f, object.description);
1162 f.Read(&object.roomActions, 3);
1163 f.Read(&object.inventoryActions, 3);
1164 printf("Object: %s (%s) - Room actions %x %x %x / Inventory %x %x %x\n ",
1165 object.name.String(), object.description.String(),
1166 object.roomActions[0], object.roomActions[1], object.roomActions[2],
1167 object.inventoryActions[0], object.inventoryActions[1], object.inventoryActions[2]);
1168 for (int j = 0; j < 8; j++)
1169 printf("%02x ", object.reserved[j]);
1170 printf("\n");
1171
1172 gObjects.push_back(object);
1173 }
1174 // objects
1175 break;
1176 }
1177 case 4:
1178 {
1179 // descriptions
1180 break;
1181 }
1182 case 5:
1183 {
1184 // action results
1185 break;
1186 }
1187 case 6:
1188 {
1189 // compass
1190 break;
1191 }
1192 case 7:
1193 {
1194 // TODO
1195 break;
1196 }
1197 case 8:
1198 {
1199 // TODO
1200 break;
1201 }
1202 case 9:
1203 {
1204 // TODO
1205 break;
1206 }
1207 }
1208
1209 // TODO remove when we parse more chunks
1210 if (chunkId == 3)
1211 break;
1212 }
1213}
1214
1215void ReadRoomsScripts(BFile& f)
1216{
1217 static int roomNumber = 0;
1218
1219 off_t fileSize;
1220 f.GetSize(&fileSize);
1221 while (fileSize > 0) {
1222 uint16_t roomSize;
1223 f.Read(&roomSize, 2);
1224 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
1225 fileSize -= roomSize;
1226
1227 if (roomSize == 0) {
1228 fileSize -= 2;
1229 break;
1230 }
1231
1232 if (gRooms.size() < roomNumber + 1)
1233 gRooms.resize(roomNumber + 1);
1234
1235 int i = 0;
1236 while (roomSize > 0) {
1237 uint16_t scriptSize;
1238
1239 if (f.Read(&scriptSize, 2) != 2) {
1240 fprintf(stderr, "ERROR\n");
1241 return;
1242 }
1243
1244 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
1245 if (scriptSize == 0) {
1246 roomSize -= 2;
1247 break;
1248 }
1249
1250 if (gRooms[roomNumber].fScripts.size() < i + 1)
1251 gRooms[roomNumber].fScripts.resize(i + 1);
1252 gRooms[roomNumber].fScripts[i].resize(scriptSize - 2);
1253 f.Read(gRooms[roomNumber].fScripts[i].data(), scriptSize - 2);
1254
1255 roomSize -= scriptSize;
1256 i++;
1257 }
1258 roomNumber++;
1259 }
1260}
1261
1262void ReadRoomsMessages(BFile& f)
1263{
1264 static int roomNumber = 0;
1265
1266 off_t fileSize;
1267 f.GetSize(&fileSize);
1268 while (fileSize > 0) {
1269 uint16_t roomSize;
1270 f.Read(&roomSize, 2);
1271 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
1272 fileSize -= roomSize;
1273
1274 if (roomSize == 0) {
1275 fileSize -= 2;
1276 break;
1277 }
1278
1279 roomSize -= 2;
1280
1281 if (gRooms.size() < roomNumber + 1)
1282 gRooms.resize(roomNumber + 1);
1283
1284 int i = 0;
1285 while (roomSize > 0) {
1286 uint16_t scriptSize;
1287
1288 ssize_t r = f.Read(&scriptSize, 2);
1289 if (r != 2) {
1290 fprintf(stderr, "ERROR %d (%s)\n", r, strerror(errno));
1291 return;
1292 }
1293 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
1294 if (scriptSize == 0) {
1295 roomSize -= 2;
1296 break;
1297 }
1298
1299 if (gRooms[roomNumber].fMessages.size() < i + 1)
1300 gRooms[roomNumber].fMessages.resize(i + 1);
1301 gRooms[roomNumber].fMessages[i].resize(scriptSize - 2);
1302 f.Read(gRooms[roomNumber].fMessages[i].data(), scriptSize - 2);
1303
1304 roomSize -= scriptSize;
1305 i++;
1306 }
1307 roomNumber++;
1308 }
1309}
1310
1311int main(int argc, char** argv)
1312{
1313 if (argc != 2) {
1314 // TODO open a filepanel instead to load a game
1315 // TODO or just look for the TITRES.DAT file
1316 usage();
1317 return 1;
1318 }
1319
1320 BDirectory gameDir(argv[1]);
1321
1322 if (gameDir.InitCheck() != B_OK) {
1323 fprintf(stderr, "Game directory %s not found.\n", argv[1]);
1324 usage();
1325 return 1;
1326 }
1327
1328 BEntry entry;
1329 BFile initFile;
1330 BFile msgRoomsFile;
1331 BFile msgGeneralFile;
1332 BFile condRoomsFile;
1333 BFile condGenFile;
1334 BFile condUltiFile;
1335
1336 if (gameDir.FindEntry("INIT.DAT", &entry) == B_OK) {
1337 initFile.SetTo(&entry, B_READ_ONLY);
1338 } else {
1339 fprintf(stderr, "Game does not contain an INIT.DAT file.\n");
1340 return 2;
1341 }
1342
1343 if (gameDir.FindEntry("MSG.DAT", &entry) == B_OK) {
1344 msgRoomsFile.SetTo(&entry, B_READ_ONLY);
1345 } else {
1346 fprintf(stderr, "Game does not contain an MSG.DAT file.\n");
1347 return 2;
1348 }
1349
1350 if (gameDir.FindEntry("MSGGEN.DAT", &entry) == B_OK) {
1351 msgGeneralFile.SetTo(&entry, B_READ_ONLY);
1352 } else {
1353 fprintf(stderr, "Game does not contain an MSGGEN.DAT file.\n");
1354 return 2;
1355 }
1356
1357 if (gameDir.FindEntry("CONDSAL.DAT", &entry) == B_OK) {
1358 condRoomsFile.SetTo(&entry, B_READ_ONLY);
1359 } else {
1360 fprintf(stderr, "Game does not contain a CONDSAL.DAT file.\n");
1361 return 2;
1362 }
1363
1364 if (gameDir.FindEntry("CONDGALE.DAT", &entry) == B_OK) {
1365 condGenFile.SetTo(&entry, B_READ_ONLY);
1366 } else {
1367 fprintf(stderr, "Game does not contain a CONDGALE.DAT file.\n");
1368 return 2;
1369 }
1370
1371 if (gameDir.FindEntry("CONDULTI.DAT", &entry) == B_OK) {
1372 condUltiFile.SetTo(&entry, B_READ_ONLY);
1373 } else {
1374 fprintf(stderr, "Game does not contain a CONDULTI.DAT file.\n");
1375 return 2;
1376 }
1377
1378 ReadInit(initFile);
1379 ReadRoomsScripts(condGenFile);
1380 ReadRoomsScripts(condRoomsFile);
1381 ReadRoomsScripts(condUltiFile);
1382
1383 ReadRoomsMessages(msgGeneralFile);
1384 ReadRoomsMessages(msgRoomsFile);
1385
1386 // TODO how many variables are there?
1387 gVar8.resize(16);
1388 gVar16.resize(16);
1389
1390 Readingame app;
1391 app.Run();
1392}
Note: See TracBrowser for help on using the repository browser.