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