source: Readingame/main.cpp@ 8704702

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

Show inventory in debugger window (with object names)

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