source: Readingame/main.cpp@ 7778ea6

main
Last change on this file since 7778ea6 was 7778ea6, checked in by PulkoMandy <pulkomandy@…>, 8 weeks ago

Recursive pretty-printing of hyperlinks

Allows use of @ and other escape sequences in hyperlinks.

  • 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 decoded += '\n';
580
581 text_run_array runs;
582 runs.count = 1;
583 runs.runs->font = be_fixed_font;
584 runs.runs->offset = 0;
585 runs.runs->color = make_color(0, 0, 0);
586 fMainText->Insert(decoded, &runs);
587 fMainText->Invalidate();
588 Unlock();
589 }
590
591 void PrettyPrint(const std::string& message, BString& decoded)
592 {
593 size_t cursor = 0;
594 while (cursor < message.size()) {
595 switch(message[cursor]) {
596 case '\\':
597 cursor += HandleEscape(message, cursor + 1);
598 break;
599 case '%':
600 cursor += HandlePercentEscape(message, cursor + 1, decoded);
601 break;
602 case '[':
603 {
604 text_run_array runs;
605 runs.count = 1;
606 runs.runs->font = be_fixed_font;
607 runs.runs->offset = 0;
608 runs.runs->color = make_color(0, 0, 0);
609 fMainText->Insert(decoded, &runs);
610 decoded = "";
611 cursor += HandleHyperlink(message, cursor);
612 break;
613 }
614 case '{':
615 {
616 text_run_array runs;
617 runs.count = 1;
618 runs.runs->font = be_plain_font;
619 runs.runs->offset = 0;
620 runs.runs->color = make_color(0, 0, 0);
621 fMainText->Insert(decoded, &runs);
622 decoded = "";
623 cursor += HandleConditional(message, cursor);
624 break;
625 }
626 case '@':
627 {
628 // @ Gender mark (print an e if the player identifies as female)
629 if (gGender)
630 decoded += 'e';
631 cursor++;
632 break;
633 }
634 case '$':
635 // $ Plural mark (print an s if the counter is plural)
636 // TODO not implemented yet
637 printf(COLOR "%c\n" NORMAL, message[cursor]);
638 cursor++;
639 break;
640 case '_':
641 // Used as a non-breakable space
642 decoded += "\u00A0";
643 cursor++;
644 break;
645 default:
646 if (message[cursor] >= 0)
647 decoded += message[cursor];
648 else
649 decoded += cp850_lookup[(message[cursor] - 0x80) & 0xFF];
650 cursor++;
651 break;
652 }
653 }
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 linkText += message[cursor + i];
766 i++;
767 }
768
769 BString decoded;
770 PrettyPrint(linkText.String(), decoded);
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(decoded, 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 decoded += '\n';
788 return 3;
789 } else if (message[cursor] == 'p') {
790 decoded += gFirstName;
791 return 2;
792 } else if (message[cursor] == 'n') {
793 decoded += gLastName;
794 return 2;
795 } else {
796 // TODO unknown escape sequence
797 fprintf(stderr, "ERROR: unknown percent escape sequence %c\n", message[cursor]);
798 return 2;
799 }
800 }
801
802 int HandleEscape(const std::string& message, int cursor)
803 {
804 if (message[cursor] == 'o') {
805 fInlineButton->SetLabel("OK");
806 SetButtonVisible(true);
807 int v = 0;
808 int i = 1;
809 while (isdigit(message[cursor + i])) {
810 v = v * 10 + message[cursor + i] - '0';
811 i++;
812 }
813
814 // If there is no specified value, the default is 0x64
815 if (i == 1)
816 v = 0x64;
817
818 fInlineButton->SetMessage(new BMessage(v));
819 return i + 1;
820 } else if (message[cursor] == '!') {
821 printf(COLOR "APPEND" NORMAL "\n");
822 return 2;
823 } else {
824 // TODO unknown escape sequence
825 fprintf(stderr, "ERROR: unknown escape sequence %c\n", message[cursor]);
826 return 2;
827 }
828 }
829
830 private:
831 HyperTextView* fMainText;
832 BButton* fInlineButton;
833 BGroupLayout* fLayout;
834 BGridLayout* fGrid;
835};
836
837class Readingame: public BApplication
838{
839 public:
840 Readingame()
841 : BApplication("application/x-vnd.PulkoMandy.Readingame")
842 {
843 }
844
845 void ReadyToRun() override
846 {
847 BWindow* identity = new IdentityWindow();
848 identity->Show();
849 identity->CenterOnScreen();
850
851 // TODO check if we have a game loaded
852 fMainWindow = new GameWindow();
853
854 // FIXME add the global buttons to the main window
855
856 fDebugWindow = new DebugWindow();
857
858
859 // TODO: only if asked from the command line
860 fDebugWindow->Show();
861
862 // Initial update so we can get the proper highlights for changed variables after running
863 // the init
864 fDebugWindow->Update();
865
866 }
867
868 void MessageReceived(BMessage* message) override
869 {
870 if (message->what == 'STRT') {
871 fMainWindow->Show();
872 fMainWindow->CenterOnScreen();
873
874 bool runNextLine = true;
875 int line = 0;
876 while (runNextLine && (line < gRooms[0].fScripts.size())) {
877 gSavePC = 0;
878 runNextLine = RunScriptLine(gRooms[0].fScripts[line]);
879 gLastRunScript = line;
880 line++;
881 }
882 fMainWindow->SetMainText(gRooms[gCurrentRoom].fMessages[gCurrentScreen - 1]);
883 fDebugWindow->Update();
884 } else if (message->what < 256) {
885 gTrigger = message->what;
886
887 bool runNextLine = true;
888
889 if (gSavePC != 0) {
890 goto resume;
891 }
892
893 gLine = 0;
894 gCurrentScreen = gHomeScreen;
895 while (runNextLine && (gLine < gRooms[0].fScripts.size())) {
896 gSavePC = 0;
897 runNextLine = RunScriptLine(gRooms[0].fScripts[gLine]);
898 gLastRunScript = gLine;
899 gLine++;
900 }
901 gLine = 0;
902 while (runNextLine && (gLine < gRooms[gCurrentRoom].fScripts.size())) {
903 gSavePC = 0;
904resume:
905 runNextLine = RunScriptLine(gRooms[gCurrentRoom].fScripts[gLine]);
906 gLastRunScript = gLine | (gCurrentRoom << 16);
907 if (gSavePC == 0)
908 gLine++;
909 }
910
911 if (gGameStatus == 0 && gSavePC == 0) {
912 fMainWindow->SetMainText(gRooms[gCurrentRoom].fMessages[gCurrentScreen - 1]);
913 gLine = 0;
914 }
915
916 if (gGameStatus != 0) {
917 // TODO go back to main menu, when we have one
918 fMainWindow->Lock();
919 fMainWindow->SetButtonVisible(false);
920 fMainWindow->Unlock();
921 }
922
923 fDebugWindow->Update();
924 } else {
925 BApplication::MessageReceived(message);
926 }
927 }
928
929 bool RunScriptLine(const std::vector<uint8_t>& script)
930 {
931 int pc = gSavePC;
932 gSavePC = 0;
933 bool done = false;
934 bool runNextLine = false;
935 bool conditionIsInverted = false;
936
937 while (!done) {
938 switch(script[pc++]) {
939 case 0x00:
940 {
941 done = true;
942 break;
943 }
944 case 0x01:
945 {
946 uint8_t varAddress = script[pc++];
947 if (conditionIsInverted ^ (gVar8[varAddress] != 0)) {
948 done = true;
949 runNextLine = true;
950 }
951 conditionIsInverted = false;
952 break;
953 }
954 case 0x03:
955 {
956 uint8_t varAddress = script[pc++] - 1;
957 if (conditionIsInverted ^ (gObjects[varAddress].inInventory == 0)) {
958 done = true;
959 runNextLine = true;
960 }
961 conditionIsInverted = false;
962 break;
963 }
964 case 0x09:
965 {
966 uint8_t varAddress = script[pc++];
967 uint16_t varValue = script[pc++];
968 varValue = varValue | (script[pc++] << 8);
969 if (conditionIsInverted ^ (gVar16[varAddress] != varValue)) {
970 done = true;
971 runNextLine = true;
972 }
973 conditionIsInverted = false;
974 break;
975 }
976 case 0x0C:
977 {
978 uint8_t maxValue = script[pc++];
979 uint8_t varAddress = script[pc++];
980 gVar8[varAddress] = random() % maxValue;
981 break;
982 }
983 case 0x0D:
984 {
985 uint16_t eventToCheck = script[pc++];
986 eventToCheck |= script[pc++] << 8;
987 if (conditionIsInverted ^ (gTrigger != eventToCheck)) {
988 done = true;
989 runNextLine = true;
990 }
991 conditionIsInverted = false;
992 break;
993 }
994 case 0x0E:
995 {
996 uint8_t eventToCheck = script[pc++];
997 // FIXME is it really gTrigger here, or should it be something else?
998 if (gGlobalTrigger == eventToCheck) {
999 gGlobalTrigger++;
1000 } else {
1001 done = true;
1002 runNextLine = true;
1003 }
1004 break;
1005 }
1006 case 0x7E:
1007 {
1008 // FIXME figure this out
1009 break;
1010 }
1011
1012 case 0x7F:
1013 {
1014 conditionIsInverted = true;
1015 break;
1016 }
1017 case 0x81:
1018 {
1019 uint8_t varAddress = script[pc++];
1020 gVar8[varAddress] = !gVar8[varAddress];
1021 break;
1022 }
1023 case 0x83:
1024 {
1025 uint8_t varAddress = script[pc++];
1026 gObjects[varAddress - 1].inInventory = true;
1027 break;
1028 }
1029 case 0x87:
1030 {
1031 uint8_t varAddress = script[pc++];
1032 uint16_t varValue = script[pc++];
1033 varValue = varValue | (script[pc++] << 8);
1034 gVar16[varAddress] = varValue;
1035 break;
1036 }
1037 case 0x8A:
1038 {
1039 uint8_t varAddress = script[pc++];
1040 uint16_t varValue = script[pc++];
1041 varValue = varValue | (script[pc++] << 8);
1042 gVar16[varAddress] -= varValue;
1043 break;
1044 }
1045 case 0x8F:
1046 {
1047 gCurrentRoom = script[pc++];
1048 break;
1049 }
1050 case 0x90:
1051 {
1052 gCurrentScreen = script[pc++];
1053 break;
1054 }
1055 case 0x91:
1056 {
1057 fMainWindow->SetMainText(gRooms[0].fMessages[script[pc++]]);
1058 break;
1059 }
1060 case 0x93:
1061 {
1062 gHomeScreen = script[pc++];
1063 gCurrentScreen = gHomeScreen;
1064 break;
1065 }
1066 case 0x95:
1067 {
1068 uint8_t varAddress = script[pc++];
1069 uint8_t varValue = script[pc++];
1070 gVar8[varAddress] = varValue;
1071 break;
1072 }
1073 case 0x97:
1074 {
1075 gSavePC = pc;
1076 done = true;
1077 break;
1078 }
1079 case 0xA0:
1080 {
1081 // FIXME: the global hooks are always evaluated anyways?
1082 pc += 3;
1083 break;
1084 }
1085 case 0xAE:
1086 {
1087 printf("TODO: unhandled opcode AE\n");
1088 break;
1089 }
1090 case 0xFC:
1091 {
1092 printf("Game over\n");
1093 gGameStatus = 1;
1094 done = true;
1095 break;
1096 }
1097 case 0xFF:
1098 {
1099 printf("You won!\n");
1100 gGameStatus = 2;
1101 done = true;
1102 break;
1103 }
1104 default:
1105 fprintf(stderr, "ERROR: unknown opcode %02x at %04x (screen %d)\n", script[pc - 1], pc - 1, gHomeScreen);
1106 for (int k = 0; k < script.size(); k++) {
1107 if (k == pc - 1)
1108 printf("*");
1109 printf("%02x ", script[k]);
1110 }
1111 puts("");
1112 done = true;
1113 break;
1114 }
1115
1116 if (done && (script[pc] == 0x7E)) {
1117 done = false;
1118 }
1119 }
1120
1121 return runNextLine;
1122 }
1123
1124private:
1125 GameWindow* fMainWindow;
1126 DebugWindow* fDebugWindow;
1127};
1128
1129void usage()
1130{
1131 fprintf(stderr,
1132 "An engine for playing game originally designed for the Lectures Enjeu application\n\n"
1133 "Usage: readingame path/to/gamedir/\n"
1134 );
1135}
1136
1137void ReadString(BFile& f, BString& output)
1138{
1139 char buffer[256];
1140 int off = 0;
1141
1142 do {
1143 f.Read(buffer + off, 1);
1144 off++;
1145 } while (buffer[off - 1] != 0);
1146
1147 output = buffer;
1148}
1149
1150void ReadInit(BFile& f)
1151{
1152 uint16_t header[40];
1153 f.Read(&header, 80);
1154
1155 for (int i = 0; i < 40; i++)
1156 {
1157 header[i] = B_LENDIAN_TO_HOST_INT16(header[i]);
1158 //printf("%04x ", header[i]);
1159 }
1160 //puts("");
1161
1162 off_t fileSize;
1163 f.GetSize(&fileSize);
1164 while (fileSize > 0) {
1165 uint16_t chunkId;
1166 f.Read(&chunkId, 2);
1167 chunkId = B_LENDIAN_TO_HOST_INT16(chunkId);
1168
1169 fileSize -= 2;
1170
1171 switch (chunkId) {
1172 case 1:
1173 {
1174 // widgets
1175 ButtonDescriptor button;
1176
1177 for(;;) {
1178 f.Read(&button.x, 2);
1179 f.Read(&button.y, 2);
1180
1181 if ((button.x == 0) && (button.y == 0))
1182 break;
1183
1184 f.Read(&button.color, 2);
1185 f.Read(&button.reserved, 6);
1186 f.Read(&button.trigger, 2);
1187
1188 char buffer[256];
1189 int off = 0;
1190
1191 ReadString(f, button.label);
1192 ReadString(f, button.shortcut);
1193
1194 button.x = B_LENDIAN_TO_HOST_INT16(button.x);
1195 button.y = B_LENDIAN_TO_HOST_INT16(button.y);
1196 button.trigger = B_LENDIAN_TO_HOST_INT16(button.trigger);
1197
1198 printf("Button: x %d y %d c %d t %04x %s %s (%04x %04x %04x)\n", button.x, button.y,
1199 button.color, button.trigger, button.label.String(), button.shortcut.String(),
1200 button.reserved[0], button.reserved[1], button.reserved[2]);
1201 gButtons.push_back(button);
1202 }
1203 break;
1204 }
1205 case 2:
1206 {
1207 // actions
1208 struct Action {
1209 uint8_t id;
1210 BString label;
1211 BString shortcut;
1212 } action;
1213
1214 for(;;) {
1215 f.Read(&action.id, 1);
1216 if (action.id == 0)
1217 break;
1218 ReadString(f, action.label);
1219 ReadString(f, action.shortcut);
1220 printf("Action: %02x %s %s\n", action.id, action.shortcut.String(), action.label.String());
1221 }
1222
1223 break;
1224 }
1225 case 3:
1226 {
1227 uint16_t header;
1228 f.Read(&header, 2);
1229 header = B_LENDIAN_TO_HOST_INT16(header);
1230 printf("Object header: %d\n", header);
1231
1232 Object object;
1233
1234 for (;;) {
1235 f.Read(&object.reserved[0], 1);
1236 if (object.reserved[0] == 0)
1237 break;
1238 f.Read(&object.reserved[1], 7);
1239 ReadString(f, object.name);
1240 ReadString(f, object.description);
1241 f.Read(&object.roomActions, 3);
1242 f.Read(&object.inventoryActions, 3);
1243 printf("Object: %s (%s) - Room actions %x %x %x / Inventory %x %x %x\n ",
1244 object.name.String(), object.description.String(),
1245 object.roomActions[0], object.roomActions[1], object.roomActions[2],
1246 object.inventoryActions[0], object.inventoryActions[1], object.inventoryActions[2]);
1247 for (int j = 0; j < 8; j++)
1248 printf("%02x ", object.reserved[j]);
1249 printf("\n");
1250
1251 gObjects.push_back(object);
1252 }
1253 // objects
1254 break;
1255 }
1256 case 4:
1257 {
1258 // descriptions
1259 break;
1260 }
1261 case 5:
1262 {
1263 // action results
1264 break;
1265 }
1266 case 6:
1267 {
1268 // compass
1269 break;
1270 }
1271 case 7:
1272 {
1273 // TODO
1274 break;
1275 }
1276 case 8:
1277 {
1278 // TODO
1279 break;
1280 }
1281 case 9:
1282 {
1283 // TODO
1284 break;
1285 }
1286 }
1287
1288 // TODO remove when we parse more chunks
1289 if (chunkId == 3)
1290 break;
1291 }
1292}
1293
1294void ReadRoomsScripts(BFile& f)
1295{
1296 static int roomNumber = 0;
1297
1298 off_t fileSize;
1299 f.GetSize(&fileSize);
1300 while (fileSize > 0) {
1301 uint16_t roomSize;
1302 f.Read(&roomSize, 2);
1303 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
1304 fileSize -= roomSize;
1305
1306 if (roomSize == 0) {
1307 fileSize -= 2;
1308 break;
1309 }
1310
1311 if (gRooms.size() < roomNumber + 1)
1312 gRooms.resize(roomNumber + 1);
1313
1314 int i = 0;
1315 while (roomSize > 0) {
1316 uint16_t scriptSize;
1317
1318 if (f.Read(&scriptSize, 2) != 2) {
1319 fprintf(stderr, "ERROR\n");
1320 return;
1321 }
1322
1323 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
1324 if (scriptSize == 0) {
1325 roomSize -= 2;
1326 break;
1327 }
1328
1329 if (gRooms[roomNumber].fScripts.size() < i + 1)
1330 gRooms[roomNumber].fScripts.resize(i + 1);
1331 gRooms[roomNumber].fScripts[i].resize(scriptSize - 2);
1332 f.Read(gRooms[roomNumber].fScripts[i].data(), scriptSize - 2);
1333
1334 roomSize -= scriptSize;
1335 i++;
1336 }
1337 roomNumber++;
1338 }
1339}
1340
1341void ReadRoomsMessages(BFile& f)
1342{
1343 static int roomNumber = 0;
1344
1345 off_t fileSize;
1346 f.GetSize(&fileSize);
1347 while (fileSize > 0) {
1348 uint16_t roomSize;
1349 f.Read(&roomSize, 2);
1350 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
1351 fileSize -= roomSize;
1352
1353 if (roomSize == 0) {
1354 fileSize -= 2;
1355 break;
1356 }
1357
1358 roomSize -= 2;
1359
1360 if (gRooms.size() < roomNumber + 1)
1361 gRooms.resize(roomNumber + 1);
1362
1363 int i = 0;
1364 while (roomSize > 0) {
1365 uint16_t scriptSize;
1366
1367 ssize_t r = f.Read(&scriptSize, 2);
1368 if (r != 2) {
1369 fprintf(stderr, "ERROR %d (%s)\n", r, strerror(errno));
1370 return;
1371 }
1372 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
1373 if (scriptSize == 0) {
1374 roomSize -= 2;
1375 break;
1376 }
1377
1378 if (gRooms[roomNumber].fMessages.size() < i + 1)
1379 gRooms[roomNumber].fMessages.resize(i + 1);
1380 gRooms[roomNumber].fMessages[i].resize(scriptSize - 2);
1381 f.Read(gRooms[roomNumber].fMessages[i].data(), scriptSize - 2);
1382
1383 roomSize -= scriptSize;
1384 i++;
1385 }
1386 roomNumber++;
1387 }
1388}
1389
1390int main(int argc, char** argv)
1391{
1392 if (argc != 2) {
1393 // TODO open a filepanel instead to load a game
1394 // TODO or just look for the TITRES.DAT file
1395 usage();
1396 return 1;
1397 }
1398
1399 BDirectory gameDir(argv[1]);
1400
1401 if (gameDir.InitCheck() != B_OK) {
1402 fprintf(stderr, "Game directory %s not found.\n", argv[1]);
1403 usage();
1404 return 1;
1405 }
1406
1407 BEntry entry;
1408 BFile initFile;
1409 BFile msgRoomsFile;
1410 BFile msgGeneralFile;
1411 BFile condRoomsFile;
1412 BFile condGenFile;
1413 BFile condUltiFile;
1414
1415 if (gameDir.FindEntry("INIT.DAT", &entry) == B_OK) {
1416 initFile.SetTo(&entry, B_READ_ONLY);
1417 } else {
1418 fprintf(stderr, "Game does not contain an INIT.DAT file.\n");
1419 return 2;
1420 }
1421
1422 if (gameDir.FindEntry("MSG.DAT", &entry) == B_OK) {
1423 msgRoomsFile.SetTo(&entry, B_READ_ONLY);
1424 } else {
1425 fprintf(stderr, "Game does not contain an MSG.DAT file.\n");
1426 return 2;
1427 }
1428
1429 if (gameDir.FindEntry("MSGGEN.DAT", &entry) == B_OK) {
1430 msgGeneralFile.SetTo(&entry, B_READ_ONLY);
1431 } else {
1432 fprintf(stderr, "Game does not contain an MSGGEN.DAT file.\n");
1433 return 2;
1434 }
1435
1436 if (gameDir.FindEntry("CONDSAL.DAT", &entry) == B_OK) {
1437 condRoomsFile.SetTo(&entry, B_READ_ONLY);
1438 } else {
1439 fprintf(stderr, "Game does not contain a CONDSAL.DAT file.\n");
1440 return 2;
1441 }
1442
1443 if (gameDir.FindEntry("CONDGALE.DAT", &entry) == B_OK) {
1444 condGenFile.SetTo(&entry, B_READ_ONLY);
1445 } else {
1446 fprintf(stderr, "Game does not contain a CONDGALE.DAT file.\n");
1447 return 2;
1448 }
1449
1450 if (gameDir.FindEntry("CONDULTI.DAT", &entry) == B_OK) {
1451 condUltiFile.SetTo(&entry, B_READ_ONLY);
1452 } else {
1453 fprintf(stderr, "Game does not contain a CONDULTI.DAT file.\n");
1454 return 2;
1455 }
1456
1457 ReadInit(initFile);
1458 ReadRoomsScripts(condGenFile);
1459 ReadRoomsScripts(condRoomsFile);
1460 ReadRoomsScripts(condUltiFile);
1461
1462 ReadRoomsMessages(msgGeneralFile);
1463 ReadRoomsMessages(msgRoomsFile);
1464
1465 // TODO how many variables are there?
1466 gVar8.resize(16);
1467 gVar16.resize(16);
1468
1469 Readingame app;
1470 app.Run();
1471}
Note: See TracBrowser for help on using the repository browser.