1 /* ScummVM - Graphic Adventure Engine
2 *
3 * ScummVM is the legal property of its developers, whose names
4 * are too numerous to list here. Please refer to the COPYRIGHT
5 * file distributed with this source distribution.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23 #include "common/system.h"
24 #include "common/debug.h"
25 #include "common/error.h"
26 #include "common/file.h"
27 #include "common/stream.h"
28 #include "common/ptr.h"
29
30 #include "adl/adl.h"
31 #include "adl/graphics.h"
32 #include "adl/display_a2.h"
33
34 namespace Adl {
35
36 #define IDS_HR1_EXE_0 "AUTO LOAD OBJ"
37 #define IDS_HR1_EXE_1 "ADVENTURE"
38 #define IDS_HR1_MESSAGES "MESSAGES"
39
40 #define IDI_HR1_NUM_ROOMS 41
41 #define IDI_HR1_NUM_PICS 97
42 #define IDI_HR1_NUM_VARS 20
43 #define IDI_HR1_NUM_ITEM_OFFSETS 21
44 #define IDI_HR1_NUM_MESSAGES 168
45
46 // Messages used outside of scripts
47 #define IDI_HR1_MSG_CANT_GO_THERE 137
48 #define IDI_HR1_MSG_DONT_UNDERSTAND 37
49 #define IDI_HR1_MSG_ITEM_DOESNT_MOVE 151
50 #define IDI_HR1_MSG_ITEM_NOT_HERE 152
51 #define IDI_HR1_MSG_THANKS_FOR_PLAYING 140
52 #define IDI_HR1_MSG_DONT_HAVE_IT 127
53 #define IDI_HR1_MSG_GETTING_DARK 7
54
55 #define IDI_HR1_OFS_STR_ENTER_COMMAND 0x5bbc
56 #define IDI_HR1_OFS_STR_VERB_ERROR 0x5b4f
57 #define IDI_HR1_OFS_STR_NOUN_ERROR 0x5b8e
58 #define IDI_HR1_OFS_STR_PLAY_AGAIN 0x5f1e
59 #define IDI_HR1_OFS_STR_CANT_GO_THERE 0x6c0a
60 #define IDI_HR1_OFS_STR_DONT_HAVE_IT 0x6c31
61 #define IDI_HR1_OFS_STR_DONT_UNDERSTAND 0x6c51
62 #define IDI_HR1_OFS_STR_GETTING_DARK 0x6c7c
63 #define IDI_HR1_OFS_STR_PRESS_RETURN 0x5f68
64 #define IDI_HR1_OFS_STR_LINE_FEEDS 0x59d4
65
66 #define IDI_HR1_OFS_PD_TEXT_0 0x005d
67 #define IDI_HR1_OFS_PD_TEXT_1 0x012b
68 #define IDI_HR1_OFS_PD_TEXT_2 0x016d
69 #define IDI_HR1_OFS_PD_TEXT_3 0x0259
70
71 #define IDI_HR1_OFS_INTRO_TEXT 0x0066
72 #define IDI_HR1_OFS_GAME_OR_HELP 0x000f
73
74 #define IDI_HR1_OFS_LOGO_0 0x1003
75
76 #define IDI_HR1_OFS_ITEMS 0x0100
77 #define IDI_HR1_OFS_ROOMS 0x050a
78 #define IDI_HR1_OFS_PICS 0x4b03
79 #define IDI_HR1_OFS_CMDS_0 0x3c00
80 #define IDI_HR1_OFS_CMDS_1 0x3d00
81 #define IDI_HR1_OFS_MSGS 0x4d00
82
83 #define IDI_HR1_OFS_ITEM_OFFSETS 0x68ff
84 #define IDI_HR1_OFS_SHAPES 0x4f00
85
86 #define IDI_HR1_OFS_VERBS 0x3800
87 #define IDI_HR1_OFS_NOUNS 0x0f00
88
89 class HiRes1Engine : public AdlEngine {
90 public:
HiRes1Engine(OSystem * syst,const AdlGameDescription * gd)91 HiRes1Engine(OSystem *syst, const AdlGameDescription *gd) :
92 AdlEngine(syst, gd),
93 _files(nullptr),
94 _messageDelay(true) { }
~HiRes1Engine()95 ~HiRes1Engine() { delete _files; }
96
97 private:
98 // AdlEngine
99 void runIntro();
100 void init();
101 void initGameState();
102 void restartGame();
103 void printString(const Common::String &str);
104 Common::String loadMessage(uint idx) const;
105 void printMessage(uint idx);
106 void drawItems();
107 void drawItem(Item &item, const Common::Point &pos);
108 void loadRoom(byte roomNr);
109 void showRoom();
110
111 void showInstructions(Common::SeekableReadStream &stream, const uint pages[], bool goHome);
112 void wordWrap(Common::String &str) const;
113
114 Files *_files;
115 Common::File _exe;
116 Common::Array<DataBlockPtr> _corners;
117 Common::Array<byte> _roomDesc;
118 bool _messageDelay;
119
120 struct {
121 Common::String cantGoThere;
122 Common::String dontHaveIt;
123 Common::String dontUnderstand;
124 Common::String gettingDark;
125 } _gameStrings;
126 };
127
showInstructions(Common::SeekableReadStream & stream,const uint pages[],bool goHome)128 void HiRes1Engine::showInstructions(Common::SeekableReadStream &stream, const uint pages[], bool goHome) {
129 _display->setMode(Display::kModeText);
130
131 uint page = 0;
132 while (pages[page] != 0) {
133 if (goHome)
134 _display->home();
135
136 uint count = pages[page++];
137 for (uint i = 0; i < count; ++i) {
138 _display->printString(readString(stream));
139 stream.seek(3, SEEK_CUR);
140 }
141
142 inputString();
143
144 if (shouldQuit())
145 return;
146
147 stream.seek((goHome ? 6 : 3), SEEK_CUR);
148 }
149 }
150
runIntro()151 void HiRes1Engine::runIntro() {
152 StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_0));
153
154 // Early version have no bitmap in 'AUTO LOAD OBJ'
155 if (getGameVersion() >= GAME_VER_HR1_COARSE) {
156 stream->seek(IDI_HR1_OFS_LOGO_0);
157 _display->setMode(Display::kModeGraphics);
158 static_cast<Display_A2 *>(_display)->loadFrameBuffer(*stream);
159 _display->renderGraphics();
160
161 if (getGameVersion() == GAME_VER_HR1_PD) {
162 // Only the PD version shows a title screen during the load
163 delay(4000);
164
165 if (shouldQuit())
166 return;
167 }
168 }
169
170 Common::String str;
171
172 // Show the PD disclaimer for the PD release
173 if (getGameVersion() == GAME_VER_HR1_PD) {
174 // The PD release on the Roberta Williams Anthology disc has a PDE
175 // splash screen. The original HELLO file has been renamed to
176 // MYSTERY.HELLO. It's unclear whether or not this splash screen
177 // was present in the original PD release back in 1987.
178 StreamPtr basic(_files->createReadStream("MYSTERY.HELLO"));
179
180 _display->setMode(Display::kModeText);
181 _display->home();
182
183 str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_0, '"');
184 _display->printAsciiString(str + '\r');
185
186 str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_1, '"');
187 _display->printAsciiString(str + "\r\r");
188
189 str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_2, '"');
190 _display->printAsciiString(str + "\r\r");
191
192 str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_3, '"');
193 _display->printAsciiString(str + '\r');
194
195 inputKey();
196 if (shouldQuit())
197 return;
198 }
199
200 _display->setMode(Display::kModeMixed);
201
202 str = readStringAt(*stream, IDI_HR1_OFS_GAME_OR_HELP);
203
204 if (getGameVersion() >= GAME_VER_HR1_COARSE) {
205 bool instructions = false;
206
207 while (1) {
208 _display->printString(str);
209 Common::String s = inputString();
210
211 if (shouldQuit())
212 break;
213
214 if (s.empty())
215 continue;
216
217 if (s[0] == _display->asciiToNative('I')) {
218 instructions = true;
219 break;
220 } else if (s[0] == _display->asciiToNative('G')) {
221 break;
222 }
223 }
224
225 if (instructions) {
226 // This version shows the last page during the loading of the game
227 // We wait for a key instead (even though there's no prompt for that).
228 const uint pages[] = { 6, 6, 4, 5, 8, 7, 0 };
229 stream->seek(IDI_HR1_OFS_INTRO_TEXT);
230 showInstructions(*stream, pages, true);
231 _display->printAsciiString("\r");
232 }
233 } else {
234 const uint pages[] = { 6, 6, 8, 6, 0 };
235 stream->seek(6);
236 showInstructions(*stream, pages, false);
237 }
238
239 stream.reset(_files->createReadStream(IDS_HR1_EXE_1));
240 stream->seek(0x1800);
241 static_cast<Display_A2 *>(_display)->loadFrameBuffer(*stream);
242 _display->renderGraphics();
243
244 _display->setMode(Display::kModeMixed);
245
246 if (getGameVersion() == GAME_VER_HR1_SIMI) {
247 // The original waits for the key after initializing the state.
248 // This causes it to also wait for a key on a blank screen when
249 // a game is restarted. We only wait for a key here during the
250 // intro.
251
252 // This does mean we need to push out some extra line feeds to clear the screen
253 _display->printString(_strings.lineFeeds);
254 inputKey();
255 if (shouldQuit())
256 return;
257 }
258 }
259
init()260 void HiRes1Engine::init() {
261 if (Common::File::exists("ADVENTURE")) {
262 _files = new Files_Plain();
263 } else {
264 Files_AppleDOS *files = new Files_AppleDOS();
265 // The 2nd release obfuscates the VTOC (same may be true for the 1st release)
266 if (!files->open(getDiskImageName(0), (getGameVersion() == GAME_VER_HR1_COARSE ? 16 : 17)))
267 error("Failed to open '%s'", getDiskImageName(0).c_str());
268 _files = files;
269 }
270
271 _graphics = new GraphicsMan_v1<Display_A2>(*static_cast<Display_A2 *>(_display));
272 _display->moveCursorTo(Common::Point(0, 3));
273
274 StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1));
275
276 // Some messages have overrides inside the executable
277 _gameStrings.cantGoThere = readStringAt(*stream, IDI_HR1_OFS_STR_CANT_GO_THERE);
278 _gameStrings.dontHaveIt = readStringAt(*stream, IDI_HR1_OFS_STR_DONT_HAVE_IT);
279 _gameStrings.dontUnderstand = readStringAt(*stream, IDI_HR1_OFS_STR_DONT_UNDERSTAND);
280 _gameStrings.gettingDark = readStringAt(*stream, IDI_HR1_OFS_STR_GETTING_DARK);
281
282 // Load other strings from executable
283 _strings.enterCommand = readStringAt(*stream, IDI_HR1_OFS_STR_ENTER_COMMAND);
284 _strings.verbError = readStringAt(*stream, IDI_HR1_OFS_STR_VERB_ERROR);
285 _strings.nounError = readStringAt(*stream, IDI_HR1_OFS_STR_NOUN_ERROR);
286 _strings.playAgain = readStringAt(*stream, IDI_HR1_OFS_STR_PLAY_AGAIN);
287 _strings.pressReturn = readStringAt(*stream, IDI_HR1_OFS_STR_PRESS_RETURN);
288 _strings.lineFeeds = readStringAt(*stream, IDI_HR1_OFS_STR_LINE_FEEDS);
289
290 // Set message IDs
291 _messageIds.cantGoThere = IDI_HR1_MSG_CANT_GO_THERE;
292 _messageIds.dontUnderstand = IDI_HR1_MSG_DONT_UNDERSTAND;
293 _messageIds.itemDoesntMove = IDI_HR1_MSG_ITEM_DOESNT_MOVE;
294 _messageIds.itemNotHere = IDI_HR1_MSG_ITEM_NOT_HERE;
295 _messageIds.thanksForPlaying = IDI_HR1_MSG_THANKS_FOR_PLAYING;
296
297 // Load message offsets
298 stream->seek(IDI_HR1_OFS_MSGS);
299 for (uint i = 0; i < IDI_HR1_NUM_MESSAGES; ++i)
300 _messages.push_back(_files->getDataBlock(IDS_HR1_MESSAGES, stream->readUint16LE()));
301
302 // Load picture data from executable
303 stream->seek(IDI_HR1_OFS_PICS);
304 for (uint i = 1; i <= IDI_HR1_NUM_PICS; ++i) {
305 byte block = stream->readByte();
306 Common::String name = Common::String::format("BLOCK%i", block);
307 uint16 offset = stream->readUint16LE();
308 _pictures[i] = _files->getDataBlock(name, offset);
309 }
310
311 // Load commands from executable
312 stream->seek(IDI_HR1_OFS_CMDS_1);
313 readCommands(*stream, _roomCommands);
314
315 stream->seek(IDI_HR1_OFS_CMDS_0);
316 readCommands(*stream, _globalCommands);
317
318 // Load dropped item offsets
319 stream->seek(IDI_HR1_OFS_ITEM_OFFSETS);
320 loadDroppedItemOffsets(*stream, IDI_HR1_NUM_ITEM_OFFSETS);
321
322 // Load shapes
323 stream->seek(IDI_HR1_OFS_SHAPES);
324 uint16 cornersCount = stream->readUint16LE();
325 for (uint i = 0; i < cornersCount; ++i)
326 _corners.push_back(_files->getDataBlock(IDS_HR1_EXE_1, IDI_HR1_OFS_SHAPES + stream->readUint16LE()));
327
328 if (stream->eos() || stream->err())
329 error("Failed to read game data from '" IDS_HR1_EXE_1 "'");
330
331 stream->seek(IDI_HR1_OFS_VERBS);
332 loadWords(*stream, _verbs, _priVerbs);
333
334 stream->seek(IDI_HR1_OFS_NOUNS);
335 loadWords(*stream, _nouns, _priNouns);
336 }
337
initGameState()338 void HiRes1Engine::initGameState() {
339 _state.vars.resize(IDI_HR1_NUM_VARS);
340
341 StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1));
342
343 // Load room data from executable
344 _roomDesc.clear();
345 stream->seek(IDI_HR1_OFS_ROOMS);
346 for (uint i = 0; i < IDI_HR1_NUM_ROOMS; ++i) {
347 Room room;
348 stream->readByte();
349 _roomDesc.push_back(stream->readByte());
350 for (uint j = 0; j < 6; ++j)
351 room.connections[j] = stream->readByte();
352 room.picture = stream->readByte();
353 room.curPicture = stream->readByte();
354 _state.rooms.push_back(room);
355 }
356
357 // Load item data from executable
358 stream->seek(IDI_HR1_OFS_ITEMS);
359 byte id;
360 while ((id = stream->readByte()) != 0xff) {
361 Item item;
362
363 item.id = id;
364 item.noun = stream->readByte();
365 item.room = stream->readByte();
366 item.picture = stream->readByte();
367 item.isShape = stream->readByte();
368 item.position.x = stream->readByte();
369 item.position.y = stream->readByte();
370 item.state = stream->readByte();
371 item.description = stream->readByte();
372
373 stream->readByte();
374
375 byte size = stream->readByte();
376
377 for (uint i = 0; i < size; ++i)
378 item.roomPictures.push_back(stream->readByte());
379
380 _state.items.push_back(item);
381 }
382 }
383
restartGame()384 void HiRes1Engine::restartGame() {
385 _display->printString(_strings.pressReturn);
386 initState();
387 _display->printAsciiString(_strings.lineFeeds);
388 }
389
printString(const Common::String & str)390 void HiRes1Engine::printString(const Common::String &str) {
391 Common::String wrap = str;
392 wordWrap(wrap);
393 _display->printString(wrap);
394
395 if (_messageDelay)
396 delay(14 * 166018 / 1000);
397 }
398
loadMessage(uint idx) const399 Common::String HiRes1Engine::loadMessage(uint idx) const {
400 const char returnChar = _display->asciiToNative('\r');
401 StreamPtr stream(_messages[idx]->createReadStream());
402 return readString(*stream, returnChar) + returnChar;
403 }
404
printMessage(uint idx)405 void HiRes1Engine::printMessage(uint idx) {
406 // Messages with hardcoded overrides don't delay after printing.
407 // It's unclear if this is a bug or not. In some cases the result
408 // is that these strings will scroll past the four-line text window
409 // before the user gets a chance to read them.
410 // NOTE: later games seem to wait for a key when the text window
411 // overflows and don't use delays. It might be better to use
412 // that system for this game as well.
413 switch (idx) {
414 case IDI_HR1_MSG_CANT_GO_THERE:
415 _display->printString(_gameStrings.cantGoThere);
416 return;
417 case IDI_HR1_MSG_DONT_HAVE_IT:
418 _display->printString(_gameStrings.dontHaveIt);
419 return;
420 case IDI_HR1_MSG_DONT_UNDERSTAND:
421 _display->printString(_gameStrings.dontUnderstand);
422 return;
423 case IDI_HR1_MSG_GETTING_DARK:
424 _display->printString(_gameStrings.gettingDark);
425 return;
426 default:
427 printString(loadMessage(idx));
428 }
429 }
430
drawItems()431 void HiRes1Engine::drawItems() {
432 Common::List<Item>::iterator item;
433
434 uint dropped = 0;
435
436 for (item = _state.items.begin(); item != _state.items.end(); ++item) {
437 // Skip items not in this room
438 if (item->room != _state.room)
439 continue;
440
441 if (item->state == IDI_ITEM_DROPPED) {
442 // Draw dropped item if in normal view
443 if (getCurRoom().picture == getCurRoom().curPicture)
444 drawItem(*item, _itemOffsets[dropped++]);
445 } else {
446 // Draw fixed item if current view is in the pic list
447 Common::Array<byte>::const_iterator pic;
448
449 for (pic = item->roomPictures.begin(); pic != item->roomPictures.end(); ++pic) {
450 if (*pic == getCurRoom().curPicture) {
451 drawItem(*item, item->position);
452 break;
453 }
454 }
455 }
456 }
457 }
458
drawItem(Item & item,const Common::Point & pos)459 void HiRes1Engine::drawItem(Item &item, const Common::Point &pos) {
460 if (item.isShape) {
461 StreamPtr stream(_corners[item.picture - 1]->createReadStream());
462 Common::Point p(pos);
463 _graphics->drawShape(*stream, p);
464 } else
465 drawPic(item.picture, pos);
466 }
467
loadRoom(byte roomNr)468 void HiRes1Engine::loadRoom(byte roomNr) {
469 _roomData.description = loadMessage(_roomDesc[_state.room - 1]);
470 }
471
showRoom()472 void HiRes1Engine::showRoom() {
473 _state.curPicture = getCurRoom().curPicture;
474 _graphics->clearScreen();
475 loadRoom(_state.room);
476
477 if (!_state.isDark) {
478 drawPic(getCurRoom().curPicture);
479 drawItems();
480 }
481
482 _display->renderGraphics();
483 _messageDelay = false;
484 printString(_roomData.description);
485 _messageDelay = true;
486 }
487
wordWrap(Common::String & str) const488 void HiRes1Engine::wordWrap(Common::String &str) const {
489 uint end = 39;
490
491 const char spaceChar = _display->asciiToNative(' ');
492 const char returnChar = _display->asciiToNative('\r');
493
494 while (1) {
495 if (str.size() <= end)
496 return;
497
498 while (str[end] != spaceChar)
499 --end;
500
501 str.setChar(returnChar, end);
502 end += 40;
503 }
504 }
505
HiRes1Engine_create(OSystem * syst,const AdlGameDescription * gd)506 Engine *HiRes1Engine_create(OSystem *syst, const AdlGameDescription *gd) {
507 return new HiRes1Engine(syst, gd);
508 }
509
510 } // End of namespace Adl
511