source: Readingame/main.cpp@ d6dc8dd

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

Skip over AE opcode for now

Not sure what this does?

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