source: Readingame/main.cpp@ 50676d6

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

Make Le Lyon de la Révolution fully playable

  • Property mode set to 100644
File size: 33.5 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("IsSet(%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("Clear(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 0x82:
1024 {
1025 uint8_t varAddress = script[pc++];
1026 gVar8[varAddress] = 0;
1027 break;
1028 }
1029 case 0x83:
1030 {
1031 uint8_t varAddress = script[pc++];
1032 gObjects[varAddress - 1].inInventory = true;
1033 break;
1034 }
1035 case 0x87:
1036 {
1037 uint8_t varAddress = script[pc++];
1038 uint16_t varValue = script[pc++];
1039 varValue = varValue | (script[pc++] << 8);
1040 gVar16[varAddress] = varValue;
1041 break;
1042 }
1043 case 0x8A:
1044 {
1045 uint8_t varAddress = script[pc++];
1046 uint16_t varValue = script[pc++];
1047 varValue = varValue | (script[pc++] << 8);
1048 gVar16[varAddress] -= varValue;
1049 break;
1050 }
1051 case 0x8F:
1052 {
1053 gCurrentRoom = script[pc++];
1054 break;
1055 }
1056 case 0x90:
1057 {
1058 gCurrentScreen = script[pc++];
1059 break;
1060 }
1061 case 0x91:
1062 {
1063 fMainWindow->SetMainText(gRooms[0].fMessages[script[pc++]]);
1064 break;
1065 }
1066 case 0x93:
1067 {
1068 gHomeScreen = script[pc++];
1069 gCurrentScreen = gHomeScreen;
1070 break;
1071 }
1072 case 0x95:
1073 {
1074 uint8_t varAddress = script[pc++];
1075 uint8_t varValue = script[pc++];
1076 gVar8[varAddress] = varValue;
1077 break;
1078 }
1079 case 0x97:
1080 {
1081 gSavePC = pc;
1082 done = true;
1083 break;
1084 }
1085 case 0xA0:
1086 {
1087 // FIXME: the global hooks are always evaluated anyways?
1088 pc += 3;
1089 break;
1090 }
1091 case 0xAE:
1092 {
1093 printf("TODO: unhandled opcode AE\n");
1094 break;
1095 }
1096 case 0xFC:
1097 {
1098 printf("Game over\n");
1099 gGameStatus = 1;
1100 done = true;
1101 break;
1102 }
1103 case 0xFF:
1104 {
1105 printf("You won!\n");
1106 gGameStatus = 2;
1107 done = true;
1108 break;
1109 }
1110 default:
1111 fprintf(stderr, "ERROR: unknown opcode %02x at %04x (screen %d)\n", script[pc - 1], pc - 1, gHomeScreen);
1112 for (int k = 0; k < script.size(); k++) {
1113 if (k == pc - 1)
1114 printf("*");
1115 printf("%02x ", script[k]);
1116 }
1117 puts("");
1118 done = true;
1119 break;
1120 }
1121
1122 if (done && (script[pc] == 0x7E)) {
1123 done = false;
1124 }
1125 }
1126
1127 return runNextLine;
1128 }
1129
1130private:
1131 GameWindow* fMainWindow;
1132 DebugWindow* fDebugWindow;
1133};
1134
1135void usage()
1136{
1137 fprintf(stderr,
1138 "An engine for playing game originally designed for the Lectures Enjeu application\n\n"
1139 "Usage: readingame path/to/gamedir/\n"
1140 );
1141}
1142
1143void ReadString(BFile& f, BString& output)
1144{
1145 char buffer[256];
1146 int off = 0;
1147
1148 do {
1149 f.Read(buffer + off, 1);
1150 off++;
1151 } while (buffer[off - 1] != 0);
1152
1153 output = buffer;
1154}
1155
1156void ReadInit(BFile& f)
1157{
1158 uint16_t header[40];
1159 f.Read(&header, 80);
1160
1161 for (int i = 0; i < 40; i++)
1162 {
1163 header[i] = B_LENDIAN_TO_HOST_INT16(header[i]);
1164 //printf("%04x ", header[i]);
1165 }
1166 //puts("");
1167
1168 off_t fileSize;
1169 f.GetSize(&fileSize);
1170 while (fileSize > 0) {
1171 uint16_t chunkId;
1172 f.Read(&chunkId, 2);
1173 chunkId = B_LENDIAN_TO_HOST_INT16(chunkId);
1174
1175 fileSize -= 2;
1176
1177 switch (chunkId) {
1178 case 1:
1179 {
1180 // widgets
1181 ButtonDescriptor button;
1182
1183 for(;;) {
1184 f.Read(&button.x, 2);
1185 f.Read(&button.y, 2);
1186
1187 if ((button.x == 0) && (button.y == 0))
1188 break;
1189
1190 f.Read(&button.color, 2);
1191 f.Read(&button.reserved, 6);
1192 f.Read(&button.trigger, 2);
1193
1194 char buffer[256];
1195 int off = 0;
1196
1197 ReadString(f, button.label);
1198 ReadString(f, button.shortcut);
1199
1200 button.x = B_LENDIAN_TO_HOST_INT16(button.x);
1201 button.y = B_LENDIAN_TO_HOST_INT16(button.y);
1202 button.trigger = B_LENDIAN_TO_HOST_INT16(button.trigger);
1203
1204 printf("Button: x %d y %d c %d t %04x %s %s (%04x %04x %04x)\n", button.x, button.y,
1205 button.color, button.trigger, button.label.String(), button.shortcut.String(),
1206 button.reserved[0], button.reserved[1], button.reserved[2]);
1207 gButtons.push_back(button);
1208 }
1209 break;
1210 }
1211 case 2:
1212 {
1213 // actions
1214 struct Action {
1215 uint8_t id;
1216 BString label;
1217 BString shortcut;
1218 } action;
1219
1220 for(;;) {
1221 f.Read(&action.id, 1);
1222 if (action.id == 0)
1223 break;
1224 ReadString(f, action.label);
1225 ReadString(f, action.shortcut);
1226 printf("Action: %02x %s %s\n", action.id, action.shortcut.String(), action.label.String());
1227 }
1228
1229 break;
1230 }
1231 case 3:
1232 {
1233 uint16_t header;
1234 f.Read(&header, 2);
1235 header = B_LENDIAN_TO_HOST_INT16(header);
1236 printf("Object header: %d\n", header);
1237
1238 Object object;
1239
1240 for (;;) {
1241 f.Read(&object.reserved[0], 1);
1242 if (object.reserved[0] == 0)
1243 break;
1244 f.Read(&object.reserved[1], 7);
1245 ReadString(f, object.name);
1246 ReadString(f, object.description);
1247 f.Read(&object.roomActions, 3);
1248 f.Read(&object.inventoryActions, 3);
1249 printf("Object: %s (%s) - Room actions %x %x %x / Inventory %x %x %x\n ",
1250 object.name.String(), object.description.String(),
1251 object.roomActions[0], object.roomActions[1], object.roomActions[2],
1252 object.inventoryActions[0], object.inventoryActions[1], object.inventoryActions[2]);
1253 for (int j = 0; j < 8; j++)
1254 printf("%02x ", object.reserved[j]);
1255 printf("\n");
1256
1257 gObjects.push_back(object);
1258 }
1259 // objects
1260 break;
1261 }
1262 case 4:
1263 {
1264 // descriptions
1265 break;
1266 }
1267 case 5:
1268 {
1269 // action results
1270 break;
1271 }
1272 case 6:
1273 {
1274 // compass
1275 break;
1276 }
1277 case 7:
1278 {
1279 // TODO
1280 break;
1281 }
1282 case 8:
1283 {
1284 // TODO
1285 break;
1286 }
1287 case 9:
1288 {
1289 // TODO
1290 break;
1291 }
1292 }
1293
1294 // TODO remove when we parse more chunks
1295 if (chunkId == 3)
1296 break;
1297 }
1298}
1299
1300void ReadRoomsScripts(BFile& f)
1301{
1302 static int roomNumber = 0;
1303
1304 off_t fileSize;
1305 f.GetSize(&fileSize);
1306 while (fileSize > 0) {
1307 uint16_t roomSize;
1308 f.Read(&roomSize, 2);
1309 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
1310 fileSize -= roomSize;
1311
1312 if (roomSize == 0) {
1313 fileSize -= 2;
1314 break;
1315 }
1316
1317 if (gRooms.size() < roomNumber + 1)
1318 gRooms.resize(roomNumber + 1);
1319
1320 int i = 0;
1321 while (roomSize > 0) {
1322 uint16_t scriptSize;
1323
1324 if (f.Read(&scriptSize, 2) != 2) {
1325 fprintf(stderr, "ERROR\n");
1326 return;
1327 }
1328
1329 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
1330 if (scriptSize == 0) {
1331 roomSize -= 2;
1332 break;
1333 }
1334
1335 if (gRooms[roomNumber].fScripts.size() < i + 1)
1336 gRooms[roomNumber].fScripts.resize(i + 1);
1337 gRooms[roomNumber].fScripts[i].resize(scriptSize - 2);
1338 f.Read(gRooms[roomNumber].fScripts[i].data(), scriptSize - 2);
1339
1340 roomSize -= scriptSize;
1341 i++;
1342 }
1343 roomNumber++;
1344 }
1345}
1346
1347void ReadRoomsMessages(BFile& f)
1348{
1349 static int roomNumber = 0;
1350
1351 off_t fileSize;
1352 f.GetSize(&fileSize);
1353 while (fileSize > 0) {
1354 uint16_t roomSize;
1355 f.Read(&roomSize, 2);
1356 roomSize = B_LENDIAN_TO_HOST_INT16(roomSize);
1357 fileSize -= roomSize;
1358
1359 if (roomSize == 0) {
1360 fileSize -= 2;
1361 break;
1362 }
1363
1364 roomSize -= 2;
1365
1366 if (gRooms.size() < roomNumber + 1)
1367 gRooms.resize(roomNumber + 1);
1368
1369 int i = 0;
1370 while (roomSize > 0) {
1371 uint16_t scriptSize;
1372
1373 ssize_t r = f.Read(&scriptSize, 2);
1374 if (r != 2) {
1375 fprintf(stderr, "ERROR %d (%s)\n", r, strerror(errno));
1376 return;
1377 }
1378 scriptSize = B_LENDIAN_TO_HOST_INT16(scriptSize);
1379 if (scriptSize == 0) {
1380 roomSize -= 2;
1381 break;
1382 }
1383
1384 if (gRooms[roomNumber].fMessages.size() < i + 1)
1385 gRooms[roomNumber].fMessages.resize(i + 1);
1386 gRooms[roomNumber].fMessages[i].resize(scriptSize - 2);
1387 f.Read(gRooms[roomNumber].fMessages[i].data(), scriptSize - 2);
1388
1389 roomSize -= scriptSize;
1390 i++;
1391 }
1392 roomNumber++;
1393 }
1394}
1395
1396int main(int argc, char** argv)
1397{
1398 if (argc != 2) {
1399 // TODO open a filepanel instead to load a game
1400 // TODO or just look for the TITRES.DAT file
1401 usage();
1402 return 1;
1403 }
1404
1405 BDirectory gameDir(argv[1]);
1406
1407 if (gameDir.InitCheck() != B_OK) {
1408 fprintf(stderr, "Game directory %s not found.\n", argv[1]);
1409 usage();
1410 return 1;
1411 }
1412
1413 BEntry entry;
1414 BFile initFile;
1415 BFile msgRoomsFile;
1416 BFile msgGeneralFile;
1417 BFile condRoomsFile;
1418 BFile condGenFile;
1419 BFile condUltiFile;
1420
1421 if (gameDir.FindEntry("INIT.DAT", &entry) == B_OK) {
1422 initFile.SetTo(&entry, B_READ_ONLY);
1423 } else {
1424 fprintf(stderr, "Game does not contain an INIT.DAT file.\n");
1425 return 2;
1426 }
1427
1428 if (gameDir.FindEntry("MSG.DAT", &entry) == B_OK) {
1429 msgRoomsFile.SetTo(&entry, B_READ_ONLY);
1430 } else {
1431 fprintf(stderr, "Game does not contain an MSG.DAT file.\n");
1432 return 2;
1433 }
1434
1435 if (gameDir.FindEntry("MSGGEN.DAT", &entry) == B_OK) {
1436 msgGeneralFile.SetTo(&entry, B_READ_ONLY);
1437 } else {
1438 fprintf(stderr, "Game does not contain an MSGGEN.DAT file.\n");
1439 return 2;
1440 }
1441
1442 if (gameDir.FindEntry("CONDSAL.DAT", &entry) == B_OK) {
1443 condRoomsFile.SetTo(&entry, B_READ_ONLY);
1444 } else {
1445 fprintf(stderr, "Game does not contain a CONDSAL.DAT file.\n");
1446 return 2;
1447 }
1448
1449 if (gameDir.FindEntry("CONDGALE.DAT", &entry) == B_OK) {
1450 condGenFile.SetTo(&entry, B_READ_ONLY);
1451 } else {
1452 fprintf(stderr, "Game does not contain a CONDGALE.DAT file.\n");
1453 return 2;
1454 }
1455
1456 if (gameDir.FindEntry("CONDULTI.DAT", &entry) == B_OK) {
1457 condUltiFile.SetTo(&entry, B_READ_ONLY);
1458 } else {
1459 fprintf(stderr, "Game does not contain a CONDULTI.DAT file.\n");
1460 return 2;
1461 }
1462
1463 ReadInit(initFile);
1464 ReadRoomsScripts(condGenFile);
1465 ReadRoomsScripts(condRoomsFile);
1466 ReadRoomsScripts(condUltiFile);
1467
1468 ReadRoomsMessages(msgGeneralFile);
1469 ReadRoomsMessages(msgRoomsFile);
1470
1471 // TODO how many variables are there?
1472 gVar8.resize(16);
1473 gVar16.resize(16);
1474
1475 Readingame app;
1476 app.Run();
1477}
Note: See TracBrowser for help on using the repository browser.