Initial commit
This commit is contained in:
4
engines/agi/POTFILES
Normal file
4
engines/agi/POTFILES
Normal file
@@ -0,0 +1,4 @@
|
||||
engines/agi/detection_tables.h
|
||||
engines/agi/font.cpp
|
||||
engines/agi/metaengine.cpp
|
||||
engines/agi/saveload.cpp
|
||||
884
engines/agi/agi.cpp
Normal file
884
engines/agi/agi.cpp
Normal file
@@ -0,0 +1,884 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/md5.h"
|
||||
#include "common/file.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/savefile.h"
|
||||
#include "common/config-manager.h"
|
||||
#include "common/debug-channels.h"
|
||||
#include "common/random.h"
|
||||
#include "common/textconsole.h"
|
||||
|
||||
#include "engines/util.h"
|
||||
|
||||
#include "base/plugins.h"
|
||||
#include "base/version.h"
|
||||
|
||||
#include "graphics/cursorman.h"
|
||||
|
||||
#include "audio/mididrv.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/detection.h"
|
||||
#include "agi/font.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/inv.h"
|
||||
#include "agi/loader.h"
|
||||
#include "agi/sprite.h"
|
||||
#include "agi/text.h"
|
||||
#include "agi/keyboard.h"
|
||||
#include "agi/menu.h"
|
||||
#include "agi/systemui.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "gui/predictivedialog.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
void AgiEngine::allowSynthetic(bool allow) {
|
||||
_allowSynthetic = allow;
|
||||
}
|
||||
|
||||
void AgiEngine::wait(uint32 msec, bool busy) {
|
||||
uint32 endTime = _system->getMillis() + msec;
|
||||
|
||||
if (busy) {
|
||||
_gfx->setMouseCursor(true); // Busy mouse cursor
|
||||
}
|
||||
|
||||
do {
|
||||
processScummVMEvents();
|
||||
_system->updateScreen();
|
||||
_system->delayMillis(10);
|
||||
} while (_system->getMillis() < endTime);
|
||||
|
||||
if (busy) {
|
||||
_gfx->setMouseCursor(); // regular mouse cursor
|
||||
}
|
||||
}
|
||||
|
||||
int AgiEngine::agiInit() {
|
||||
debug(2, "initializing");
|
||||
debug(2, "game version = 0x%x", getVersion());
|
||||
|
||||
// initialize with adj.ego.move.to.x.y(0, 0) so to speak
|
||||
_game.adjMouseX = _game.adjMouseY = 0;
|
||||
|
||||
// reset all flags to false and all variables to 0
|
||||
memset(_game.flags, 0, sizeof(_game.flags));
|
||||
memset(_game.vars, 0, sizeof(_game.vars));
|
||||
|
||||
// clear all resources and events
|
||||
for (int i = 0; i < MAX_DIRECTORY_ENTRIES; i++) {
|
||||
_game.views[i].reset();
|
||||
_game.pictures[i].reset();
|
||||
_game.logics[i].reset();
|
||||
_game.sounds[i] = nullptr; // _game.sounds contains pointers now
|
||||
_game.dirView[i].reset();
|
||||
_game.dirPic[i].reset();
|
||||
_game.dirLogic[i].reset();
|
||||
_game.dirSound[i].reset();
|
||||
}
|
||||
|
||||
// clear view table
|
||||
for (int i = 0; i < SCREENOBJECTS_MAX; i++) {
|
||||
_game.screenObjTable[i].reset();
|
||||
}
|
||||
|
||||
_game.addToPicView.reset();
|
||||
|
||||
_words->clearEgoWords();
|
||||
|
||||
if (!_menu)
|
||||
_menu = new GfxMenu(this, _gfx, _picture, _text);
|
||||
|
||||
_gfx->initPriorityTable();
|
||||
|
||||
// Clear the string buffer on startup, but not when the game restarts, as
|
||||
// some scripts expect that the game strings remain unaffected after a
|
||||
// restart. An example is script 98 in SQ2, which is not invoked on restart
|
||||
// to ask Ego's name again. The name is supposed to be maintained in string 1.
|
||||
// Fixes bug #5673.
|
||||
if (!_restartGame) {
|
||||
memset(_game.strings, 0, sizeof(_game.strings));
|
||||
}
|
||||
|
||||
// setup emulation
|
||||
|
||||
switch (getVersion() >> 12) {
|
||||
case 2:
|
||||
debug("Emulating Sierra AGI v%x.%03x",
|
||||
(int)(getVersion() >> 12) & 0xF,
|
||||
(int)(getVersion()) & 0xFFF);
|
||||
break;
|
||||
case 3:
|
||||
debug("Emulating Sierra AGI v%x.002.%03x",
|
||||
(int)(getVersion() >> 12) & 0xF,
|
||||
(int)(getVersion()) & 0xFFF);
|
||||
break;
|
||||
default:
|
||||
warning("Unknown AGI Emulation Version %x", (int)(getVersion() >> 12));
|
||||
break;
|
||||
}
|
||||
|
||||
if (getPlatform() == Common::kPlatformAmiga)
|
||||
debug(1, "Amiga padded game detected.");
|
||||
|
||||
if (getFeatures() & GF_AGDS)
|
||||
debug(1, "AGDS mode enabled.");
|
||||
|
||||
int ec = _loader->loadDirs();
|
||||
|
||||
if (ec == errOK)
|
||||
ec = _loader->loadObjects();
|
||||
|
||||
// note: demos has no words.tok
|
||||
if (ec == errOK)
|
||||
ec = _loader->loadWords();
|
||||
|
||||
// Load logic 0 into memory
|
||||
if (ec == errOK)
|
||||
ec = loadResource(RESOURCETYPE_LOGIC, 0);
|
||||
|
||||
_keyHoldMode = false;
|
||||
_keyHoldModeLastKey = Common::KEYCODE_INVALID;
|
||||
|
||||
_game.mouseFence.setWidth(0); // Reset
|
||||
|
||||
// Reset in-game timer
|
||||
inGameTimerReset();
|
||||
|
||||
applyVolumeToMixer();
|
||||
|
||||
// Error on Game Adaptation Language, because it is not implemented yet.
|
||||
// This allows testing the GAL components that have been developed, such
|
||||
// as the resource loader, with our debug console.
|
||||
if (getGameType() == GType_GAL) {
|
||||
error("Game Adaptation Language not implemented yet");
|
||||
}
|
||||
|
||||
return ec;
|
||||
}
|
||||
|
||||
void AgiEngine::unloadResources() {
|
||||
// Make sure logic 0 is always loaded
|
||||
for (int i = 1; i < MAX_DIRECTORY_ENTRIES; i++) {
|
||||
unloadResource(RESOURCETYPE_LOGIC, i);
|
||||
}
|
||||
for (int i = 0; i < MAX_DIRECTORY_ENTRIES; i++) {
|
||||
unloadResource(RESOURCETYPE_VIEW, i);
|
||||
unloadResource(RESOURCETYPE_PICTURE, i);
|
||||
unloadResource(RESOURCETYPE_SOUND, i);
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::agiDeinit() {
|
||||
if (!_loader)
|
||||
return;
|
||||
|
||||
_words->clearEgoWords(); // remove all words from memory
|
||||
unloadResources(); // unload resources in memory
|
||||
unloadResource(RESOURCETYPE_LOGIC, 0);
|
||||
_objects.clear();
|
||||
_words->unloadDictionary();
|
||||
|
||||
clearImageStack();
|
||||
}
|
||||
|
||||
int AgiEngine::loadResource(int16 resourceType, int16 resourceNr) {
|
||||
if (resourceNr >= MAX_DIRECTORY_ENTRIES)
|
||||
return errBadResource;
|
||||
|
||||
int ec = errOK;
|
||||
uint8 *data = nullptr;
|
||||
switch (resourceType) {
|
||||
case RESOURCETYPE_LOGIC:
|
||||
if (~_game.dirLogic[resourceNr].flags & RES_LOADED) {
|
||||
unloadResource(RESOURCETYPE_LOGIC, resourceNr);
|
||||
|
||||
// load raw resource into data
|
||||
data = _loader->loadVolumeResource(&_game.dirLogic[resourceNr]);
|
||||
_game.logics[resourceNr].data = data;
|
||||
|
||||
// uncompressed logic files need to be decrypted
|
||||
if (data != nullptr) {
|
||||
// RES_LOADED flag gets set by decode logic
|
||||
ec = decodeLogic(resourceNr);
|
||||
_game.logics[resourceNr].sIP = 2;
|
||||
} else {
|
||||
ec = errBadResource;
|
||||
}
|
||||
}
|
||||
|
||||
// reset code pointer in case logic was cached
|
||||
_game.logics[resourceNr].cIP = _game.logics[resourceNr].sIP;
|
||||
break;
|
||||
|
||||
case RESOURCETYPE_PICTURE:
|
||||
if (~_game.dirPic[resourceNr].flags & RES_LOADED) {
|
||||
// if loaded but not cached, unload it
|
||||
// if cached but not loaded, etc
|
||||
unloadResource(RESOURCETYPE_PICTURE, resourceNr);
|
||||
data = _loader->loadVolumeResource(&_game.dirPic[resourceNr]);
|
||||
|
||||
if (data != nullptr) {
|
||||
_game.pictures[resourceNr].rdata = data;
|
||||
_game.dirPic[resourceNr].flags |= RES_LOADED;
|
||||
} else {
|
||||
ec = errBadResource;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case RESOURCETYPE_SOUND:
|
||||
if (~_game.dirSound[resourceNr].flags & RES_LOADED) {
|
||||
data = _loader->loadVolumeResource(&_game.dirSound[resourceNr]);
|
||||
|
||||
// "data" is freed by objects created by createFromRawResource on success
|
||||
const bool isAgiV1 = (getVersion() <= 0x2001);
|
||||
_game.sounds[resourceNr] = AgiSound::createFromRawResource(data, _game.dirSound[resourceNr].len, resourceNr, _soundemu, isAgiV1);
|
||||
if (_game.sounds[resourceNr] != nullptr) {
|
||||
_game.dirSound[resourceNr].flags |= RES_LOADED;
|
||||
} else {
|
||||
free(data);
|
||||
ec = errBadResource;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case RESOURCETYPE_VIEW:
|
||||
// Load a VIEW resource into memory...
|
||||
// Since VIEWS alter the view table ALL the time
|
||||
// can we cache the view? or must we reload it all
|
||||
// the time?
|
||||
if (~_game.dirView[resourceNr].flags & RES_LOADED) {
|
||||
unloadResource(RESOURCETYPE_VIEW, resourceNr);
|
||||
data = _loader->loadVolumeResource(&_game.dirView[resourceNr]);
|
||||
if (data) {
|
||||
_game.dirView[resourceNr].flags |= RES_LOADED;
|
||||
ec = decodeView(data, _game.dirView[resourceNr].len, resourceNr);
|
||||
free(data);
|
||||
} else {
|
||||
ec = errBadResource;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ec = errBadResource;
|
||||
break;
|
||||
}
|
||||
|
||||
// WORKAROUND: Patches broken picture 147 in a corrupted Amiga version of Gold Rush! (v2.05 1989-03-09).
|
||||
// The picture can be seen in room 147 after dropping through the outhouse's hole in room 146.
|
||||
if (ec == errOK && getGameID() == GID_GOLDRUSH && resourceType == RESOURCETYPE_PICTURE && resourceNr == 147 && _game.dirPic[resourceNr].len == 1982) {
|
||||
uint8 *pic = _game.pictures[resourceNr].rdata;
|
||||
Common::MemoryReadStream picStream(pic, _game.dirPic[resourceNr].len);
|
||||
Common::String md5str = Common::computeStreamMD5AsString(picStream, _game.dirPic[resourceNr].len);
|
||||
if (md5str == "1c685eb048656cedcee4eb6eca2cecea") {
|
||||
pic[0x042] = 0x4B; // 0x49 -> 0x4B
|
||||
pic[0x043] = 0x66; // 0x26 -> 0x66
|
||||
pic[0x204] = 0x68; // 0x28 -> 0x68
|
||||
pic[0x6C0] = 0x2D; // 0x25 -> 0x2D
|
||||
pic[0x6F0] = 0xF0; // 0x70 -> 0xF0
|
||||
pic[0x734] = 0x6F; // 0x2F -> 0x6F
|
||||
}
|
||||
}
|
||||
|
||||
return ec;
|
||||
}
|
||||
|
||||
void AgiEngine::unloadResource(int16 resourceType, int16 resourceNr) {
|
||||
switch (resourceType) {
|
||||
case RESOURCETYPE_LOGIC:
|
||||
unloadLogic(resourceNr);
|
||||
break;
|
||||
case RESOURCETYPE_PICTURE:
|
||||
unloadPicture(resourceNr);
|
||||
break;
|
||||
case RESOURCETYPE_VIEW:
|
||||
unloadView(resourceNr);
|
||||
break;
|
||||
case RESOURCETYPE_SOUND:
|
||||
_sound->unloadSound(resourceNr);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::unloadPicture(int16 picNr) {
|
||||
if (_game.dirPic[picNr].flags & RES_LOADED) {
|
||||
free(_game.pictures[picNr].rdata);
|
||||
_game.pictures[picNr].rdata = nullptr;
|
||||
_game.dirPic[picNr].flags &= ~RES_LOADED;
|
||||
}
|
||||
}
|
||||
|
||||
struct GameSettings {
|
||||
const char *gameid;
|
||||
const char *description;
|
||||
byte id;
|
||||
uint32 features;
|
||||
const char *detectname;
|
||||
};
|
||||
|
||||
AgiBase::AgiBase(OSystem *syst, const AGIGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
|
||||
_noSaveLoadAllowed = false;
|
||||
|
||||
_rnd = new Common::RandomSource("agi");
|
||||
_sound = nullptr;
|
||||
|
||||
initFeatures();
|
||||
initVersion();
|
||||
}
|
||||
|
||||
AgiBase::~AgiBase() {
|
||||
delete _rnd;
|
||||
delete _sound;
|
||||
}
|
||||
|
||||
void AgiBase::initRenderMode() {
|
||||
Common::Platform platform = Common::parsePlatform(ConfMan.get("platform"));
|
||||
Common::RenderMode configRenderMode = Common::parseRenderMode(ConfMan.get("render_mode").c_str());
|
||||
|
||||
// Default to EGA PC rendering
|
||||
_renderMode = Common::kRenderEGA;
|
||||
|
||||
switch (platform) {
|
||||
case Common::kPlatformDOS:
|
||||
// Keep EGA
|
||||
break;
|
||||
case Common::kPlatformAmiga:
|
||||
_renderMode = Common::kRenderAmiga;
|
||||
break;
|
||||
case Common::kPlatformApple2GS:
|
||||
_renderMode = Common::kRenderApple2GS;
|
||||
break;
|
||||
case Common::kPlatformAtariST:
|
||||
_renderMode = Common::kRenderAtariST;
|
||||
break;
|
||||
case Common::kPlatformMacintosh:
|
||||
_renderMode = Common::kRenderMacintosh;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// If render mode is explicitly set, force rendermode
|
||||
switch (configRenderMode) {
|
||||
case Common::kRenderCGA:
|
||||
case Common::kRenderEGA:
|
||||
case Common::kRenderVGA:
|
||||
case Common::kRenderHercG:
|
||||
case Common::kRenderHercA:
|
||||
case Common::kRenderAmiga:
|
||||
case Common::kRenderApple2GS:
|
||||
case Common::kRenderAtariST:
|
||||
case Common::kRenderMacintosh:
|
||||
_renderMode = configRenderMode;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (getFeatures() & GF_AGI256) {
|
||||
// If current game is AGI256, switch (force) to VGA render mode
|
||||
_renderMode = Common::kRenderVGA;
|
||||
}
|
||||
}
|
||||
|
||||
const byte *AgiBase::getFontData() {
|
||||
return _font->getFontData();
|
||||
}
|
||||
|
||||
AgiEngine::AgiEngine(OSystem *syst, const AGIGameDescription *gameDesc) : AgiBase(syst, gameDesc) {
|
||||
// Setup mixer
|
||||
syncSoundSettings();
|
||||
|
||||
memset(&_debug, 0, sizeof(struct AgiDebug));
|
||||
|
||||
_game.mouseEnabled = ConfMan.getBool("mousesupport");
|
||||
_game.mouseHidden = !_game.mouseEnabled;
|
||||
|
||||
_game.predictiveDlgOnMouseClick = ConfMan.getBool("predictivedlgonmouseclick");
|
||||
|
||||
_game._vm = this;
|
||||
|
||||
_game.gfxMode = true;
|
||||
|
||||
_keyQueueStart = 0;
|
||||
_keyQueueEnd = 0;
|
||||
|
||||
_allowSynthetic = false;
|
||||
|
||||
_intobj = nullptr;
|
||||
|
||||
_restartGame = false;
|
||||
|
||||
_firstSlot = 0;
|
||||
|
||||
resetControllers();
|
||||
|
||||
_game._curLogic = nullptr;
|
||||
_veryFirstInitialCycle = true;
|
||||
_instructionCounter = 0;
|
||||
resetGetVarSecondsHeuristic();
|
||||
|
||||
_setVolumeBrokenFangame = false; // for further study see AgiEngine::applyVolumeToMixer()
|
||||
|
||||
_playTimeInSecondsAdjust = 0;
|
||||
_lastUsedPlayTimeInCycles = 0;
|
||||
_lastUsedPlayTimeInSeconds = 0;
|
||||
_passedPlayTimeCycles = 0;
|
||||
|
||||
memset(_keyQueue, 0, sizeof(_keyQueue));
|
||||
|
||||
_font = nullptr;
|
||||
_gfx = nullptr;
|
||||
_sound = nullptr;
|
||||
_picture = nullptr;
|
||||
_sprites = nullptr;
|
||||
_text = nullptr;
|
||||
_loader = nullptr;
|
||||
_menu = nullptr;
|
||||
_systemUI = nullptr;
|
||||
_inventory = nullptr;
|
||||
_logFile = nullptr;
|
||||
|
||||
_keyHoldMode = false;
|
||||
_keyHoldModeLastKey = Common::KEYCODE_INVALID;
|
||||
|
||||
_artificialDelayCurrentRoom = 0;
|
||||
_artificialDelayCurrentPicture = 0;
|
||||
|
||||
#ifdef USE_TTS
|
||||
_previousDisplayRow = -1;
|
||||
_queueNextText = false;
|
||||
_voiceClock = true;
|
||||
_replaceDisplayNewlines = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void AgiEngine::initialize() {
|
||||
// TODO: Some sound emulation modes do not fit our current music
|
||||
// drivers, and I'm not sure what they are. For now, they might
|
||||
// as well be called "PC Speaker" and "Not PC Speaker".
|
||||
|
||||
// If platform is Apple or CoCo3 then their sound emulation must be used.
|
||||
// The sound resources in these games have platform-specific formats.
|
||||
if (getPlatform() == Common::kPlatformApple2) {
|
||||
_soundemu = SOUND_EMU_APPLE2;
|
||||
} else if (getPlatform() == Common::kPlatformApple2GS) {
|
||||
_soundemu = SOUND_EMU_APPLE2GS;
|
||||
} else if (getPlatform() == Common::kPlatformCoCo3) {
|
||||
_soundemu = SOUND_EMU_COCO3;
|
||||
} else if (ConfMan.get("music_driver") == "auto") {
|
||||
// Default sound is the proper PCJr emulation
|
||||
_soundemu = SOUND_EMU_PCJR;
|
||||
} else {
|
||||
switch (MidiDriver::getMusicType(MidiDriver::detectDevice(MDT_PCSPK | MDT_AMIGA | MDT_ADLIB | MDT_PCJR | MDT_MIDI))) {
|
||||
case MT_PCSPK:
|
||||
_soundemu = SOUND_EMU_PC;
|
||||
break;
|
||||
case MT_ADLIB:
|
||||
_soundemu = SOUND_EMU_NONE;
|
||||
break;
|
||||
case MT_PCJR:
|
||||
_soundemu = SOUND_EMU_PCJR;
|
||||
break;
|
||||
case MT_AMIGA:
|
||||
_soundemu = SOUND_EMU_AMIGA;
|
||||
break;
|
||||
default:
|
||||
_soundemu = SOUND_EMU_MIDI;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
initRenderMode();
|
||||
|
||||
setDebugger(new Console(this));
|
||||
_words = new Words(this);
|
||||
_font = new GfxFont(this);
|
||||
_gfx = new GfxMgr(this, _font);
|
||||
_sound = new SoundMgr(this, _mixer);
|
||||
_picture = new PictureMgr(this, _gfx);
|
||||
_sprites = new SpritesMgr(this, _gfx);
|
||||
_text = new TextMgr(this, _words, _gfx);
|
||||
_systemUI = new SystemUI(this, _gfx, _text);
|
||||
_inventory = new InventoryMgr(this, _gfx, _text, _systemUI);
|
||||
|
||||
_font->init();
|
||||
_gfx->initVideo();
|
||||
|
||||
_text->init(_systemUI);
|
||||
|
||||
_text->charAttrib_Set(15, 0);
|
||||
|
||||
if (getGameType() == GType_GAL) {
|
||||
if (getPlatform() == Common::kPlatformApple2) {
|
||||
_loader = new GalLoader_A2(this);
|
||||
} else {
|
||||
_loader = new GalLoader(this);
|
||||
}
|
||||
} else if (getPlatform() == Common::kPlatformApple2) {
|
||||
_loader = new AgiLoader_A2(this);
|
||||
} else if (getVersion() <= 0x2001) {
|
||||
_loader = new AgiLoader_v1(this);
|
||||
} else if (getVersion() <= 0x2999) {
|
||||
_loader = new AgiLoader_v2(this);
|
||||
} else {
|
||||
_loader = new AgiLoader_v3(this);
|
||||
}
|
||||
_loader->init();
|
||||
|
||||
// finally set up actual VM opcodes, because we should now have figured out the right AGI version
|
||||
setupOpCodes(getVersion());
|
||||
|
||||
#ifdef USE_TTS
|
||||
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
|
||||
if (ttsMan) {
|
||||
ttsMan->enable(ConfMan.getBool("tts_enabled"));
|
||||
ttsMan->setLanguage(ConfMan.get("language"));
|
||||
}
|
||||
|
||||
switch (getLanguage()) {
|
||||
case Common::HE_ISR:
|
||||
_ttsEncoding = Common::CodePage::kWindows1255;
|
||||
break;
|
||||
case Common::RU_RUS:
|
||||
_ttsEncoding = Common::CodePage::kDos866;
|
||||
break;
|
||||
default:
|
||||
_ttsEncoding = Common::CodePage::kDos850;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool AgiEngine::promptIsEnabled() {
|
||||
return _text->promptIsEnabled();
|
||||
}
|
||||
|
||||
void AgiEngine::redrawScreen() {
|
||||
_game.gfxMode = true; // enable graphics mode
|
||||
_gfx->setPalette(true); // set graphics mode palette
|
||||
_text->charAttrib_Set(_text->_textAttrib.foreground, _text->_textAttrib.background);
|
||||
_gfx->clearDisplay(0);
|
||||
_picture->showPicture();
|
||||
_text->statusDraw();
|
||||
_text->promptRedraw();
|
||||
}
|
||||
|
||||
AgiEngine::~AgiEngine() {
|
||||
agiDeinit();
|
||||
delete _loader;
|
||||
if (_gfx) {
|
||||
_gfx->deinitVideo();
|
||||
}
|
||||
if (_logFile) {
|
||||
_logFile->finalize();
|
||||
_logFile->close();
|
||||
}
|
||||
delete _logFile;
|
||||
delete _inventory;
|
||||
delete _systemUI;
|
||||
delete _menu;
|
||||
delete _text;
|
||||
delete _sprites;
|
||||
delete _picture;
|
||||
delete _gfx;
|
||||
delete _font;
|
||||
delete _words;
|
||||
}
|
||||
|
||||
Common::Error AgiBase::init() {
|
||||
initialize();
|
||||
|
||||
_gfx->setPalette(true);
|
||||
|
||||
return Common::kNoError;
|
||||
}
|
||||
|
||||
Common::Error AgiEngine::go() {
|
||||
if (_game.mouseEnabled) {
|
||||
CursorMan.showMouse(true);
|
||||
}
|
||||
inGameTimerReset();
|
||||
|
||||
int ec = runGame();
|
||||
|
||||
switch (ec) {
|
||||
case errOK: return Common::kNoError;
|
||||
case errFilesNotFound: return Common::kNoGameDataFoundError;
|
||||
case errBadFileOpen: return Common::kReadingFailed;
|
||||
default: return Common::kUnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::syncSoundSettings() {
|
||||
Engine::syncSoundSettings();
|
||||
|
||||
applyVolumeToMixer();
|
||||
}
|
||||
|
||||
#ifdef USE_TTS
|
||||
|
||||
void AgiEngine::sayText(const Common::String &text, Common::TextToSpeechManager::Action action, bool checkPreviousSaid) {
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
|
||||
if (ttsMan && ConfMan.getBool("tts_enabled") && (!checkPreviousSaid || _previousSaid != text)) {
|
||||
Common::String ttsMessage = text;
|
||||
|
||||
if (_replaceDisplayNewlines) {
|
||||
ttsMessage.replace('\n', ' ');
|
||||
}
|
||||
|
||||
ttsMessage.replace('<', ' ');
|
||||
ttsMessage.replace('=', ' ');
|
||||
ttsMan->say(ttsMessage, action, _ttsEncoding);
|
||||
_previousSaid = text;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::stopTextToSpeech(bool clearPreviousSaid) {
|
||||
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
|
||||
if (ttsMan && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
|
||||
ttsMan->stop();
|
||||
|
||||
if (clearPreviousSaid) {
|
||||
_previousSaid.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// WORKAROUND:
|
||||
// Sometimes Sierra printed some text on the screen and did a room change immediately afterwards expecting the
|
||||
// interpreter to load the data for a bit of time. This of course doesn't happen in our AGI, so we try to
|
||||
// detect such situations via heuristic and then delay the game for a bit.
|
||||
// In those cases a wait mouse cursor will be shown.
|
||||
//
|
||||
// Scenes that need this:
|
||||
//
|
||||
// Gold Rush:
|
||||
// - During Stagecoach path, after getting solving the steep hill "Congratulations!!!" (NewRoom)
|
||||
// - when following your mule "Yet right on his tail!!!" (NewRoom/NewPicture - but room 123 stays the same)
|
||||
// Manhunter 1:
|
||||
// - intro text screen (DrawPic)
|
||||
// - MAD "zooming in..." during intro and other scenes, for example room 124 (NewRoom)
|
||||
// The NewRoom call is not done during the same cycle as the "zooming in..." print call.
|
||||
// Space Quest 1:
|
||||
// - right at the start of the game (NewRoom)
|
||||
// - right at the end of the asteroids "That was mighty close!" (NewRoom)
|
||||
// Space Quest 2
|
||||
// - right at the start of the game (NewRoom)
|
||||
// - after exiting the very first room, a message pops up, that isn't readable without it (NewRoom)
|
||||
// - Climbing into shuttle on planet Labion. "You open the hatch and head on in." (NewRoom)
|
||||
|
||||
|
||||
// Games, that must not be triggered:
|
||||
//
|
||||
// Fanmade Voodoo Girl:
|
||||
// - waterfall (DrawPic, room 17)
|
||||
// - inside shop (NewRoom, changes to same room every new button, room 4)
|
||||
|
||||
void AgiEngine::nonBlockingText_IsShown() {
|
||||
_game.nonBlockingTextShown = true;
|
||||
_game.nonBlockingTextCyclesLeft = 2; // 1 additional script cycle is counted too
|
||||
}
|
||||
void AgiEngine::nonBlockingText_Forget() {
|
||||
_game.nonBlockingTextShown = false;
|
||||
_game.nonBlockingTextCyclesLeft = 0;
|
||||
}
|
||||
|
||||
void AgiEngine::artificialDelay_Reset() {
|
||||
nonBlockingText_Forget();
|
||||
_artificialDelayCurrentRoom = -1;
|
||||
_artificialDelayCurrentPicture = -1;
|
||||
}
|
||||
|
||||
void AgiEngine::artificialDelay_CycleDone() {
|
||||
if (_game.nonBlockingTextCyclesLeft) {
|
||||
_game.nonBlockingTextCyclesLeft--;
|
||||
|
||||
if (!_game.nonBlockingTextCyclesLeft) {
|
||||
// cycle count expired, we assume that non-blocking text won't be a problem for room / pic change
|
||||
_game.nonBlockingTextShown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WORKAROUND:
|
||||
// On Apple IIgs, there are situations like for example the Police Quest 1 intro, where music is playing
|
||||
// and then the scripts switch to a new room, expecting it to load for a bit of time. In ScummVM this results
|
||||
// in music getting cut off, because our loading is basically done in an instant. This also happens in the
|
||||
// original interpreter, when you use a faster CPU in emulation.
|
||||
//
|
||||
// That's why there is an additional table, where one can add such situations to it.
|
||||
// These issues are basically impossible to detect, because sometimes music is also supposed to play throughout
|
||||
// multiple rooms.
|
||||
//
|
||||
// Normally all text-based issues should get detected by the current heuristic. Do not add those in here.
|
||||
|
||||
// script, description, signature patch
|
||||
static const AgiArtificialDelayEntry artificialDelayTable[] = {
|
||||
{ GID_GOLDRUSH, Common::kPlatformApple2GS, ARTIFICIALDELAYTYPE_NEWROOM, 14, 21, 2200 }, // Stagecoach path: right after getting on it in Brooklyn
|
||||
{ GID_PQ1, Common::kPlatformApple2GS, ARTIFICIALDELAYTYPE_NEWPICTURE, 1, 2, 2200 }, // Intro: music track is supposed to finish before credits screen. Developers must have assumed that room loading would take that long.
|
||||
{ GID_MH1, Common::kPlatformApple2GS, ARTIFICIALDELAYTYPE_NEWPICTURE, 155, 183, 2200 }, // Happens, when hitting fingers at bar
|
||||
{ GID_AGIDEMO, Common::kPlatformUnknown, ARTIFICIALDELAYTYPE_END, -1, -1, 0 }
|
||||
};
|
||||
|
||||
uint16 AgiEngine::artificialDelay_SearchTable(AgiArtificialDelayTriggerType triggerType, int16 orgNr, int16 newNr) {
|
||||
if (getPlatform() != Common::kPlatformApple2GS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const AgiArtificialDelayEntry *delayEntry = artificialDelayTable;
|
||||
|
||||
while (delayEntry->triggerType != ARTIFICIALDELAYTYPE_END) {
|
||||
if (triggerType == delayEntry->triggerType) {
|
||||
if ((orgNr == delayEntry->orgNr) && (newNr == delayEntry->newNr)) {
|
||||
if ((getGameID() == delayEntry->gameId) && (getPlatform() == delayEntry->platform)) {
|
||||
warning("artificial delay forced");
|
||||
return delayEntry->millisecondsDelay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delayEntry++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AgiEngine::artificialDelayTrigger_NewRoom(int16 newRoomNr) {
|
||||
//warning("artificial delay trigger: room %d -> new room %d", _artificialDelayCurrentRoom, newRoomNr);
|
||||
|
||||
if (!_game.automaticRestoreGame) {
|
||||
uint16 millisecondsDelay = artificialDelay_SearchTable(ARTIFICIALDELAYTYPE_NEWROOM, _artificialDelayCurrentRoom, newRoomNr);
|
||||
|
||||
if (_game.nonBlockingTextShown) {
|
||||
if (newRoomNr != _artificialDelayCurrentRoom) {
|
||||
if (millisecondsDelay < 2000) {
|
||||
// wait a bit, we detected non-blocking text
|
||||
millisecondsDelay = 2000; // 2 seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (millisecondsDelay) {
|
||||
wait(millisecondsDelay, true); // set busy mouse cursor
|
||||
_game.nonBlockingTextShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_TTS
|
||||
// Delay room changes until TTS is done speaking, which helps TTS keep up with the text on screen
|
||||
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
|
||||
while (ttsMan && ttsMan->isSpeaking()) {
|
||||
int key = doPollKeyboard();
|
||||
|
||||
if (key != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
wait(10);
|
||||
}
|
||||
#endif
|
||||
|
||||
_artificialDelayCurrentRoom = newRoomNr;
|
||||
}
|
||||
|
||||
void AgiEngine::artificialDelayTrigger_DrawPicture(int16 newPictureNr) {
|
||||
//warning("artificial delay trigger: picture %d -> new picture %d", _artificialDelayCurrentPicture, newPictureNr);
|
||||
|
||||
if (!_game.automaticRestoreGame) {
|
||||
uint16 millisecondsDelay = artificialDelay_SearchTable(ARTIFICIALDELAYTYPE_NEWPICTURE, _artificialDelayCurrentPicture, newPictureNr);
|
||||
|
||||
if (_game.nonBlockingTextShown) {
|
||||
if (newPictureNr != _artificialDelayCurrentPicture) {
|
||||
if (millisecondsDelay < 2000) {
|
||||
// wait a bit, we detected non-blocking text
|
||||
millisecondsDelay = 2000; // 2 seconds, set busy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (millisecondsDelay) {
|
||||
wait(millisecondsDelay, true); // set busy mouse cursor
|
||||
_game.nonBlockingTextShown = false;
|
||||
}
|
||||
}
|
||||
_artificialDelayCurrentPicture = newPictureNr;
|
||||
}
|
||||
|
||||
const char *AgiGame::getString(int number) {
|
||||
if (0 <= number && number <= MAX_STRINGS) {
|
||||
return strings[number];
|
||||
} else {
|
||||
// WORKAROUND: Flag Quest detects the interpreter version by comparing
|
||||
// out of bounds strings to values know to be in memory in Sierra's
|
||||
// interpreters. The game only starts if a known value matches an
|
||||
// allowed version. We return the value for version 2.917. Bug #15060
|
||||
if (number == 56) {
|
||||
return ".917";
|
||||
}
|
||||
warning("invalid string number: %d", number);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void AgiGame::setString(int number, const char *str) {
|
||||
if (0 <= number && number <= MAX_STRINGS) {
|
||||
Common::strlcpy(strings[number], str, MAX_STRINGLEN);
|
||||
} else {
|
||||
// Occurs in Groza, number = 150
|
||||
warning("invalid string number: %d, '%s'", number, str);
|
||||
}
|
||||
}
|
||||
|
||||
void AgiGame::setSpeedLevel(byte s) {
|
||||
speedLevel = s;
|
||||
_vm->setVar(VM_VAR_WINDOW_AUTO_CLOSE_TIMER, 6);
|
||||
switch (speedLevel) {
|
||||
case 0:
|
||||
_vm->_text->messageBox("Fastest speed.");
|
||||
break;
|
||||
case 1:
|
||||
_vm->_text->messageBox("Fast speed.");
|
||||
break;
|
||||
case 2:
|
||||
_vm->_text->messageBox("Normal speed.");
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
_vm->_text->messageBox("Slow speed.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
983
engines/agi/agi.h
Normal file
983
engines/agi/agi.h
Normal file
@@ -0,0 +1,983 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_AGI_H
|
||||
#define AGI_AGI_H
|
||||
|
||||
#include "common/scummsys.h"
|
||||
#include "common/error.h"
|
||||
#include "common/util.h"
|
||||
#include "common/file.h"
|
||||
#include "common/keyboard.h"
|
||||
#include "common/rect.h"
|
||||
#include "common/rendermode.h"
|
||||
#include "common/stack.h"
|
||||
#include "common/str.h"
|
||||
#include "common/system.h"
|
||||
#include "common/text-to-speech.h"
|
||||
|
||||
#include "engines/engine.h"
|
||||
|
||||
#include "gui/debugger.h"
|
||||
|
||||
// AGI resources
|
||||
#include "agi/console.h"
|
||||
#include "agi/view.h"
|
||||
#include "agi/picture.h"
|
||||
#include "agi/logic.h"
|
||||
#include "agi/sound.h"
|
||||
|
||||
namespace Common {
|
||||
class RandomSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the namespace of the AGI engine.
|
||||
*
|
||||
* Status of this engine: ???
|
||||
*
|
||||
* Games using this engine:
|
||||
* - Early Sierra adventure games
|
||||
* - many fan made games
|
||||
* - Mickey's Space Adventure (Pre-AGI)
|
||||
* - Winnie the Pooh in the Hundred Acre Wood (Pre-AGI)
|
||||
* - Troll's Tale (Pre-AGI)
|
||||
*/
|
||||
namespace Agi {
|
||||
|
||||
#define TITLE "AGI engine"
|
||||
|
||||
#define DIR_ "dir"
|
||||
#define LOGDIR "logdir"
|
||||
#define PICDIR "picdir"
|
||||
#define VIEWDIR "viewdir"
|
||||
#define SNDDIR "snddir"
|
||||
#define OBJECTS "object"
|
||||
#define WORDS "words.tok"
|
||||
|
||||
#define MAX_DIRECTORY_ENTRIES 256
|
||||
#define MAX_CONTROLLERS 256
|
||||
#define MAX_VARS 256
|
||||
#define MAX_FLAGS (256 >> 3)
|
||||
#define SCREENOBJECTS_MAX 255 // KQ3 uses o255!
|
||||
#define SCREENOBJECTS_EGO_ENTRY 0 // first entry is ego
|
||||
#define MAX_WORDS 20
|
||||
#define MAX_STRINGS 24 // MAX_STRINGS + 1 used for get.num
|
||||
#define MAX_STRINGLEN 40
|
||||
#define MAX_CONTROLLER_KEYMAPPINGS 39
|
||||
|
||||
#define SAVEDGAME_DESCRIPTION_LEN 30
|
||||
|
||||
#define _EMPTY 0xfffff
|
||||
#define EGO_OWNED 0xff
|
||||
#define EGO_OWNED_V1 0xf9
|
||||
|
||||
#define CRYPT_KEY_SIERRA "Avis Durgan"
|
||||
#define CRYPT_KEY_AGDS "Alex Simkin"
|
||||
|
||||
#define ADD_PIC 1
|
||||
#define ADD_VIEW 2
|
||||
|
||||
#define CMD_BSIZE 12
|
||||
|
||||
enum AgiGameType {
|
||||
GType_PreAGI = 0,
|
||||
GType_V1 = 1,
|
||||
GType_V2 = 2,
|
||||
GType_V3 = 3,
|
||||
GType_A2 = 4,
|
||||
GType_GAL = 5
|
||||
};
|
||||
|
||||
enum AgiGameFeatures {
|
||||
GF_AGIMOUSE = (1 << 0), // marks games created with AGIMOUSE, disables "Click-to-walk mouse interface"
|
||||
GF_AGDS = (1 << 1), // marks games created with AGDS - all using AGI version 2.440
|
||||
GF_AGI256 = (1 << 2), // marks fanmade AGI-256 games
|
||||
GF_FANMADE = (1 << 3), // marks fanmade games
|
||||
GF_2GSOLDSOUND = (1 << 5)
|
||||
};
|
||||
|
||||
enum AgiGameID {
|
||||
GID_AGIDEMO,
|
||||
GID_BC,
|
||||
GID_DDP,
|
||||
GID_GOLDRUSH, // V3
|
||||
GID_KQ1,
|
||||
GID_KQ2,
|
||||
GID_KQ3,
|
||||
GID_KQ4,
|
||||
GID_LSL1,
|
||||
GID_MH1, // V3
|
||||
GID_MH2, // V3
|
||||
GID_MIXEDUP,
|
||||
GID_PQ1,
|
||||
GID_SQ1,
|
||||
GID_SQ2,
|
||||
GID_XMASCARD,
|
||||
GID_FANMADE,
|
||||
GID_GETOUTTASQ, // Fanmade
|
||||
GID_MICKEY, // PreAGI
|
||||
GID_WINNIE, // PreAGI
|
||||
GID_TROLL // PreAGI
|
||||
};
|
||||
|
||||
enum AGIErrors {
|
||||
errOK = 0,
|
||||
errFilesNotFound,
|
||||
errBadFileOpen,
|
||||
errNotEnoughMemory,
|
||||
errBadResource,
|
||||
errIOError,
|
||||
|
||||
errUnk = 127
|
||||
};
|
||||
|
||||
enum kDebugLevels {
|
||||
kDebugLevelMain = 1,
|
||||
kDebugLevelResources,
|
||||
kDebugLevelSprites,
|
||||
kDebugLevelPictures,
|
||||
kDebugLevelInventory,
|
||||
kDebugLevelInput,
|
||||
kDebugLevelMenu,
|
||||
kDebugLevelScripts,
|
||||
kDebugLevelSound,
|
||||
kDebugLevelText,
|
||||
kDebugLevelSavegame,
|
||||
};
|
||||
|
||||
/**
|
||||
* AGI resources.
|
||||
*/
|
||||
enum {
|
||||
RESOURCETYPE_LOGIC = 1,
|
||||
RESOURCETYPE_SOUND,
|
||||
RESOURCETYPE_VIEW,
|
||||
RESOURCETYPE_PICTURE
|
||||
};
|
||||
|
||||
enum {
|
||||
RES_LOADED = 0x01,
|
||||
RES_COMPRESSED = 0x40,
|
||||
RES_PICTURE_V3_NIBBLE_PARM = 0x80 // Flag that gets set for picture resources,
|
||||
// which use a nibble instead of a byte as F0+F2 parameters
|
||||
};
|
||||
|
||||
enum {
|
||||
lCOMMAND_MODE = 1,
|
||||
lTEST_MODE
|
||||
};
|
||||
|
||||
struct gameIdList {
|
||||
gameIdList *next;
|
||||
uint32 version;
|
||||
uint32 crc;
|
||||
char *gName;
|
||||
char *switches;
|
||||
};
|
||||
|
||||
struct Mouse {
|
||||
int button;
|
||||
Common::Point pos;
|
||||
|
||||
Mouse() : button(0) {}
|
||||
};
|
||||
|
||||
// Used by AGI Mouse protocol 1.0 for v27 (i.e. button pressed -variable).
|
||||
enum AgiMouseButton {
|
||||
kAgiMouseButtonUp, // Mouse button is up (not pressed)
|
||||
kAgiMouseButtonLeft, // Left mouse button
|
||||
kAgiMouseButtonRight, // Right mouse button
|
||||
kAgiMouseButtonMiddle // Middle mouse button
|
||||
};
|
||||
|
||||
/**
|
||||
* AGI variables.
|
||||
*/
|
||||
enum {
|
||||
VM_VAR_CURRENT_ROOM = 0, // 0
|
||||
VM_VAR_PREVIOUS_ROOM, // 1
|
||||
VM_VAR_BORDER_TOUCH_EGO, // 2
|
||||
VM_VAR_SCORE, // 3
|
||||
VM_VAR_BORDER_CODE, // 4
|
||||
VM_VAR_BORDER_TOUCH_OBJECT, // 5
|
||||
VM_VAR_EGO_DIRECTION, // 6
|
||||
VM_VAR_MAX_SCORE, // 7
|
||||
VM_VAR_FREE_PAGES, // 8
|
||||
VM_VAR_WORD_NOT_FOUND, // 9
|
||||
VM_VAR_TIME_DELAY, // 10
|
||||
VM_VAR_SECONDS, // 11
|
||||
VM_VAR_MINUTES, // 12
|
||||
VM_VAR_HOURS, // 13
|
||||
VM_VAR_DAYS, // 14
|
||||
VM_VAR_JOYSTICK_SENSITIVITY, // 15
|
||||
VM_VAR_EGO_VIEW_RESOURCE, // 16
|
||||
VM_VAR_AGI_ERROR_CODE, // 17
|
||||
VM_VAR_AGI_ERROR_INFO, // 18
|
||||
VM_VAR_KEY, // 19
|
||||
VM_VAR_COMPUTER, // 20
|
||||
VM_VAR_WINDOW_AUTO_CLOSE_TIMER, // 21
|
||||
VM_VAR_SOUNDGENERATOR, // 22
|
||||
VM_VAR_VOLUME, // 23
|
||||
VM_VAR_MAX_INPUT_CHARACTERS, // 24
|
||||
VM_VAR_SELECTED_INVENTORY_ITEM, // 25
|
||||
VM_VAR_MONITOR = 26, // 26
|
||||
VM_VAR_MOUSE_BUTTONSTATE = 27, // 27
|
||||
VM_VAR_MOUSE_X = 28, // 28
|
||||
VM_VAR_MOUSE_Y = 29 // 29
|
||||
};
|
||||
|
||||
/**
|
||||
* Different monitor types.
|
||||
* Used with AGI variable 26 i.e. vMonitor.
|
||||
*/
|
||||
enum AgiMonitorType {
|
||||
kAgiMonitorCga = 0,
|
||||
//kAgiMonitorTandy = 1, // Not sure about this
|
||||
kAgiMonitorHercules = 2,
|
||||
kAgiMonitorEga = 3
|
||||
//kAgiMonitorVga = 4 // Not sure about this
|
||||
};
|
||||
|
||||
/**
|
||||
* Different computer types.
|
||||
* Used with AGI variable 20 i.e. vComputer.
|
||||
*/
|
||||
enum AgiComputerType {
|
||||
kAgiComputerPC = 0,
|
||||
kAgiComputerApple2 = 3,
|
||||
kAgiComputerAtariST = 4,
|
||||
kAgiComputerAmiga = 5,
|
||||
kAgiComputerApple2GS = 7
|
||||
};
|
||||
|
||||
enum AgiSoundType {
|
||||
kAgiSoundPC = 1,
|
||||
kAgiSoundTandy = 3, // Tandy (This value is also used by the Amiga AGI and Apple IIGS AGI)
|
||||
kAgiSound2GSOld = 8 // Apple IIGS's Gold Rush! (Version 1.0M 1989-02-28 (CE), AGI 3.003) uses value 8
|
||||
};
|
||||
|
||||
/**
|
||||
* AGI flags
|
||||
*/
|
||||
enum {
|
||||
VM_FLAG_EGO_WATER = 0, // 0
|
||||
VM_FLAG_EGO_INVISIBLE,
|
||||
VM_FLAG_ENTERED_CLI,
|
||||
VM_FLAG_EGO_TOUCHED_P2,
|
||||
VM_FLAG_SAID_ACCEPTED_INPUT,
|
||||
VM_FLAG_NEW_ROOM_EXEC, // 5
|
||||
VM_FLAG_RESTART_GAME,
|
||||
VM_FLAG_SCRIPT_BLOCKED,
|
||||
VM_FLAG_JOY_SENSITIVITY,
|
||||
VM_FLAG_SOUND_ON,
|
||||
VM_FLAG_DEBUGGER_ON, // 10
|
||||
VM_FLAG_LOGIC_ZERO_FIRST_TIME,
|
||||
VM_FLAG_RESTORE_JUST_RAN,
|
||||
VM_FLAG_STATUS_SELECTS_ITEMS,
|
||||
VM_FLAG_MENUS_ACCESSIBLE,
|
||||
VM_FLAG_OUTPUT_MODE, // 15
|
||||
VM_FLAG_AUTO_RESTART
|
||||
};
|
||||
|
||||
struct AgiControllerKeyMapping {
|
||||
uint16 keycode;
|
||||
byte controllerSlot;
|
||||
|
||||
AgiControllerKeyMapping() : keycode(0), controllerSlot(0) {}
|
||||
};
|
||||
|
||||
struct AgiObject {
|
||||
int location;
|
||||
Common::String name;
|
||||
};
|
||||
|
||||
struct AgiDir {
|
||||
uint8 volume;
|
||||
uint32 offset;
|
||||
uint32 len;
|
||||
uint32 clen;
|
||||
|
||||
// 0 = not in mem, can be freed
|
||||
// 1 = in mem, can be released
|
||||
// 2 = not in mem, can't be released
|
||||
// 3 = in mem, can't be released
|
||||
// 0x40 = was compressed
|
||||
uint8 flags;
|
||||
|
||||
void reset() {
|
||||
volume = 0xff;
|
||||
offset = _EMPTY;
|
||||
len = 0;
|
||||
clen = 0;
|
||||
flags = 0;
|
||||
}
|
||||
|
||||
AgiDir() { reset(); }
|
||||
};
|
||||
|
||||
struct AgiBlock {
|
||||
bool active;
|
||||
int16 x1, y1;
|
||||
int16 x2, y2;
|
||||
|
||||
AgiBlock() : active(false), x1(0), y1(0), x2(0), y2(0) {}
|
||||
};
|
||||
|
||||
struct ScriptPos {
|
||||
int script;
|
||||
int curIP;
|
||||
};
|
||||
|
||||
enum CycleInnerLoopType {
|
||||
CYCLE_INNERLOOP_GETSTRING = 0,
|
||||
CYCLE_INNERLOOP_GETNUMBER,
|
||||
CYCLE_INNERLOOP_INVENTORY,
|
||||
CYCLE_INNERLOOP_MENU_VIA_KEYBOARD,
|
||||
CYCLE_INNERLOOP_MENU_VIA_MOUSE,
|
||||
CYCLE_INNERLOOP_SYSTEMUI_SELECTSAVEDGAMESLOT,
|
||||
CYCLE_INNERLOOP_SYSTEMUI_VERIFICATION,
|
||||
CYCLE_INNERLOOP_MESSAGEBOX,
|
||||
CYCLE_INNERLOOP_HAVEKEY
|
||||
};
|
||||
|
||||
typedef Common::Array<int16> SavedGameSlotIdArray;
|
||||
|
||||
struct AgiAppleIIgsDelayOverwriteGameEntry;
|
||||
|
||||
/**
|
||||
* AGI game structure.
|
||||
* This structure contains all global data of an AGI game executed
|
||||
* by the interpreter.
|
||||
*/
|
||||
struct AgiGame {
|
||||
AgiEngine *_vm;
|
||||
|
||||
// TODO: Check whether adjMouseX and adjMouseY must be saved and loaded when using savegames.
|
||||
// If they must be then loading and saving is partially broken at the moment.
|
||||
int adjMouseX; /**< last given adj.ego.move.to.x.y-command's 1st parameter */
|
||||
int adjMouseY; /**< last given adj.ego.move.to.x.y-command's 2nd parameter */
|
||||
|
||||
char id[8]; /**< game id */
|
||||
uint32 crc; /**< game CRC */
|
||||
|
||||
// game flags and variables
|
||||
uint8 flags[MAX_FLAGS]; /**< 256 1-bit flags combined into a total of 32 bytes */
|
||||
uint8 vars[MAX_VARS]; /**< 256 variables */
|
||||
|
||||
// internal variables
|
||||
int16 horizon; /**< horizon y coordinate */
|
||||
|
||||
bool cycleInnerLoopActive;
|
||||
int16 cycleInnerLoopType;
|
||||
|
||||
int16 curLogicNr; /**< current logic number */
|
||||
Common::Array<ScriptPos> execStack;
|
||||
|
||||
// internal flags
|
||||
bool playerControl; /**< player is in control */
|
||||
bool exitAllLogics; /**< break cycle after new.room */
|
||||
bool pictureShown; /**< show.pic has been issued */
|
||||
|
||||
// windows
|
||||
AgiBlock block;
|
||||
|
||||
// graphics & text
|
||||
bool gfxMode;
|
||||
|
||||
unsigned int numObjects;
|
||||
|
||||
bool controllerOccurred[MAX_CONTROLLERS]; /**< keyboard keypress events */
|
||||
AgiControllerKeyMapping controllerKeyMapping[MAX_CONTROLLER_KEYMAPPINGS];
|
||||
|
||||
char strings[MAX_STRINGS + 1][MAX_STRINGLEN]; /**< strings */
|
||||
|
||||
// directory entries for resources
|
||||
AgiDir dirLogic[MAX_DIRECTORY_ENTRIES];
|
||||
AgiDir dirPic[MAX_DIRECTORY_ENTRIES];
|
||||
AgiDir dirView[MAX_DIRECTORY_ENTRIES];
|
||||
AgiDir dirSound[MAX_DIRECTORY_ENTRIES];
|
||||
|
||||
// resources
|
||||
AgiPicture pictures[MAX_DIRECTORY_ENTRIES]; /**< AGI picture resources */
|
||||
AgiLogic logics[MAX_DIRECTORY_ENTRIES]; /**< AGI logic resources */
|
||||
AgiView views[MAX_DIRECTORY_ENTRIES]; /**< AGI view resources */
|
||||
AgiSound *sounds[MAX_DIRECTORY_ENTRIES]; /**< Pointers to AGI sound resources */
|
||||
|
||||
AgiLogic *_curLogic;
|
||||
|
||||
// view table
|
||||
ScreenObjEntry screenObjTable[SCREENOBJECTS_MAX];
|
||||
|
||||
ScreenObjEntry addToPicView;
|
||||
|
||||
bool automaticSave; /**< set by CmdSetSimple() */
|
||||
char automaticSaveDescription[SAVEDGAME_DESCRIPTION_LEN + 1];
|
||||
|
||||
Common::Rect mouseFence; /**< rectangle set by fence.mouse command */
|
||||
bool mouseEnabled; /**< if mouse is supposed to be active */
|
||||
bool mouseHidden; /**< if mouse is currently hidden */
|
||||
bool predictiveDlgOnMouseClick; /**< if predictive dialog is enabled for mouse clicks */
|
||||
|
||||
// IF condition handling
|
||||
bool testResult;
|
||||
|
||||
int max_logics;
|
||||
int logic_list[256];
|
||||
|
||||
// used to detect situations, where the game shows some text and changes rooms right afterwards
|
||||
// for example Space Quest 2 intro right at the start
|
||||
// or Space Quest 2, when entering the vent also right at the start
|
||||
// The developers assumed that loading the new room would take a bit.
|
||||
// In ScummVM it's basically done in an instant, which means that
|
||||
// the text would only get shown for a split second.
|
||||
// We delay a bit as soon as such situations get detected.
|
||||
bool nonBlockingTextShown;
|
||||
int16 nonBlockingTextCyclesLeft;
|
||||
|
||||
bool automaticRestoreGame;
|
||||
|
||||
byte speedLevel; /**< Current game speed for certain platforms/versions */
|
||||
|
||||
uint16 appleIIgsSpeedControllerSlot;
|
||||
|
||||
const char *getString(int number);
|
||||
void setString(int number, const char *str);
|
||||
/**
|
||||
* Sets the speed level and displays a message box.
|
||||
*/
|
||||
void setSpeedLevel(byte s);
|
||||
|
||||
AgiGame() {
|
||||
_vm = nullptr;
|
||||
|
||||
adjMouseX = 0;
|
||||
adjMouseY = 0;
|
||||
|
||||
memset(id, 0, sizeof(id));
|
||||
crc = 0;
|
||||
memset(flags, 0, sizeof(flags));
|
||||
memset(vars, 0, sizeof(vars));
|
||||
|
||||
horizon = 0;
|
||||
|
||||
cycleInnerLoopActive = false;
|
||||
cycleInnerLoopType = 0;
|
||||
|
||||
curLogicNr = 0;
|
||||
|
||||
// execStack is defaulted by Common::Array constructor
|
||||
|
||||
playerControl = false;
|
||||
exitAllLogics = false;
|
||||
pictureShown = false;
|
||||
|
||||
// block defaulted by AgiBlock constructor
|
||||
|
||||
gfxMode = false;
|
||||
|
||||
numObjects = 0;
|
||||
|
||||
memset(controllerOccurred, 0, sizeof(controllerOccurred));
|
||||
|
||||
// controllerKeyMapping defaulted by AgiControllerKeyMapping constructor
|
||||
|
||||
memset(strings, 0, sizeof(strings));
|
||||
|
||||
// dirLogic cleared by AgiDir constructor
|
||||
// dirPic cleared by AgiDir constructor
|
||||
// dirView cleared by AgiDir constructor
|
||||
// dirSound cleared by AgiDir constructor
|
||||
|
||||
// pictures cleared by AgiPicture constructor
|
||||
// logics cleared by AgiLogic constructor
|
||||
// views cleared by AgiView constructor
|
||||
memset(sounds, 0, sizeof(sounds));
|
||||
|
||||
_curLogic = nullptr;
|
||||
|
||||
// screenObjTable cleared by ScreenObjEntry constructor
|
||||
|
||||
// addToPicView cleared by ScreenObjEntry constructor
|
||||
|
||||
automaticSave = false;
|
||||
memset(automaticSaveDescription, 0, sizeof(automaticSaveDescription));
|
||||
|
||||
// mouseFence cleared by Common::Rect constructor
|
||||
mouseEnabled = false;
|
||||
mouseHidden = false;
|
||||
predictiveDlgOnMouseClick = false;
|
||||
|
||||
testResult = false;
|
||||
|
||||
max_logics = 0;
|
||||
memset(logic_list, 0, sizeof(logic_list));
|
||||
|
||||
nonBlockingTextShown = false;
|
||||
nonBlockingTextCyclesLeft = 0;
|
||||
|
||||
automaticRestoreGame = false;
|
||||
|
||||
speedLevel = 2; // normal speed
|
||||
|
||||
appleIIgsSpeedControllerSlot = 0xffff; // we didn't add yet speed menu
|
||||
}
|
||||
};
|
||||
|
||||
class AgiLoader;
|
||||
class GfxFont;
|
||||
class GfxMgr;
|
||||
class SpritesMgr;
|
||||
class InventoryMgr;
|
||||
class TextMgr;
|
||||
class GfxMenu;
|
||||
class SystemUI;
|
||||
class Words;
|
||||
struct AGIGameDescription;
|
||||
|
||||
// Image stack support
|
||||
struct ImageStackElement {
|
||||
uint8 type;
|
||||
uint8 pad;
|
||||
int16 parm1;
|
||||
int16 parm2;
|
||||
int16 parm3;
|
||||
int16 parm4;
|
||||
int16 parm5;
|
||||
int16 parm6;
|
||||
int16 parm7;
|
||||
};
|
||||
|
||||
#define TICK_SECONDS 20
|
||||
|
||||
#define KEY_QUEUE_SIZE 16
|
||||
|
||||
class AgiBase : public ::Engine {
|
||||
protected:
|
||||
// Engine API
|
||||
Common::Error init();
|
||||
virtual Common::Error go() = 0;
|
||||
Common::Error run() override {
|
||||
Common::Error err;
|
||||
err = init();
|
||||
if (err.getCode() != Common::kNoError)
|
||||
return err;
|
||||
return go();
|
||||
}
|
||||
bool hasFeature(EngineFeature f) const override;
|
||||
|
||||
virtual void initialize() = 0;
|
||||
|
||||
void initRenderMode();
|
||||
|
||||
public:
|
||||
Words *_words;
|
||||
|
||||
GfxFont *_font;
|
||||
GfxMgr *_gfx;
|
||||
|
||||
Common::RenderMode _renderMode;
|
||||
AgiDebug _debug;
|
||||
AgiGame _game;
|
||||
Common::RandomSource *_rnd;
|
||||
|
||||
SoundMgr *_sound;
|
||||
|
||||
Mouse _mouse;
|
||||
|
||||
bool _noSaveLoadAllowed;
|
||||
|
||||
virtual bool promptIsEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual int getKeypress() = 0;
|
||||
virtual bool isKeypress() = 0;
|
||||
virtual void clearKeyQueue() = 0;
|
||||
|
||||
AgiBase(OSystem *syst, const AGIGameDescription *gameDesc);
|
||||
~AgiBase() override;
|
||||
|
||||
virtual void clearImageStack() = 0;
|
||||
virtual void recordImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3,
|
||||
int16 p4, int16 p5, int16 p6, int16 p7) = 0;
|
||||
virtual void replayImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3,
|
||||
int16 p4, int16 p5, int16 p6, int16 p7) = 0;
|
||||
virtual void releaseImageStack() = 0;
|
||||
|
||||
int _soundemu;
|
||||
|
||||
bool getFlag(int16 flagNr);
|
||||
void setFlag(int16 flagNr, bool newState);
|
||||
void flipFlag(int16 flagNr);
|
||||
/** Sets a flag in AGIv2+, sets a variable in AGIv1 */
|
||||
void setFlagOrVar(int16 flagNr, bool newState);
|
||||
|
||||
const AGIGameDescription *_gameDescription;
|
||||
|
||||
uint32 _gameFeatures;
|
||||
uint16 _gameVersion;
|
||||
|
||||
uint32 getGameID() const;
|
||||
uint32 getFeatures() const;
|
||||
uint16 getVersion() const;
|
||||
uint16 getGameType() const;
|
||||
Common::Language getLanguage() const;
|
||||
bool isLanguageRTL() const;
|
||||
Common::Platform getPlatform() const;
|
||||
const char *getGameMD5() const;
|
||||
void initFeatures();
|
||||
void initVersion();
|
||||
|
||||
const char *getDiskName(uint16 id);
|
||||
|
||||
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
|
||||
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
|
||||
|
||||
const byte *getFontData();
|
||||
|
||||
void cycleInnerLoopActive(int16 loopType) {
|
||||
_game.cycleInnerLoopActive = true;
|
||||
_game.cycleInnerLoopType = loopType;
|
||||
};
|
||||
void cycleInnerLoopInactive() {
|
||||
_game.cycleInnerLoopActive = false;
|
||||
};
|
||||
bool cycleInnerLoopIsActive() {
|
||||
return _game.cycleInnerLoopActive;
|
||||
}
|
||||
};
|
||||
|
||||
enum AgiArtificialDelayTriggerType {
|
||||
ARTIFICIALDELAYTYPE_NEWROOM = 0,
|
||||
ARTIFICIALDELAYTYPE_NEWPICTURE = 1,
|
||||
ARTIFICIALDELAYTYPE_END = -1
|
||||
};
|
||||
|
||||
struct AgiArtificialDelayEntry {
|
||||
uint32 gameId;
|
||||
Common::Platform platform;
|
||||
AgiArtificialDelayTriggerType triggerType;
|
||||
int16 orgNr;
|
||||
int16 newNr;
|
||||
uint16 millisecondsDelay;
|
||||
};
|
||||
|
||||
typedef void (*AgiOpCodeFunction)(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
|
||||
struct AgiOpCodeEntry {
|
||||
const char *name;
|
||||
const char *parameters;
|
||||
AgiOpCodeFunction functionPtr;
|
||||
uint16 parameterSize;
|
||||
};
|
||||
|
||||
struct AgiOpCodeDefinitionEntry {
|
||||
const char *name;
|
||||
const char *parameters;
|
||||
AgiOpCodeFunction functionPtr;
|
||||
};
|
||||
|
||||
class AgiEngine : public AgiBase {
|
||||
protected:
|
||||
// Engine APIs
|
||||
Common::Error go() override;
|
||||
|
||||
void initialize() override;
|
||||
|
||||
public:
|
||||
AgiEngine(OSystem *syst, const AGIGameDescription *gameDesc);
|
||||
~AgiEngine() override;
|
||||
|
||||
bool promptIsEnabled() override;
|
||||
|
||||
Common::Error loadGameState(int slot) override;
|
||||
Common::Error saveGameState(int slot, const Common::String &description, bool isAutosave = false) override;
|
||||
|
||||
private:
|
||||
int _keyQueue[KEY_QUEUE_SIZE];
|
||||
int _keyQueueStart;
|
||||
int _keyQueueEnd;
|
||||
|
||||
bool _allowSynthetic;
|
||||
|
||||
bool checkPriority(ScreenObjEntry *v);
|
||||
bool checkCollision(ScreenObjEntry *v);
|
||||
bool checkPosition(ScreenObjEntry *v);
|
||||
|
||||
int _firstSlot;
|
||||
|
||||
public:
|
||||
Common::Array<AgiObject> _objects; // objects in the game
|
||||
|
||||
SavedGameSlotIdArray getSavegameSlotIds();
|
||||
bool getSavegameInformation(int16 slotId, Common::String &saveDescription, uint32 &saveDate, uint32 &saveTime, bool &saveIsValid);
|
||||
|
||||
int saveGame(const Common::String &fileName, const Common::String &descriptionString);
|
||||
int loadGame(const Common::String &fileName, bool checkId = true);
|
||||
bool saveGameDialog();
|
||||
bool saveGameAutomatic();
|
||||
bool loadGameDialog();
|
||||
bool loadGameAutomatic();
|
||||
int doSave(int slot, const Common::String &desc);
|
||||
int doLoad(int slot, bool showMessages);
|
||||
int scummVMSaveLoadDialog(bool isSave);
|
||||
|
||||
uint8 *_intobj;
|
||||
bool _restartGame;
|
||||
|
||||
SpritesMgr *_sprites;
|
||||
TextMgr *_text;
|
||||
InventoryMgr *_inventory;
|
||||
PictureMgr *_picture;
|
||||
AgiLoader *_loader;
|
||||
GfxMenu *_menu;
|
||||
SystemUI *_systemUI;
|
||||
Common::DumpFile *_logFile; // File used for the log() agi command.
|
||||
|
||||
Common::Stack<ImageStackElement> _imageStack;
|
||||
|
||||
#ifdef USE_TTS
|
||||
int16 _previousDisplayRow;
|
||||
Common::String _combinedText;
|
||||
Common::String _previousSaid;
|
||||
bool _queueNextText;
|
||||
bool _voiceClock;
|
||||
bool _replaceDisplayNewlines;
|
||||
Common::CodePage _ttsEncoding;
|
||||
#endif
|
||||
|
||||
void clearImageStack() override;
|
||||
void recordImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3,
|
||||
int16 p4, int16 p5, int16 p6, int16 p7) override;
|
||||
void replayImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3,
|
||||
int16 p4, int16 p5, int16 p6, int16 p7) override;
|
||||
void releaseImageStack() override;
|
||||
|
||||
void wait(uint32 msec, bool busy = false);
|
||||
|
||||
int agiInit();
|
||||
void agiDeinit();
|
||||
int loadResource(int16 resourceType, int16 resourceNr);
|
||||
void unloadResource(int16 resourceType, int16 resourceNr);
|
||||
/**
|
||||
* Unload all resources except Logic 0
|
||||
*/
|
||||
void unloadResources();
|
||||
|
||||
int getKeypress() override;
|
||||
bool isKeypress() override;
|
||||
void clearKeyQueue() override;
|
||||
|
||||
byte getVar(int16 varNr);
|
||||
void setVar(int16 varNr, byte newValue);
|
||||
|
||||
private:
|
||||
void applyVolumeToMixer();
|
||||
|
||||
public:
|
||||
void syncSoundSettings() override;
|
||||
|
||||
#ifdef USE_TTS
|
||||
void sayText(const Common::String &text, Common::TextToSpeechManager::Action action = Common::TextToSpeechManager::QUEUE,
|
||||
bool checkPreviousSaid = true);
|
||||
void stopTextToSpeech(bool clearPreviousSaid = true);
|
||||
#endif
|
||||
|
||||
public:
|
||||
void decrypt(uint8 *mem, int len);
|
||||
uint16 processAGIEvents();
|
||||
int runGame();
|
||||
|
||||
void newRoom(int16 newRoomNr);
|
||||
void resetControllers();
|
||||
void interpretCycle();
|
||||
void playGame();
|
||||
|
||||
void allowSynthetic(bool);
|
||||
void processScummVMEvents();
|
||||
void checkQuickLoad();
|
||||
|
||||
const Common::String getTargetName() const { return _targetName; }
|
||||
|
||||
private:
|
||||
byte getAppleIIgsTimeDelay(const AgiAppleIIgsDelayOverwriteGameEntry *appleIIgsDelayOverwrite, byte &newTimeDelay) const;
|
||||
|
||||
// Objects
|
||||
public:
|
||||
int loadObjects(const char *fname);
|
||||
int loadObjects(Common::SeekableReadStream &fp, int flen);
|
||||
const char *objectName(uint16 objectNr);
|
||||
int objectGetLocation(uint16 objectNr);
|
||||
void objectSetLocation(uint16 objectNr, int location);
|
||||
private:
|
||||
int decodeObjects(uint8 *mem, uint32 flen);
|
||||
|
||||
// Logic
|
||||
public:
|
||||
int decodeLogic(int16 logicNr);
|
||||
void unloadLogic(int16 logicNr);
|
||||
int runLogic(int16 logicNr);
|
||||
void debugConsole(int lognum, int mode, const char *str);
|
||||
bool testIfCode(int16 logicNr);
|
||||
void executeAgiCommand(uint8 op, uint8 *p);
|
||||
|
||||
private:
|
||||
bool _veryFirstInitialCycle; /**< signals, that currently the very first cycle is executed (restarts, etc. do not count!) */
|
||||
uint32 _instructionCounter; /**< counts every instruction, that got executed, can wrap around */
|
||||
|
||||
bool _setVolumeBrokenFangame;
|
||||
|
||||
void resetGetVarSecondsHeuristic();
|
||||
void getVarSecondsHeuristicTrigger();
|
||||
uint32 _getVarSecondsHeuristicLastInstructionCounter; /**< last time VM_VAR_SECONDS were read */
|
||||
uint16 _getVarSecondsHeuristicCounter; /**< how many times heuristic was triggered */
|
||||
|
||||
uint32 _playTimeInSecondsAdjust; /**< milliseconds to adjust for calculating current play time in seconds, see setVarSecondsTrigger() */
|
||||
|
||||
void setVarSecondsTrigger(byte newSeconds);
|
||||
|
||||
public:
|
||||
// Some submethods of testIfCode
|
||||
void skipInstruction(byte op);
|
||||
void skipInstructionsUntil(byte v);
|
||||
bool testObjRight(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2);
|
||||
bool testObjCenter(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2);
|
||||
bool testObjInBox(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2);
|
||||
bool testPosn(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2);
|
||||
bool testSaid(uint8 nwords, uint8 *cc);
|
||||
bool testController(uint8 cont);
|
||||
bool testCompareStrings(uint8 s1, uint8 s2);
|
||||
|
||||
// Picture
|
||||
private:
|
||||
void unloadPicture(int16 picNr);
|
||||
|
||||
// View
|
||||
private:
|
||||
void updateView(ScreenObjEntry *screenObj);
|
||||
|
||||
public:
|
||||
void setView(ScreenObjEntry *screenObj, int16 viewNr);
|
||||
void setLoop(ScreenObjEntry *screenObj, int16 loopNr);
|
||||
void setCel(ScreenObjEntry *screenObj, int16 celNr);
|
||||
|
||||
void clipViewCoordinates(ScreenObjEntry *screenObj);
|
||||
|
||||
void startUpdate(ScreenObjEntry *viewPtr);
|
||||
void stopUpdate(ScreenObjEntry *viewPtr);
|
||||
void updateScreenObjTable();
|
||||
void unloadView(int16 viewNr);
|
||||
int decodeView(byte *resourceData, uint16 resourceSize, int16 viewNr);
|
||||
|
||||
private:
|
||||
void unpackViewCelData(AgiViewCel *celData, byte *compressedData, uint16 compressedSize, int16 viewNr);
|
||||
void unpackViewCelDataAGI256(AgiViewCel *celData, byte *compressedData, uint16 compressedSize, int16 viewNr);
|
||||
|
||||
public:
|
||||
bool isEgoView(const ScreenObjEntry *screenObj);
|
||||
|
||||
// Motion
|
||||
private:
|
||||
int checkStep(int delta, int step);
|
||||
bool checkBlock(int16 x, int16 y);
|
||||
void changePos(ScreenObjEntry *screenObj);
|
||||
void motionWander(ScreenObjEntry *screenObj);
|
||||
void motionFollowEgo(ScreenObjEntry *screenObj);
|
||||
void motionMoveObj(ScreenObjEntry *screenObj);
|
||||
void motionMoveObjStop(ScreenObjEntry *screenObj);
|
||||
void checkMotion(ScreenObjEntry *screenObj);
|
||||
|
||||
public:
|
||||
void motionActivated(ScreenObjEntry *screenObj);
|
||||
void cyclerActivated(ScreenObjEntry *screenObj);
|
||||
void checkAllMotions();
|
||||
void moveObj(ScreenObjEntry *screenObj);
|
||||
void inDestination(ScreenObjEntry *screenObj);
|
||||
void fixPosition(int16 screenObjNr);
|
||||
void fixPosition(ScreenObjEntry *screenObj);
|
||||
void updatePosition();
|
||||
int getDirection(int16 objX, int16 objY, int16 destX, int16 destY, int16 stepSize);
|
||||
byte egoNearWater(byte limit);
|
||||
int16 nearWater(ScreenObjEntry &screenObj, byte direction, int16 x, int16 y, byte limit);
|
||||
|
||||
bool _keyHoldMode;
|
||||
Common::KeyCode _keyHoldModeLastKey;
|
||||
|
||||
// Keyboard
|
||||
int doPollKeyboard();
|
||||
|
||||
bool handleMouseClicks(uint16 &key);
|
||||
bool handleController(uint16 key);
|
||||
|
||||
bool showPredictiveDialog();
|
||||
|
||||
int waitKey();
|
||||
int waitAnyKey();
|
||||
void waitAnyKeyOrFinishedSound();
|
||||
|
||||
void nonBlockingText_IsShown();
|
||||
void nonBlockingText_Forget();
|
||||
|
||||
void artificialDelay_Reset();
|
||||
void artificialDelay_CycleDone();
|
||||
|
||||
uint16 artificialDelay_SearchTable(AgiArtificialDelayTriggerType triggerType, int16 orgNr, int16 newNr);
|
||||
|
||||
void artificialDelayTrigger_NewRoom(int16 newRoomNr);
|
||||
void artificialDelayTrigger_DrawPicture(int16 newPictureNr);
|
||||
|
||||
private:
|
||||
int16 _artificialDelayCurrentRoom;
|
||||
int16 _artificialDelayCurrentPicture;
|
||||
|
||||
public:
|
||||
void redrawScreen();
|
||||
|
||||
void inGameTimerReset(uint32 newPlayTime = 0);
|
||||
void inGameTimerResetPassedCycles();
|
||||
uint32 inGameTimerGet();
|
||||
uint32 inGameTimerGetPassedCycles();
|
||||
|
||||
void inGameTimerUpdate();
|
||||
|
||||
private:
|
||||
uint32 _lastUsedPlayTimeInCycles; // 40 per second
|
||||
uint32 _lastUsedPlayTimeInSeconds; // actual seconds
|
||||
uint32 _passedPlayTimeCycles; // increased by 1 every time we passed a cycle
|
||||
|
||||
private:
|
||||
AgiOpCodeEntry _opCodes[256]; // always keep those at 256, so that there is no way for invalid memory access
|
||||
AgiOpCodeEntry _opCodesCond[256];
|
||||
|
||||
void setupOpCodes(uint16 version);
|
||||
|
||||
public:
|
||||
const AgiOpCodeEntry *getOpCodesTable() { return _opCodes; }
|
||||
|
||||
private:
|
||||
void goldRushClockTimeWorkaround_OnReadVar();
|
||||
void goldRushClockTimeWorkaround_OnWriteVar(byte oldValue);
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_AGI_H */
|
||||
110
engines/agi/appleIIgs_timedelay_overwrite.h
Normal file
110
engines/agi/appleIIgs_timedelay_overwrite.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_APPLEIIGS_DELAY_OVERWRITE_H
|
||||
#define AGI_APPLEIIGS_DELAY_OVERWRITE_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
struct AgiAppleIIgsDelayOverwriteRoomEntry {
|
||||
int16 fromRoom;
|
||||
int16 toRoom;
|
||||
int16 activePictureNr; // resource number of current active background picture
|
||||
int16 timeDelayOverwrite; // time delay here is like on PC
|
||||
// so 0 - unlimited, 1 - 20 cycles, 2 - 10 cycles, -1 means do not touch speed set by scripts
|
||||
bool onlyWhenPlayerNotInControl; // only sets speed, when play is not in control
|
||||
};
|
||||
|
||||
struct AgiAppleIIgsDelayOverwriteGameEntry {
|
||||
uint32 gameId;
|
||||
int16 defaultTimeDelayOverwrite; // time delay here is like on PC
|
||||
// so 0 - unlimited, 1 - 20 cycles, 2 - 10 cycles, -1 means do not touch speed set by scripts
|
||||
const AgiAppleIIgsDelayOverwriteRoomEntry *roomTable; // pointer to room table (optional)
|
||||
};
|
||||
|
||||
static const AgiAppleIIgsDelayOverwriteRoomEntry appleIIgsDelayOverwriteKQ4[] = {
|
||||
{ 120, 121, -1, -1, true }, // Part of the intro: Graham gets his hat, throws it and breaks down, don't touch speed (3 is set)
|
||||
{ 128, 128, -1, -1, true }, // Part of the intro: first actual room for gameplay, but during intro, don't touch speed (3 is set)
|
||||
{ 92, 92, -1, -1, true }, // Part of caught by gargoyle w/ Lolotte cutscene (3 is set)
|
||||
// room 54 sets the speed for a short time to 3 right after entering "clean" command. It doesn't seem to hurt that we switch it down to 2
|
||||
// room 92 is dual use, part of cutscenes, part of gameplay, that's why we only stop touching it, when player is not in control
|
||||
{ 135, 135, -1, -1, true }, // Part of ending cutscene. Don't touch speed (3 is set)
|
||||
{ -1, -1, -1, -1, false }
|
||||
};
|
||||
|
||||
static const AgiAppleIIgsDelayOverwriteRoomEntry appleIIgsDelayOverwriteMH1[] = {
|
||||
//{ 153, 153, -1, 2, false }, // Intro w/ credits
|
||||
//{ 104, 104, -1, 2, false }, // Intro cutscene
|
||||
//{ 117, 117, -1, 2, false }, // Intro cutscene (ego waking up)
|
||||
{ 114, 114, -1, -1, false }, // interactive MAD map
|
||||
{ 124, 125, -1, -1, false }, // MAD during intro (tracking), seem to work properly at given speed
|
||||
{ 132, 133, -1, -1, false }, // MAD day 2 intro (tracking)
|
||||
{ 137, 137, 17, -1, false }, // Night Club 4th arcade game - game sets speed to 7, needs to run that slow to be playable
|
||||
{ 137, 137, -1, 4, false }, // Night Club first few arcade games - game sets speed to 0, we need to fix it
|
||||
{ 115, 116, -1, -1, false }, // MAD day 3 intro (tracking)
|
||||
{ 148, 148, -1, -1, false }, // day 3: arcade sequence under pawn shop (game sets speed to 6)
|
||||
{ 103, 103, -1, -1, false }, // MAD day 4 intro (tracking)
|
||||
{ 105, 105, -1, -1, false }, // day 4 tracking mini game right at the start (game sets speed to 3)
|
||||
{ 107, 107, -1, -1, false }, // MAD day 4 intro (tracking)
|
||||
{ 112, 112, -1, -1, false }, // MAD day 4 intro (tracking)
|
||||
{ -1, -1, -1, -1, false }
|
||||
};
|
||||
|
||||
static const AgiAppleIIgsDelayOverwriteRoomEntry appleIIgsDelayOverwriteMIXEDUP[] = {
|
||||
{ 1, 1, -1, 3, true }, // Jack and Jill cutscene (issue 11210)
|
||||
{ -1, -1, -1, -1, false }
|
||||
};
|
||||
|
||||
|
||||
static const AgiAppleIIgsDelayOverwriteRoomEntry appleIIgsDelayOverwriteSQ2[] = {
|
||||
{ 1, 1, -1, -1, false }, // Intro: space ship entering space port, don't touch speed
|
||||
{ -1, -1, -1, -1, false }
|
||||
};
|
||||
|
||||
static const AgiAppleIIgsDelayOverwriteGameEntry appleIIgsDelayOverwriteGameTable[] = {
|
||||
{ GID_BC, 2, nullptr }, // sets the speed at the start and doesn't modify it
|
||||
{ GID_GOLDRUSH, 2, nullptr },
|
||||
{ GID_KQ1, 2, nullptr },
|
||||
// KQ2 seems to work fine at speed given by scripts
|
||||
{ GID_KQ3, 2, nullptr },
|
||||
{ GID_KQ4, 2, appleIIgsDelayOverwriteKQ4 },
|
||||
{ GID_LSL1, 2, nullptr }, // Switch Larry 1 to 10 cycles per second (that's around PC Larry 1's "normal" speed
|
||||
{ GID_MH1, 2, appleIIgsDelayOverwriteMH1 },
|
||||
{ GID_MIXEDUP, 2, appleIIgsDelayOverwriteMIXEDUP },
|
||||
{ GID_PQ1, 2, nullptr },
|
||||
{ GID_SQ1, 2, nullptr }, // completed, no issues using these settings
|
||||
{ GID_SQ2, 2, appleIIgsDelayOverwriteSQ2 },
|
||||
{ GID_AGIDEMO, -1, nullptr }
|
||||
};
|
||||
|
||||
static const AgiAppleIIgsDelayOverwriteGameEntry *getAppleIIgsDelayOverwriteGameEntry(uint32 gameId) {
|
||||
const AgiAppleIIgsDelayOverwriteGameEntry *appleIIgsDelayOverwrite = appleIIgsDelayOverwriteGameTable;
|
||||
while (appleIIgsDelayOverwrite->gameId != GID_AGIDEMO) {
|
||||
if (appleIIgsDelayOverwrite->gameId == gameId)
|
||||
break; // game found
|
||||
appleIIgsDelayOverwrite++;
|
||||
}
|
||||
return appleIIgsDelayOverwrite;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_APPLEIIGS_DELAY_OVERWRITE_H */
|
||||
495
engines/agi/checks.cpp
Normal file
495
engines/agi/checks.cpp
Normal file
@@ -0,0 +1,495 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
bool AgiEngine::checkPosition(ScreenObjEntry *screenObj) {
|
||||
bool result = true; // position is fine
|
||||
debugC(4, kDebugLevelSprites, "check position @ %d, %d", screenObj->xPos, screenObj->yPos);
|
||||
|
||||
if (screenObj->xPos < 0) {
|
||||
result = false;
|
||||
} else {
|
||||
if (screenObj->xPos + screenObj->xSize > SCRIPT_WIDTH) {
|
||||
result = false;
|
||||
} else {
|
||||
if (screenObj->yPos - screenObj->ySize < -1) {
|
||||
result = false;
|
||||
} else {
|
||||
if (screenObj->yPos >= SCRIPT_HEIGHT) {
|
||||
result = false;
|
||||
} else {
|
||||
if (((!(screenObj->flags & fIgnoreHorizon)) && screenObj->yPos <= _game.horizon)) {
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MH1 needs this, but it breaks LSL1
|
||||
// TODO: *NOT* in disassembly of AGI3 .149, why was this needed?
|
||||
// if (getVersion() >= 0x3000) {
|
||||
// if (screenObj->yPos < screenObj->ySize)
|
||||
// result = false;
|
||||
// }
|
||||
|
||||
if (!result) {
|
||||
debugC(4, kDebugLevelSprites, "check position failed: x=%d, y=%d, h=%d, w=%d",
|
||||
screenObj->xPos, screenObj->yPos, screenObj->xSize, screenObj->ySize);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's another object on the way
|
||||
*/
|
||||
bool AgiEngine::checkCollision(ScreenObjEntry *screenObj) {
|
||||
ScreenObjEntry *checkObj;
|
||||
|
||||
if (screenObj->flags & fIgnoreObjects)
|
||||
return false;
|
||||
|
||||
for (checkObj = _game.screenObjTable; checkObj < &_game.screenObjTable[SCREENOBJECTS_MAX]; checkObj++) {
|
||||
if ((checkObj->flags & (fAnimated | fDrawn)) != (fAnimated | fDrawn))
|
||||
continue;
|
||||
|
||||
if (checkObj->flags & fIgnoreObjects)
|
||||
continue;
|
||||
|
||||
// Same object, check next
|
||||
if (screenObj->objectNr == checkObj->objectNr)
|
||||
continue;
|
||||
|
||||
// No horizontal overlap, check next
|
||||
if (screenObj->xPos + screenObj->xSize < checkObj->xPos || screenObj->xPos > checkObj->xPos + checkObj->xSize)
|
||||
continue;
|
||||
|
||||
// Same y, return error!
|
||||
if (screenObj->yPos == checkObj->yPos) {
|
||||
debugC(4, kDebugLevelSprites, "check returns 1 (object %d)", screenObj->objectNr);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Crossed the baseline, return error!
|
||||
if ((screenObj->yPos > checkObj->yPos && screenObj->yPos_prev < checkObj->yPos_prev) ||
|
||||
(screenObj->yPos < checkObj->yPos && screenObj->yPos_prev > checkObj->yPos_prev)) {
|
||||
debugC(4, kDebugLevelSprites, "check returns 1 (object %d)", screenObj->objectNr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AgiEngine::checkPriority(ScreenObjEntry *screenObj) {
|
||||
bool touchedWater = false;
|
||||
bool touchedTrigger = false;
|
||||
bool touchedControl = true;
|
||||
|
||||
if (!(screenObj->flags & fFixedPriority)) {
|
||||
// Priority bands
|
||||
screenObj->priority = _gfx->priorityFromY(screenObj->yPos);
|
||||
}
|
||||
|
||||
if (screenObj->priority != 0x0f) {
|
||||
touchedWater = true;
|
||||
|
||||
int16 curX = screenObj->xPos;
|
||||
int16 curY = screenObj->yPos;
|
||||
|
||||
for (int16 celX = 0; celX < screenObj->xSize; celX++, curX++) {
|
||||
byte screenPriority = _gfx->getPriority(curX, curY);
|
||||
|
||||
if (screenPriority == 0) { // unconditional black. no go at all!
|
||||
touchedControl = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (screenPriority != 3) { // not water surface
|
||||
touchedWater = false;
|
||||
|
||||
if (screenPriority == 1) { // conditional blue
|
||||
if (!(screenObj->flags & fIgnoreBlocks)) {
|
||||
debugC(4, kDebugLevelSprites, "Blocks observed!");
|
||||
touchedControl = false;
|
||||
break;
|
||||
}
|
||||
} else if (screenPriority == 2) {
|
||||
debugC(4, kDebugLevelSprites, "stepped on trigger");
|
||||
if (!_debug.ignoretriggers)
|
||||
touchedTrigger = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (touchedControl) {
|
||||
if (!touchedWater) {
|
||||
if (screenObj->flags & fOnWater)
|
||||
touchedControl = false;
|
||||
} else {
|
||||
if (screenObj->flags & fOnLand)
|
||||
touchedControl = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check ego
|
||||
if (screenObj->objectNr == 0) {
|
||||
setFlag(VM_FLAG_EGO_TOUCHED_P2, touchedTrigger);
|
||||
setFlag(VM_FLAG_EGO_WATER, touchedWater);
|
||||
|
||||
// WORKAROUND: KQ3 infinite falling, bug #13379
|
||||
// Falling off of the ladder in room 22 or the stairs in room 64 can
|
||||
// cause ego to fall forever in place. In both rooms, and possibly
|
||||
// others, an unrelated black priority line overlaps with fall paths.
|
||||
// This also occurs in the original. Ignore these lines when falling.
|
||||
if (!touchedControl && getGameID() == GID_KQ3 && screenObj->currentViewNr == 11) {
|
||||
touchedControl = true;
|
||||
}
|
||||
}
|
||||
|
||||
return touchedControl;
|
||||
}
|
||||
|
||||
/*
|
||||
* Public functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update position of objects
|
||||
* This function updates the position of all animated, updating view
|
||||
* table entries according to its motion type, step size, etc. The
|
||||
* new position must be valid according to the sprite positioning
|
||||
* rules, otherwise the previous position will be kept.
|
||||
*/
|
||||
void AgiEngine::updatePosition() {
|
||||
setVar(VM_VAR_BORDER_CODE, 0);
|
||||
setVar(VM_VAR_BORDER_TOUCH_EGO, 0);
|
||||
setVar(VM_VAR_BORDER_TOUCH_OBJECT, 0);
|
||||
|
||||
ScreenObjEntry *screenObj;
|
||||
for (screenObj = _game.screenObjTable; screenObj < &_game.screenObjTable[SCREENOBJECTS_MAX]; screenObj++) {
|
||||
if ((screenObj->flags & (fAnimated | fUpdate | fDrawn)) != (fAnimated | fUpdate | fDrawn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (screenObj->stepTimeCount > 1) {
|
||||
screenObj->stepTimeCount--;
|
||||
continue;
|
||||
}
|
||||
|
||||
screenObj->stepTimeCount = screenObj->stepTime;
|
||||
|
||||
int x = screenObj->xPos;
|
||||
int oldX = x;
|
||||
int y = screenObj->yPos;
|
||||
int oldY = y;
|
||||
|
||||
// If object has moved, update its position
|
||||
if (!(screenObj->flags & fUpdatePos)) {
|
||||
const int dx[9] = { 0, 0, 1, 1, 1, 0, -1, -1, -1 };
|
||||
const int dy[9] = { 0, -1, -1, 0, 1, 1, 1, 0, -1 };
|
||||
x += screenObj->stepSize * dx[screenObj->direction];
|
||||
y += screenObj->stepSize * dy[screenObj->direction];
|
||||
}
|
||||
|
||||
// Now check if it touched the borders
|
||||
int border = 0;
|
||||
|
||||
// Check left/right borders
|
||||
if (getVersion() == 0x3086) {
|
||||
// KQ4 interpreter does a different comparison on x
|
||||
// The interpreter before (2.917) and after that (3.098) don't do them that way
|
||||
// This difference is required for at least Sarien bug #192
|
||||
// KQ4: room 135, hen moves from the center of the screen to the left border,
|
||||
// but it doesn't disappear.
|
||||
if (x <= 0) {
|
||||
x = 0;
|
||||
border = 4;
|
||||
}
|
||||
} else {
|
||||
// regular comparison
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
border = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// } else if (v->entry == 0 && x == 0 && v->flags & fAdjEgoXY) { // should not be required
|
||||
// // Extra test to walk west clicking the mouse
|
||||
// x = 0;
|
||||
// border = 4;
|
||||
|
||||
if (!border) {
|
||||
if (x + screenObj->xSize > SCRIPT_WIDTH) {
|
||||
x = SCRIPT_WIDTH - screenObj->xSize;
|
||||
border = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Check top/bottom borders.
|
||||
if (y - screenObj->ySize < -1) {
|
||||
y = screenObj->ySize - 1;
|
||||
border = 1;
|
||||
} else if (y > SCRIPT_HEIGHT - 1) {
|
||||
y = SCRIPT_HEIGHT - 1;
|
||||
border = 3;
|
||||
} else if ((!(screenObj->flags & fIgnoreHorizon)) && y <= _game.horizon) {
|
||||
debugC(4, kDebugLevelSprites, "y = %d, horizon = %d", y, _game.horizon);
|
||||
y = _game.horizon + 1;
|
||||
border = 1;
|
||||
}
|
||||
|
||||
// Test new position. rollback if test fails
|
||||
screenObj->xPos = x;
|
||||
screenObj->yPos = y;
|
||||
if (checkCollision(screenObj) || !checkPriority(screenObj)) {
|
||||
screenObj->xPos = oldX;
|
||||
screenObj->yPos = oldY;
|
||||
border = 0;
|
||||
fixPosition(screenObj->objectNr);
|
||||
}
|
||||
|
||||
if (border) {
|
||||
if (isEgoView(screenObj)) {
|
||||
setVar(VM_VAR_BORDER_TOUCH_EGO, border);
|
||||
} else {
|
||||
setVar(VM_VAR_BORDER_CODE, screenObj->objectNr);
|
||||
setVar(VM_VAR_BORDER_TOUCH_OBJECT, border);
|
||||
}
|
||||
if (screenObj->motionType == kMotionMoveObj) {
|
||||
motionMoveObjStop(screenObj);
|
||||
}
|
||||
}
|
||||
|
||||
screenObj->flags &= ~fUpdatePos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust position of a sprite
|
||||
* This function adjusts the position of a sprite moving it until
|
||||
* certain criteria is matched. According to priority and control line
|
||||
* data, a sprite may not always appear at the location we specified.
|
||||
* This behavior is also known as the "Budin-Sonneveld effect".
|
||||
*
|
||||
* @param n view table entry number
|
||||
*/
|
||||
void AgiEngine::fixPosition(int16 screenObjNr) {
|
||||
ScreenObjEntry *screenObj = &_game.screenObjTable[screenObjNr];
|
||||
fixPosition(screenObj);
|
||||
}
|
||||
|
||||
void AgiEngine::fixPosition(ScreenObjEntry *screenObj) {
|
||||
debugC(4, kDebugLevelSprites, "adjusting view table entry #%d (%d,%d)", screenObj->objectNr, screenObj->xPos, screenObj->yPos);
|
||||
|
||||
// test horizon
|
||||
if ((!(screenObj->flags & fIgnoreHorizon)) && screenObj->yPos <= _game.horizon)
|
||||
screenObj->yPos = _game.horizon + 1;
|
||||
|
||||
int dir = 0;
|
||||
int count = 1;
|
||||
int size = 1;
|
||||
|
||||
while (!checkPosition(screenObj) || checkCollision(screenObj) || !checkPriority(screenObj)) {
|
||||
switch (dir) {
|
||||
case 0: // west
|
||||
screenObj->xPos--;
|
||||
if (--count)
|
||||
continue;
|
||||
dir = 1;
|
||||
break;
|
||||
case 1: // south
|
||||
screenObj->yPos++;
|
||||
if (--count)
|
||||
continue;
|
||||
dir = 2;
|
||||
size++;
|
||||
break;
|
||||
case 2: // east
|
||||
screenObj->xPos++;
|
||||
if (--count)
|
||||
continue;
|
||||
dir = 3;
|
||||
break;
|
||||
case 3: // north
|
||||
screenObj->yPos--;
|
||||
if (--count)
|
||||
continue;
|
||||
dir = 0;
|
||||
size++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
count = size;
|
||||
}
|
||||
|
||||
debugC(4, kDebugLevelSprites, "view table entry #%d position adjusted to (%d,%d)", screenObj->objectNr, screenObj->xPos, screenObj->yPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if ego is facing nearby water without obstacles. Used by opcode 5F in
|
||||
* Black Cauldron AGIv1 to test if the water flask can be filled. Removed from
|
||||
* the interpreter in AGIv2 and replaced with position tests in game scripts.
|
||||
*
|
||||
* Returns distance to water or 250 if water is not found or is blocked.
|
||||
*/
|
||||
byte AgiEngine::egoNearWater(byte limit) {
|
||||
ScreenObjEntry &ego = _game.screenObjTable[SCREENOBJECTS_EGO_ENTRY];
|
||||
int16 x1 = ego.xPos;
|
||||
int16 x2 = 0;
|
||||
byte direction;
|
||||
|
||||
switch (ego.currentLoopNr) {
|
||||
case 0: // right
|
||||
direction = 3;
|
||||
x1 += ego.xSize;
|
||||
break;
|
||||
case 1: // left
|
||||
direction = 7;
|
||||
break;
|
||||
case 2: // down
|
||||
direction = 5;
|
||||
x1 += (ego.xSize / 2);
|
||||
break;
|
||||
case 3: // up
|
||||
direction = 1;
|
||||
x2 = x1 + ego.xSize;
|
||||
x1--;
|
||||
break;
|
||||
default: // unhandled in original
|
||||
return 250; // no water
|
||||
}
|
||||
|
||||
int16 distance = -1; // uninitialized in original
|
||||
while (x1 != 0) {
|
||||
distance = nearWater(ego, direction, x1, ego.yPos, limit);
|
||||
if (distance != -1) {
|
||||
break;
|
||||
}
|
||||
x1 = x2;
|
||||
x2 = 0;
|
||||
}
|
||||
|
||||
if (distance == -1) {
|
||||
return 250; // no water
|
||||
}
|
||||
|
||||
// adjust ego positions for collision check
|
||||
int16 prevPrevX = ego.xPos_prev;
|
||||
int16 prevPrevY = ego.yPos_prev;
|
||||
ego.xPos_prev = ego.xPos;
|
||||
ego.yPos_prev = ego.yPos;
|
||||
switch (direction) {
|
||||
case 1: // up
|
||||
ego.yPos -= distance;
|
||||
break;
|
||||
case 3: // right
|
||||
ego.xPos += (x1 - ego.xSize);
|
||||
break;
|
||||
case 5: // down
|
||||
ego.yPos += distance;
|
||||
break;
|
||||
case 7: // left
|
||||
ego.xPos -= distance;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!checkCollision(&ego)) {
|
||||
if (_game.block.active) {
|
||||
if (!(ego.flags & fIgnoreBlocks)) {
|
||||
if (checkBlock(ego.xPos, ego.yPos)) {
|
||||
distance = 250; // no water
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
distance = 250; // no water
|
||||
}
|
||||
|
||||
// restore ego positions
|
||||
ego.xPos = ego.xPos_prev;
|
||||
ego.yPos = ego.yPos_prev;
|
||||
ego.xPos_prev = prevPrevX;
|
||||
ego.yPos_prev = prevPrevY;
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a screen object is near water in a given direction.
|
||||
*
|
||||
* Returns the distance to water or -1 if water is not found or is blocked.
|
||||
*
|
||||
* Note that the original contains a bug that scans left when facing right.
|
||||
* We do not implement this bug. In Black Cauldron AGIv1, it prevents filling
|
||||
* the flask when facing right unless ego is on or facing away from water.
|
||||
*/
|
||||
int16 AgiEngine::nearWater(ScreenObjEntry &screenObj, byte direction, int16 x, int16 y, byte limit) {
|
||||
int16 dx = 0;
|
||||
int16 dy = 0;
|
||||
switch (direction) {
|
||||
case 1: dy = -1; break;
|
||||
case 3: dx = 1; break;
|
||||
case 5: dy = 1; break;
|
||||
case 7: dx = -1; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
for (int16 i = 0; i <= limit; i++) {
|
||||
if (!(0 <= x && x < SCRIPT_WIDTH && 0 <= y && y < SCRIPT_HEIGHT)) {
|
||||
break;
|
||||
}
|
||||
|
||||
byte priority = _gfx->getPriority(x, y);
|
||||
x += dx;
|
||||
y += dy;
|
||||
|
||||
// water found?
|
||||
if (priority == 3) {
|
||||
return i;
|
||||
}
|
||||
|
||||
if (screenObj.priority != 15) {
|
||||
// is blocked by priority 0?
|
||||
if (priority == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// is blocked by priority 1?
|
||||
if (priority == 1 && !(screenObj.flags & fIgnoreBlocks)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // water not found
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
3
engines/agi/configure.engine
Normal file
3
engines/agi/configure.engine
Normal file
@@ -0,0 +1,3 @@
|
||||
# This file is included from the main "configure" script
|
||||
# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] [components]
|
||||
add_engine agi "AGI" yes "" "" "" "midi"
|
||||
754
engines/agi/console.cpp
Normal file
754
engines/agi/console.cpp
Normal file
@@ -0,0 +1,754 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/opcodes.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/loader.h"
|
||||
|
||||
#include "agi/preagi/preagi.h"
|
||||
#include "agi/preagi/mickey.h"
|
||||
#include "agi/preagi/winnie.h"
|
||||
|
||||
#include "common/file.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
Console::Console(AgiEngine *vm) : GUI::Debugger() {
|
||||
_vm = vm;
|
||||
|
||||
registerCmd("debug", WRAP_METHOD(Console, Cmd_Debug));
|
||||
registerCmd("cont", WRAP_METHOD(Console, Cmd_Cont));
|
||||
registerCmd("agiver", WRAP_METHOD(Console, Cmd_Agiver));
|
||||
registerCmd("version", WRAP_METHOD(Console, Cmd_Version));
|
||||
registerCmd("flags", WRAP_METHOD(Console, Cmd_Flags));
|
||||
registerCmd("logic0", WRAP_METHOD(Console, Cmd_Logic0));
|
||||
registerCmd("objs", WRAP_METHOD(Console, Cmd_Objs));
|
||||
registerCmd("runopcode", WRAP_METHOD(Console, Cmd_RunOpcode));
|
||||
registerCmd("opcode", WRAP_METHOD(Console, Cmd_Opcode));
|
||||
registerCmd("step", WRAP_METHOD(Console, Cmd_Step));
|
||||
registerCmd("trigger", WRAP_METHOD(Console, Cmd_Trigger));
|
||||
registerCmd("vars", WRAP_METHOD(Console, Cmd_Vars));
|
||||
registerCmd("setvar", WRAP_METHOD(Console, Cmd_SetVar));
|
||||
registerCmd("setflag", WRAP_METHOD(Console, Cmd_SetFlag));
|
||||
registerCmd("setobj", WRAP_METHOD(Console, Cmd_SetObj));
|
||||
registerCmd("room", WRAP_METHOD(Console, Cmd_Room));
|
||||
registerCmd("bt", WRAP_METHOD(Console, Cmd_BT));
|
||||
registerCmd("show_map", WRAP_METHOD(Console, Cmd_ShowMap));
|
||||
registerCmd("screenobj", WRAP_METHOD(Console, Cmd_ScreenObj));
|
||||
registerCmd("vmvars", WRAP_METHOD(Console, Cmd_VmVars));
|
||||
registerCmd("vmflags", WRAP_METHOD(Console, Cmd_VmFlags));
|
||||
registerCmd("disableautosave", WRAP_METHOD(Console, Cmd_DisableAutomaticSave));
|
||||
registerCmd("diskdump", WRAP_METHOD(Console, Cmd_DiskDump));
|
||||
}
|
||||
|
||||
bool Console::Cmd_SetVar(int argc, const char **argv) {
|
||||
if (argc != 3) {
|
||||
debugPrintf("Usage: %s <varnum> <value>\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
int p1 = (int)atoi(argv[1]);
|
||||
int p2 = (int)atoi(argv[2]);
|
||||
_vm->setVar(p1, p2);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_SetFlag(int argc, const char **argv) {
|
||||
if (argc != 3) {
|
||||
debugPrintf("Usage: %s <flagnum> <value>\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
int p1 = (int)atoi(argv[1]);
|
||||
int p2 = (int)atoi(argv[2]);
|
||||
_vm->setFlag(p1, (p2 != 0));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_SetObj(int argc, const char **argv) {
|
||||
if (argc != 3) {
|
||||
debugPrintf("Usage: %s <objnum> <location>\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
int p1 = (int)atoi(argv[1]);
|
||||
int p2 = (int)atoi(argv[2]);
|
||||
_vm->objectSetLocation(p1, p2);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_RunOpcode(int argc, const char **argv) {
|
||||
const AgiOpCodeEntry *opCodes = _vm->getOpCodesTable();
|
||||
|
||||
if (argc < 2) {
|
||||
debugPrintf("Usage: %s <name> <parameter0> ...\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; opCodes[i].name; i++) {
|
||||
if (!strcmp(argv[1], opCodes[i].name)) {
|
||||
uint8 p[16];
|
||||
if ((argc - 2) != opCodes[i].parameterSize) {
|
||||
debugPrintf("AGI opcode wants %d parameters\n", opCodes[i].parameterSize);
|
||||
return 0;
|
||||
}
|
||||
p[0] = argv[2] ? (char)strtoul(argv[2], nullptr, 0) : 0;
|
||||
p[1] = argv[3] ? (char)strtoul(argv[3], nullptr, 0) : 0;
|
||||
p[2] = argv[4] ? (char)strtoul(argv[4], nullptr, 0) : 0;
|
||||
p[3] = argv[5] ? (char)strtoul(argv[5], nullptr, 0) : 0;
|
||||
p[4] = argv[6] ? (char)strtoul(argv[6], nullptr, 0) : 0;
|
||||
|
||||
debugC(5, kDebugLevelMain, "Opcode: %s %d %d %d %d %d", opCodes[i].name, p[0], p[1], p[2], p[3], p[4]);
|
||||
|
||||
_vm->executeAgiCommand(i, p);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrintf("Unknown opcode\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Agiver(int argc, const char **argv) {
|
||||
int ver = _vm->getVersion();
|
||||
int maj = (ver >> 12) & 0xf;
|
||||
int min = ver & 0xfff;
|
||||
|
||||
debugPrintf("AGI version: ");
|
||||
debugPrintf(maj <= 2 ? "%x.%03x\n" : "%x.002.%03x\n", maj, min);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#define CONSOLE_VERSION_MAXLEN 10
|
||||
|
||||
bool Console::Cmd_Version(int argc, const char **argv) {
|
||||
AgiGame *game = &_vm->_game;
|
||||
|
||||
// Show AGI version
|
||||
Cmd_Agiver(argc, argv);
|
||||
|
||||
// And now try to figure out the version of the game
|
||||
// We do this by scanning through all script texts
|
||||
// This is the best we can do about it. There is no special location for the game version number.
|
||||
// There are multiple variations, like "ver. X.XX", "ver X.XX" and even "version X.XX".
|
||||
bool versionFound = false;
|
||||
for (int scriptNr = 0; scriptNr < MAX_DIRECTORY_ENTRIES; scriptNr++) {
|
||||
if (game->dirLogic[scriptNr].offset == _EMPTY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Script is supposed to exist?
|
||||
bool scriptLoadedByUs = false;
|
||||
if (!(game->dirLogic[scriptNr].flags & RES_LOADED)) {
|
||||
// But not currently loaded? -> load it now
|
||||
if (_vm->loadResource(RESOURCETYPE_LOGIC, scriptNr) != errOK) {
|
||||
// In case we can't load the source, skip it
|
||||
continue;
|
||||
}
|
||||
scriptLoadedByUs = true;
|
||||
}
|
||||
// Script currently loaded
|
||||
// Now scan all texts
|
||||
int scriptTextCount = game->logics[scriptNr].numTexts;
|
||||
for (int scriptTextNr = 0; scriptTextNr < scriptTextCount; scriptTextNr++) {
|
||||
const char *scriptTextPtr = game->logics[scriptNr].texts[scriptTextNr];
|
||||
|
||||
// Now scan this text for version information
|
||||
const char *wordScanPtr = scriptTextPtr;
|
||||
|
||||
char curChar;
|
||||
do {
|
||||
curChar = *wordScanPtr;
|
||||
|
||||
if ((curChar == 'V') || (curChar == 'v')) {
|
||||
// "V" gefunden, ggf. beginning of version?
|
||||
const char *wordStartPtr = wordScanPtr;
|
||||
|
||||
do {
|
||||
curChar = *wordScanPtr;
|
||||
if (curChar == ' ') {
|
||||
break;
|
||||
}
|
||||
wordScanPtr++;
|
||||
} while (curChar);
|
||||
|
||||
if (curChar) {
|
||||
// end of "version" found
|
||||
bool wordFound = false;
|
||||
int wordLen = wordScanPtr - wordStartPtr;
|
||||
if (wordLen >= 3) {
|
||||
if (strncmp(wordStartPtr, "ver", wordLen) == 0)
|
||||
wordFound = true;
|
||||
if (strncmp(wordStartPtr, "Ver", wordLen) == 0)
|
||||
wordFound = true;
|
||||
}
|
||||
if ((!wordFound) && (wordLen >= 4)) {
|
||||
if (strncmp(wordStartPtr, "ver.", wordLen) == 0)
|
||||
wordFound = true;
|
||||
if (strncmp(wordStartPtr, "Ver.", wordLen) == 0)
|
||||
wordFound = true;
|
||||
}
|
||||
if ((!versionFound) && (wordLen >= 7)) {
|
||||
if (strncmp(wordStartPtr, "version", wordLen) == 0)
|
||||
wordFound = true;
|
||||
if (strncmp(wordStartPtr, "Version", wordLen) == 0)
|
||||
wordFound = true;
|
||||
if (strncmp(wordStartPtr, "VERSION", wordLen) == 0)
|
||||
wordFound = true;
|
||||
}
|
||||
|
||||
if (wordFound) {
|
||||
// We found something interesting
|
||||
//debugPrintf("%d: %s\n", scriptNr, scriptTextPtr);
|
||||
|
||||
wordScanPtr++; // skip space
|
||||
const char*versionStartPtr = wordScanPtr;
|
||||
curChar = *wordScanPtr;
|
||||
if ((curChar >= '0') && (curChar <= '9')) {
|
||||
// Next word starts with a number
|
||||
wordScanPtr++;
|
||||
curChar = *wordScanPtr;
|
||||
if (curChar == '.') {
|
||||
// Followed by a point? then we assume that we found a version number
|
||||
// Now we try to find the end of it
|
||||
wordScanPtr++;
|
||||
do {
|
||||
curChar = *wordScanPtr;
|
||||
if ((curChar == ' ') || (curChar == '\\') || (!curChar))
|
||||
break; // space or potential new line or NUL? -> found the end
|
||||
wordScanPtr++;
|
||||
} while (1);
|
||||
|
||||
int versionLen = wordScanPtr - versionStartPtr;
|
||||
if (versionLen < CONSOLE_VERSION_MAXLEN) {
|
||||
// Looks fine, now extract and show it
|
||||
char versionString[CONSOLE_VERSION_MAXLEN];
|
||||
memcpy(versionString, versionStartPtr, versionLen);
|
||||
versionString[versionLen] = 0;
|
||||
debugPrintf("Scanned game version: %s\n", versionString);
|
||||
versionFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seek back
|
||||
wordScanPtr = wordStartPtr;
|
||||
}
|
||||
wordScanPtr++;
|
||||
} while (curChar);
|
||||
}
|
||||
|
||||
if (scriptLoadedByUs) {
|
||||
_vm->unloadResource(RESOURCETYPE_LOGIC, scriptNr);
|
||||
}
|
||||
}
|
||||
|
||||
if (!versionFound) {
|
||||
debugPrintf("Scanned game version: [not found]\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Flags(int argc, const char **argv) {
|
||||
debugPrintf(" ");
|
||||
for (int i = 0; i < 10; i++)
|
||||
debugPrintf("%d ", i);
|
||||
debugPrintf("\n");
|
||||
|
||||
for (int i = 0; i < 255;) {
|
||||
debugPrintf("%3d ", i);
|
||||
for (int j = 0; j < 10; j++, i++) {
|
||||
debugPrintf("%c ", _vm->getFlag(i) ? 'T' : 'F');
|
||||
}
|
||||
debugPrintf("\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Vars(int argc, const char **argv) {
|
||||
for (int i = 0; i < 255;) {
|
||||
for (int j = 0; j < 5; j++, i++) {
|
||||
debugPrintf("%03d:%3d ", i, _vm->getVar(i));
|
||||
}
|
||||
debugPrintf("\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Objs(int argc, const char **argv) {
|
||||
for (unsigned int i = 0; i < _vm->_game.numObjects; i++) {
|
||||
debugPrintf("%3d]%-24s(%3d)\n", i, _vm->objectName(i), _vm->objectGetLocation(i));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Opcode(int argc, const char **argv) {
|
||||
if (argc != 2 || (strcmp(argv[1], "on") && strcmp(argv[1], "off"))) {
|
||||
debugPrintf("Usage: %s on|off\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
_vm->_debug.opcodes = !strcmp(argv[1], "on");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Logic0(int argc, const char **argv) {
|
||||
if (argc != 2 || (strcmp(argv[1], "on") && strcmp(argv[1], "off"))) {
|
||||
debugPrintf("Usage: %s on|off\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
_vm->_debug.logic0 = !strcmp(argv[1], "on");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Trigger(int argc, const char **argv) {
|
||||
if (argc != 2 || (strcmp(argv[1], "on") && strcmp(argv[1], "off"))) {
|
||||
debugPrintf("Usage: %s on|off\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
_vm->_debug.ignoretriggers = strcmp(argv[1], "on");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Step(int argc, const char **argv) {
|
||||
_vm->_debug.enabled = 1;
|
||||
|
||||
if (argc == 1) {
|
||||
_vm->_debug.steps = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
_vm->_debug.steps = strtoul(argv[1], nullptr, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Debug(int argc, const char **argv) {
|
||||
_vm->_debug.enabled = 1;
|
||||
_vm->_debug.steps = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Cont(int argc, const char **argv) {
|
||||
_vm->_debug.enabled = 0;
|
||||
_vm->_debug.steps = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_Room(int argc, const char **argv) {
|
||||
if (argc == 2) {
|
||||
_vm->newRoom(strtoul(argv[1], nullptr, 0));
|
||||
}
|
||||
|
||||
debugPrintf("Current room: %d\n", _vm->getVar(0));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_BT(int argc, const char **argv) {
|
||||
const AgiOpCodeEntry *opCodes = _vm->getOpCodesTable();
|
||||
|
||||
debugPrintf("Current script: %d\nStack depth: %d\n", _vm->_game.curLogicNr, _vm->_game.execStack.size());
|
||||
|
||||
uint8 p[CMD_BSIZE] = { 0 };
|
||||
|
||||
for (auto &entry : _vm->_game.execStack) {
|
||||
uint8 *code = _vm->_game.logics[entry.script].data;
|
||||
uint8 op = code[entry.curIP];
|
||||
int parameterSize = opCodes[op].parameterSize;
|
||||
memmove(p, &code[entry.curIP], parameterSize);
|
||||
memset(p + parameterSize, 0, CMD_BSIZE - parameterSize);
|
||||
|
||||
debugPrintf("%d(%d): %s(", entry.script, entry.curIP, opCodes[op].name);
|
||||
|
||||
for (int i = 0; i < parameterSize; i++)
|
||||
debugPrintf("%d, ", p[i]);
|
||||
|
||||
debugPrintf(")\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_ShowMap(int argc, const char **argv) {
|
||||
if (argc != 2) {
|
||||
debugPrintf("Switches to one of the following screen maps\n");
|
||||
debugPrintf("Usage: %s <screen map>\n", argv[0]);
|
||||
debugPrintf("Screen maps:\n");
|
||||
debugPrintf("- 0: visual map\n");
|
||||
debugPrintf("- 1: priority map\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
int map = atoi(argv[1]);
|
||||
|
||||
switch (map) {
|
||||
case 0:
|
||||
case 1:
|
||||
_vm->_gfx->debugShowMap(map);
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrintf("Map %d is not available.\n", map);
|
||||
return true;
|
||||
}
|
||||
return cmdExit(0, nullptr);
|
||||
}
|
||||
|
||||
bool Console::Cmd_ScreenObj(int argc, const char **argv) {
|
||||
if (argc != 2) {
|
||||
debugPrintf("Shows information about a specific screen object\n");
|
||||
debugPrintf("Usage: %s <screenobj number>\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
int16 screenObjNr = atoi(argv[1]);
|
||||
|
||||
if ((screenObjNr >= 0) && (screenObjNr < SCREENOBJECTS_MAX)) {
|
||||
ScreenObjEntry *screenObj = &_vm->_game.screenObjTable[screenObjNr];
|
||||
|
||||
debugPrintf("Screen Object ID %d\n", screenObj->objectNr);
|
||||
debugPrintf("view: %d, loop: %d, cel: %d\n", screenObj->currentViewNr, screenObj->currentLoopNr, screenObj->currentCelNr);
|
||||
|
||||
// Figure out flags
|
||||
Common::String flagsString;
|
||||
|
||||
if (screenObj->flags & fDrawn)
|
||||
flagsString += "Drawn ";
|
||||
if (screenObj->flags & fIgnoreBlocks)
|
||||
flagsString += "IgnoreBlocks ";
|
||||
if (screenObj->flags & fFixedPriority)
|
||||
flagsString += "FixedPriority ";
|
||||
if (screenObj->flags & fIgnoreHorizon)
|
||||
flagsString += "IgnoreHorizon ";
|
||||
if (screenObj->flags & fUpdate)
|
||||
flagsString += "Update ";
|
||||
if (screenObj->flags & fCycling)
|
||||
flagsString += "Cycling ";
|
||||
if (screenObj->flags & fAnimated)
|
||||
flagsString += "Animated ";
|
||||
if (screenObj->flags & fMotion)
|
||||
flagsString += "Motion ";
|
||||
if (screenObj->flags & fOnWater)
|
||||
flagsString += "OnWater ";
|
||||
if (screenObj->flags & fIgnoreObjects)
|
||||
flagsString += "IgnoreObjects ";
|
||||
if (screenObj->flags & fUpdatePos)
|
||||
flagsString += "UpdatePos ";
|
||||
if (screenObj->flags & fOnLand)
|
||||
flagsString += "OnLand ";
|
||||
if (screenObj->flags & fDontUpdate)
|
||||
flagsString += "DontUpdate ";
|
||||
if (screenObj->flags & fFixLoop)
|
||||
flagsString += "FixLoop ";
|
||||
if (screenObj->flags & fDidntMove)
|
||||
flagsString += "DidntMove ";
|
||||
if (screenObj->flags & fAdjEgoXY)
|
||||
flagsString += "AdjEgoXY ";
|
||||
|
||||
if (flagsString.size() == 0) {
|
||||
flagsString += "*none*";
|
||||
}
|
||||
|
||||
debugPrintf("flags: %s\n", flagsString.c_str());
|
||||
|
||||
debugPrintf("\n");
|
||||
debugPrintf("xPos: %d, yPos: %d, xSize: %d, ySize: %d\n", screenObj->xPos, screenObj->yPos, screenObj->xSize, screenObj->ySize);
|
||||
debugPrintf("previous: xPos: %d, yPos: %d, xSize: %d, ySize: %d\n", screenObj->xPos_prev, screenObj->yPos_prev, screenObj->xSize_prev, screenObj->ySize_prev);
|
||||
debugPrintf("direction: %d, priority: %d\n", screenObj->direction, screenObj->priority);
|
||||
debugPrintf("stepTime: %d, stepTimeCount: %d, stepSize: %d\n", screenObj->stepTime, screenObj->stepTimeCount, screenObj->stepSize);
|
||||
debugPrintf("cycleTime: %d, cycleTimeCount: %d\n", screenObj->cycleTime, screenObj->cycleTimeCount);
|
||||
|
||||
switch (screenObj->motionType) {
|
||||
case kMotionNormal:
|
||||
debugPrintf("\nmotion: normal\n");
|
||||
break;
|
||||
case kMotionWander:
|
||||
debugPrintf("\nmotion: wander\n");
|
||||
debugPrintf("wanderCount: %d\n", screenObj->wander_count);
|
||||
break;
|
||||
case kMotionFollowEgo:
|
||||
debugPrintf("\nmotion: follow ego\n");
|
||||
debugPrintf("stepSize: %d, flag: %d, count: %d", screenObj->follow_stepSize, screenObj->follow_flag, screenObj->follow_count);
|
||||
break;
|
||||
case kMotionMoveObj:
|
||||
case kMotionEgo:
|
||||
if (screenObj->motionType == kMotionMoveObj) {
|
||||
debugPrintf("\nmotion: move obj\n");
|
||||
} else {
|
||||
debugPrintf("\nmotion: ego\n");
|
||||
}
|
||||
debugPrintf("x: %d, y: %d, stepSize: %d, flag: %x\n", screenObj->move_x, screenObj->move_y, screenObj->move_stepSize, screenObj->move_flag);
|
||||
break;
|
||||
default:
|
||||
debugPrintf("\nmotion: UNKNOWN (%d)\n", screenObj->motionType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_VmVars(int argc, const char **argv) {
|
||||
if (argc < 2) {
|
||||
debugPrintf("Shows the content of a VM variable / sets it\n");
|
||||
debugPrintf("Usage: %s <variable number> [<value>]\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
int varNr = 0;
|
||||
if (!parseInteger(argv[1], varNr))
|
||||
return true;
|
||||
|
||||
if ((varNr < 0) || (varNr > 255)) {
|
||||
debugPrintf("invalid variable number\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (argc < 3) {
|
||||
// show contents
|
||||
debugPrintf("variable %d == %d\n", varNr, _vm->getVar(varNr));
|
||||
} else {
|
||||
int newValue = 0;
|
||||
if (!parseInteger(argv[2], newValue))
|
||||
return true;
|
||||
|
||||
_vm->setVar(varNr, newValue);
|
||||
|
||||
debugPrintf("value set.\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_VmFlags(int argc, const char **argv) {
|
||||
if (argc < 2) {
|
||||
debugPrintf("Shows the content of a VM flag / sets it\n");
|
||||
debugPrintf("Usage: %s <flag number> [<value>]\n", argv[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
int flagNr = 0;
|
||||
|
||||
if (!parseInteger(argv[1], flagNr))
|
||||
return true;
|
||||
|
||||
if ((flagNr < 0) || (flagNr > 255)) {
|
||||
debugPrintf("invalid flag number\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (argc < 3) {
|
||||
// show contents
|
||||
if (_vm->getFlag(flagNr)) {
|
||||
debugPrintf("flag %d == set\n", flagNr);
|
||||
} else {
|
||||
debugPrintf("flag %d == not set\n", flagNr);
|
||||
}
|
||||
} else {
|
||||
int newFlagState = 0;
|
||||
if (!parseInteger(argv[2], newFlagState))
|
||||
return true;
|
||||
|
||||
if ((newFlagState != 0) && (newFlagState != 1)) {
|
||||
debugPrintf("new state must be either 0 or 1\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!newFlagState) {
|
||||
_vm->setFlag(flagNr, false);
|
||||
debugPrintf("flag %d reset.\n", flagNr);
|
||||
} else {
|
||||
_vm->setFlag(flagNr, true);
|
||||
debugPrintf("flag %d set.\n", flagNr);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_DisableAutomaticSave(int argc, const char **argv) {
|
||||
if (!_vm->_game.automaticSave) {
|
||||
debugPrintf("Automatic saving is currently not enabled\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
_vm->_game.automaticSave = false;
|
||||
|
||||
debugPrintf("Automatic saving DISABLED!\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::Cmd_DiskDump(int argc, const char **argv) {
|
||||
static const char *resTypes[4] = { "logic", "picture", "view", "sound" };
|
||||
|
||||
if (!(argc == 3 || (argc == 2 && strcmp(argv[1], "*") == 0))) {
|
||||
debugPrintf("Dumps the specified resource to disk as a file\n");
|
||||
debugPrintf("Usage: %s <resource type> <resource number>\n", argv[0]);
|
||||
debugPrintf(" <resource type> may be logic, picture, view, sound, or '*' for all resources\n");
|
||||
debugPrintf(" <resource number> may be '*' to dump all resources of given type\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
int resType = -1; // -1 == all
|
||||
if (strcmp(argv[1], "*") != 0) {
|
||||
for (int i = 0; i < ARRAYSIZE(resTypes); i++) {
|
||||
if (scumm_stricmp(argv[1], resTypes[i]) == 0) {
|
||||
resType = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resType == -1) {
|
||||
debugPrintf("Resource type '%s' is not valid\n", argv[1]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int resNr = -1; // -1 == all
|
||||
if (argc >= 3 && strcmp(argv[2], "*") != 0) {
|
||||
if (!parseInteger(argv[2], resNr)) {
|
||||
return true;
|
||||
}
|
||||
if (!(0 <= resNr && resNr < MAX_DIRECTORY_ENTRIES)) {
|
||||
debugPrintf("Invalid resource number: %d\n", resNr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
AgiDir *resDirs[4] = { _vm->_game.dirLogic, _vm->_game.dirPic, _vm->_game.dirView, _vm->_game.dirSound };
|
||||
for (int t = 0; t < ARRAYSIZE(resDirs); t++) {
|
||||
if (resType != -1 && resType != t) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AgiDir *resDir = resDirs[t];
|
||||
for (int i = 0; i < MAX_DIRECTORY_ENTRIES; i++) {
|
||||
if (resNr != -1 && resNr != i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resDir[i].offset == _EMPTY) {
|
||||
if (resNr != -1) {
|
||||
debugPrintf("Resource does not exist: %s.%03d\n", resTypes[t], i);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Common::String fileName = Common::String::format("%s.%03d", resTypes[t], i);
|
||||
byte *resData = _vm->_loader->loadVolumeResource(&resDir[i]);
|
||||
if (resData != nullptr) {
|
||||
Common::DumpFile file;
|
||||
if (file.open(Common::Path(fileName))) {
|
||||
file.write(resData, resDir[i].len);
|
||||
debugPrintf("%s has been dumped to disk\n", fileName.c_str());
|
||||
} else {
|
||||
debugPrintf("Error dumping %s to disk\n", fileName.c_str());
|
||||
}
|
||||
free(resData);
|
||||
} else {
|
||||
debugPrintf("Error dumping %s to disk\n", fileName.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Console::parseInteger(const char *argument, int &result) {
|
||||
char *endPtr = nullptr;
|
||||
int idxLen = strlen(argument);
|
||||
const char *lastChar = argument + idxLen - (idxLen == 0 ? 0 : 1);
|
||||
|
||||
if ((strncmp(argument, "0x", 2) == 0) || (*lastChar == 'h')) {
|
||||
// hexadecimal number
|
||||
result = strtol(argument, &endPtr, 16);
|
||||
if ((*endPtr != 0) && (*endPtr != 'h')) {
|
||||
debugPrintf("Invalid hexadecimal number '%s'\n", argument);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// decimal number
|
||||
result = strtol(argument, &endPtr, 10);
|
||||
if (*endPtr != 0) {
|
||||
debugPrintf("Invalid decimal number '%s'\n", argument);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
MickeyConsole::MickeyConsole(MickeyEngine *mickey) : GUI::Debugger() {
|
||||
_mickey = mickey;
|
||||
|
||||
registerCmd("room", WRAP_METHOD(MickeyConsole, Cmd_Room));
|
||||
registerCmd("drawPic", WRAP_METHOD(MickeyConsole, Cmd_DrawPic));
|
||||
registerCmd("drawObj", WRAP_METHOD(MickeyConsole, Cmd_DrawObj));
|
||||
}
|
||||
|
||||
bool MickeyConsole::Cmd_Room(int argc, const char **argv) {
|
||||
if (argc == 2)
|
||||
_mickey->debugGotoRoom(atoi(argv[1]));
|
||||
|
||||
_mickey->debugCurRoom();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MickeyConsole::Cmd_DrawPic(int argc, const char **argv) {
|
||||
if (argc != 2)
|
||||
debugPrintf("Usage: %s <Picture number>\n", argv[0]);
|
||||
else
|
||||
_mickey->drawPic(atoi(argv[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MickeyConsole::Cmd_DrawObj(int argc, const char **argv) {
|
||||
if (argc != 2)
|
||||
debugPrintf("Usage: %s <Object number>\n", argv[0]);
|
||||
else
|
||||
_mickey->drawObj((ENUM_MSA_OBJECT)atoi(argv[1]), 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
WinnieConsole::WinnieConsole(WinnieEngine *winnie) : GUI::Debugger() {
|
||||
_winnie = winnie;
|
||||
|
||||
registerCmd("curRoom", WRAP_METHOD(WinnieConsole, Cmd_CurRoom));
|
||||
}
|
||||
|
||||
bool WinnieConsole::Cmd_CurRoom(int argc, const char **argv) {
|
||||
_winnie->debugCurRoom();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
103
engines/agi/console.h
Normal file
103
engines/agi/console.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_CONSOLE_H
|
||||
#define AGI_CONSOLE_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class AgiEngine;
|
||||
class PreAgiEngine;
|
||||
class MickeyEngine;
|
||||
class WinnieEngine;
|
||||
|
||||
struct AgiDebug {
|
||||
int enabled;
|
||||
int opcodes;
|
||||
int logic0;
|
||||
int steps;
|
||||
int priority;
|
||||
int statusline;
|
||||
int ignoretriggers;
|
||||
};
|
||||
|
||||
class Console : public GUI::Debugger {
|
||||
public:
|
||||
Console(AgiEngine *vm);
|
||||
|
||||
private:
|
||||
bool Cmd_SetVar(int argc, const char **argv);
|
||||
bool Cmd_SetFlag(int argc, const char **argv);
|
||||
bool Cmd_SetObj(int argc, const char **argv);
|
||||
bool Cmd_RunOpcode(int argc, const char **argv);
|
||||
bool Cmd_Agiver(int argc, const char **argv);
|
||||
bool Cmd_Version(int argc, const char **argv);
|
||||
bool Cmd_Flags(int argc, const char **argv);
|
||||
bool Cmd_Vars(int argc, const char **argv);
|
||||
bool Cmd_Objs(int argc, const char **argv);
|
||||
bool Cmd_Opcode(int argc, const char **argv);
|
||||
bool Cmd_Logic0(int argc, const char **argv);
|
||||
bool Cmd_Trigger(int argc, const char **argv);
|
||||
bool Cmd_Step(int argc, const char **argv);
|
||||
bool Cmd_Debug(int argc, const char **argv);
|
||||
bool Cmd_Cont(int argc, const char **argv);
|
||||
bool Cmd_Room(int argc, const char **argv);
|
||||
bool Cmd_BT(int argc, const char **argv);
|
||||
bool Cmd_ShowMap(int argc, const char **argv);
|
||||
bool Cmd_ScreenObj(int argc, const char **argv);
|
||||
bool Cmd_VmVars(int argc, const char **argv);
|
||||
bool Cmd_VmFlags(int argc, const char **argv);
|
||||
bool Cmd_DisableAutomaticSave(int argc, const char **argv);
|
||||
bool Cmd_DiskDump(int argc, const char **argv);
|
||||
|
||||
bool parseInteger(const char *argument, int &result);
|
||||
|
||||
private:
|
||||
AgiEngine *_vm;
|
||||
};
|
||||
|
||||
class MickeyConsole : public GUI::Debugger {
|
||||
public:
|
||||
MickeyConsole(MickeyEngine *mickey);
|
||||
~MickeyConsole() override {}
|
||||
|
||||
private:
|
||||
MickeyEngine *_mickey;
|
||||
|
||||
bool Cmd_Room(int argc, const char **argv);
|
||||
bool Cmd_DrawPic(int argc, const char **argv);
|
||||
bool Cmd_DrawObj(int argc, const char **argv);
|
||||
};
|
||||
|
||||
class WinnieConsole : public GUI::Debugger {
|
||||
public:
|
||||
WinnieConsole(WinnieEngine *winnie);
|
||||
~WinnieConsole() override {}
|
||||
|
||||
private:
|
||||
WinnieEngine *_winnie;
|
||||
|
||||
bool Cmd_CurRoom(int argc, const char **argv);
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_CONSOLE_H */
|
||||
11
engines/agi/credits.pl
Normal file
11
engines/agi/credits.pl
Normal file
@@ -0,0 +1,11 @@
|
||||
begin_section("AGI");
|
||||
add_person("Stuart George", "darkfiber", "");
|
||||
add_person("Matthew Hoops", "clone2727", "(retired)");
|
||||
add_person("Filippos Karapetis", "bluegr", "");
|
||||
add_person("Martin Kiewitz", "m_kiewitz", "");
|
||||
add_person("Paweł Kołodziejski", "aquadran", "");
|
||||
add_person("Walter van Niftrik", "waltervn", "");
|
||||
add_person("Kari Salminen", "Buddha^", "");
|
||||
add_person("Eugene Sandulenko", "sev", "");
|
||||
add_person("David Symonds", "dsymonds", "(retired)");
|
||||
end_section();
|
||||
604
engines/agi/cycle.cpp
Normal file
604
engines/agi/cycle.cpp
Normal file
@@ -0,0 +1,604 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/config-manager.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/sprite.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/inv.h"
|
||||
#include "agi/text.h"
|
||||
#include "agi/keyboard.h"
|
||||
#include "agi/menu.h"
|
||||
#include "agi/systemui.h"
|
||||
#include "agi/appleIIgs_timedelay_overwrite.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* Set up new room.
|
||||
* This function is called when ego enters a new room.
|
||||
* @param n room number
|
||||
*/
|
||||
void AgiEngine::newRoom(int16 newRoomNr) {
|
||||
// The Gold Rush copy protection quiz is based on the book "California
|
||||
// Gold, story of the rush to riches" by Phyllis and Lou Zauner. It was
|
||||
// published in 1980, eight years before the game, so presumably Sierra
|
||||
// had some sort of licensing agreement. It was not included with the
|
||||
// Software Farm re-release, and that version skips (but does not
|
||||
// reomve) the copy protection.
|
||||
//
|
||||
// Since this was done by the original authors, we disable it in all
|
||||
// versions but give the player the option to re-enable it.
|
||||
|
||||
if (getGameID() == GID_GOLDRUSH && _game.curLogicNr == 129) {
|
||||
newRoomNr = ConfMan.getBool("copy_protection") ? 125 : 73;
|
||||
}
|
||||
|
||||
ScreenObjEntry *screenObjEgo = &_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY];
|
||||
|
||||
// Loading trigger
|
||||
artificialDelayTrigger_NewRoom(newRoomNr);
|
||||
|
||||
debugC(4, kDebugLevelMain, "*** room %d ***", newRoomNr);
|
||||
_sound->stopSound();
|
||||
|
||||
for (int i = 0; i < SCREENOBJECTS_MAX; i++) {
|
||||
ScreenObjEntry &screenObj = _game.screenObjTable[i];
|
||||
screenObj.objectNr = i;
|
||||
screenObj.flags &= ~(fAnimated | fDrawn);
|
||||
screenObj.flags |= fUpdate;
|
||||
screenObj.stepTime = 1;
|
||||
screenObj.stepTimeCount = 1;
|
||||
screenObj.cycleTime = 1;
|
||||
screenObj.cycleTimeCount = 1;
|
||||
screenObj.stepSize = 1;
|
||||
}
|
||||
unloadResources();
|
||||
|
||||
_game.playerControl = true;
|
||||
_game.block.active = false;
|
||||
_game.horizon = 36;
|
||||
setVar(VM_VAR_PREVIOUS_ROOM, getVar(VM_VAR_CURRENT_ROOM));
|
||||
setVar(VM_VAR_CURRENT_ROOM, newRoomNr);
|
||||
setVar(VM_VAR_BORDER_TOUCH_OBJECT, 0);
|
||||
setVar(VM_VAR_BORDER_CODE, 0);
|
||||
setVar(VM_VAR_EGO_VIEW_RESOURCE, screenObjEgo->currentViewNr);
|
||||
|
||||
loadResource(RESOURCETYPE_LOGIC, newRoomNr);
|
||||
|
||||
// Reposition ego in the new room
|
||||
switch (getVar(VM_VAR_BORDER_TOUCH_EGO)) {
|
||||
case 1:
|
||||
screenObjEgo->yPos = SCRIPT_HEIGHT - 1;
|
||||
break;
|
||||
case 2:
|
||||
screenObjEgo->xPos = 0;
|
||||
break;
|
||||
case 3:
|
||||
screenObjEgo->yPos = _game.horizon + 1;
|
||||
break;
|
||||
case 4:
|
||||
screenObjEgo->xPos = SCRIPT_WIDTH - screenObjEgo->xSize;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
uint16 agiVersion = getVersion();
|
||||
|
||||
if (agiVersion < 0x2000) {
|
||||
warning("STUB: NewRoom(%d)", newRoomNr);
|
||||
|
||||
screenObjEgo->flags &= ~fDidntMove;
|
||||
// animateObject(0);
|
||||
loadResource(RESOURCETYPE_VIEW, screenObjEgo->currentViewNr);
|
||||
setView(screenObjEgo, screenObjEgo->currentViewNr);
|
||||
|
||||
} else {
|
||||
if (agiVersion >= 0x3000) {
|
||||
// this was only done in AGI3
|
||||
if (screenObjEgo->motionType == kMotionEgo) {
|
||||
screenObjEgo->motionType = kMotionNormal;
|
||||
setVar(VM_VAR_EGO_DIRECTION, 0);
|
||||
}
|
||||
}
|
||||
|
||||
setVar(VM_VAR_BORDER_TOUCH_EGO, 0);
|
||||
setFlag(VM_FLAG_NEW_ROOM_EXEC, true);
|
||||
|
||||
_game.exitAllLogics = true;
|
||||
|
||||
_game._vm->_text->statusDraw();
|
||||
_game._vm->_text->promptRedraw();
|
||||
|
||||
// WORKAROUND: LSL1 has a script bug where exiting room 17 via the staircase
|
||||
// leaves a flag set that ignores priority triggers in all rooms. This allows
|
||||
// the player to leave the store (room 21) without paying. Bug #13137
|
||||
if (getGameID() == GID_LSL1) {
|
||||
setFlag(36, 0); // clear "ignore special" flag on every room change
|
||||
}
|
||||
|
||||
// WORKAROUND: KQ3 has a script bug where listening to fish talk in room 31
|
||||
// prevents hearing the critical mice conversation on the ship in room 86.
|
||||
// Each scene uses a series of flag numbers, but they overlap. Bug #15130
|
||||
if (getGameID() == GID_KQ3 && newRoomNr == 77) {
|
||||
for (int16 flag = 193; flag <= 197; flag++) {
|
||||
setFlag(flag, 0); // clear all mice flags when starting ship voyage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::resetControllers() {
|
||||
for (int i = 0; i < MAX_CONTROLLERS; i++) {
|
||||
_game.controllerOccurred[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::interpretCycle() {
|
||||
ScreenObjEntry *screenObjEgo = &_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY];
|
||||
|
||||
if (!_game.playerControl)
|
||||
setVar(VM_VAR_EGO_DIRECTION, screenObjEgo->direction);
|
||||
else
|
||||
screenObjEgo->direction = getVar(VM_VAR_EGO_DIRECTION);
|
||||
|
||||
checkAllMotions();
|
||||
|
||||
byte oldScore = getVar(VM_VAR_SCORE);
|
||||
bool oldSound = getFlag(VM_FLAG_SOUND_ON);
|
||||
|
||||
// Reset script heuristic here
|
||||
resetGetVarSecondsHeuristic();
|
||||
|
||||
_game.exitAllLogics = false;
|
||||
while (runLogic(0) == 0 && !(shouldQuit() || _restartGame)) {
|
||||
setVar(VM_VAR_WORD_NOT_FOUND, 0);
|
||||
setVar(VM_VAR_BORDER_TOUCH_OBJECT, 0);
|
||||
setVar(VM_VAR_BORDER_CODE, 0);
|
||||
oldScore = getVar(VM_VAR_SCORE);
|
||||
setFlag(VM_FLAG_ENTERED_CLI, false);
|
||||
_game.exitAllLogics = false;
|
||||
_veryFirstInitialCycle = false;
|
||||
artificialDelay_CycleDone();
|
||||
resetControllers();
|
||||
|
||||
// Reset mouse button state after new.room, because we don't poll input.
|
||||
// Otherwise, AGIMOUSE games that call new.room in response to a click
|
||||
// will enter an infinite loop due to the mouse button global (27) never
|
||||
// resetting to zero. Bug #10737
|
||||
_mouse.button = kAgiMouseButtonUp;
|
||||
}
|
||||
_veryFirstInitialCycle = false;
|
||||
artificialDelay_CycleDone();
|
||||
resetControllers();
|
||||
|
||||
screenObjEgo->direction = getVar(VM_VAR_EGO_DIRECTION);
|
||||
|
||||
if (getVar(VM_VAR_SCORE) != oldScore || getFlag(VM_FLAG_SOUND_ON) != oldSound)
|
||||
_game._vm->_text->statusDraw(getVar(VM_VAR_SCORE) != oldScore, getFlag(VM_FLAG_SOUND_ON) != oldSound);
|
||||
|
||||
setVar(VM_VAR_BORDER_TOUCH_OBJECT, 0);
|
||||
setVar(VM_VAR_BORDER_CODE, 0);
|
||||
setFlag(VM_FLAG_NEW_ROOM_EXEC, false);
|
||||
setFlag(VM_FLAG_RESTART_GAME, false);
|
||||
setFlag(VM_FLAG_RESTORE_JUST_RAN, false);
|
||||
|
||||
if (_game.gfxMode) {
|
||||
updateScreenObjTable();
|
||||
}
|
||||
_gfx->updateScreen();
|
||||
//_gfx->doUpdate();
|
||||
}
|
||||
|
||||
// We return the current key, or 0 if no key was pressed
|
||||
uint16 AgiEngine::processAGIEvents() {
|
||||
ScreenObjEntry *screenObjEgo = &_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY];
|
||||
|
||||
wait(10);
|
||||
uint16 key = doPollKeyboard();
|
||||
|
||||
if (!cycleInnerLoopIsActive()) {
|
||||
// Click-to-walk mouse interface
|
||||
if (_game.playerControl && (screenObjEgo->flags & fAdjEgoXY)) {
|
||||
int toX = screenObjEgo->move_x;
|
||||
int toY = screenObjEgo->move_y;
|
||||
|
||||
// AGI Mouse games use ego's sprite's bottom left corner for mouse walking target.
|
||||
// Amiga games use ego's sprite's bottom center for mouse walking target.
|
||||
// Atari ST and Apple II GS seem to use the bottom left
|
||||
if (getPlatform() == Common::kPlatformAmiga)
|
||||
toX -= (screenObjEgo->xSize / 2); // Center ego's sprite horizontally
|
||||
|
||||
// Adjust ego's sprite's mouse walking target position (These parameters are
|
||||
// controlled with the adj.ego.move.to.x.y-command). Note that these values rely
|
||||
// on the horizontal centering of the ego's sprite at least on the Amiga platform.
|
||||
toX += _game.adjMouseX;
|
||||
toY += _game.adjMouseY;
|
||||
|
||||
screenObjEgo->direction = getDirection(screenObjEgo->xPos, screenObjEgo->yPos, toX, toY, screenObjEgo->stepSize);
|
||||
|
||||
if (screenObjEgo->direction == 0)
|
||||
inDestination(screenObjEgo);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseClicks(key);
|
||||
|
||||
if (!cycleInnerLoopIsActive()) {
|
||||
// no inner loop active at the moment, regular processing
|
||||
|
||||
if (key) {
|
||||
if (!handleController(key)) {
|
||||
// Only set VAR_KEY, when no controller/direction was detected
|
||||
setVar(VM_VAR_KEY, key & 0xFF);
|
||||
if (_text->promptIsEnabled()) {
|
||||
_text->promptKeyPress(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_menu->delayedExecuteActive()) {
|
||||
_menu->execute();
|
||||
}
|
||||
|
||||
} else {
|
||||
// inner loop active
|
||||
// call specific workers
|
||||
switch (_game.cycleInnerLoopType) {
|
||||
case CYCLE_INNERLOOP_GETSTRING: // loop called from TextMgr::stringEdit()
|
||||
case CYCLE_INNERLOOP_GETNUMBER:
|
||||
if (key) {
|
||||
_text->stringKeyPress(key);
|
||||
}
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_INVENTORY: // loop called from InventoryMgr::show()
|
||||
if (key) {
|
||||
_inventory->keyPress(key);
|
||||
}
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_MENU_VIA_KEYBOARD:
|
||||
if (key) {
|
||||
_menu->keyPress(key);
|
||||
}
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_MENU_VIA_MOUSE:
|
||||
_menu->mouseEvent(key);
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_SYSTEMUI_SELECTSAVEDGAMESLOT:
|
||||
if (key) {
|
||||
_systemUI->savedGameSlot_KeyPress(key);
|
||||
}
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_SYSTEMUI_VERIFICATION:
|
||||
_systemUI->askForVerificationKeyPress(key);
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_MESSAGEBOX:
|
||||
if (key) {
|
||||
_text->messageBox_KeyPress(key);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// WORKAROUND: For Apple II gs we added a Speed menu; here the user choose some speed setting from the menu
|
||||
if (getPlatform() == Common::kPlatformApple2GS && _game.appleIIgsSpeedControllerSlot != 0xffff)
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (_game.controllerOccurred[_game.appleIIgsSpeedControllerSlot + i]) {
|
||||
_game.controllerOccurred[_game.appleIIgsSpeedControllerSlot + i] = false;
|
||||
_game.setSpeedLevel(i);
|
||||
}
|
||||
|
||||
_gfx->updateScreen();
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
void AgiEngine::playGame() {
|
||||
debugC(2, kDebugLevelMain, "initializing...");
|
||||
debugC(2, kDebugLevelMain, "game version = 0x%x", getVersion());
|
||||
|
||||
_sound->stopSound();
|
||||
|
||||
// We need to do this accurately and reset the AGI priorityscreen to 4
|
||||
// otherwise at least the fan game Nick's Quest will go into an endless
|
||||
// loop, because the game draws views before it draws the first background picture.
|
||||
// For further study see bug #5916
|
||||
_gfx->clear(0, 4);
|
||||
|
||||
_game.horizon = 36;
|
||||
_game.playerControl = false;
|
||||
|
||||
setFlag(VM_FLAG_LOGIC_ZERO_FIRST_TIME, true); // not in 2.917
|
||||
setFlag(VM_FLAG_NEW_ROOM_EXEC, true); // needed for MUMG and SQ2!
|
||||
setFlag(VM_FLAG_SOUND_ON, true); // enable sound
|
||||
// do not set VM_VAR_TIME_DELAY, original AGI did not do it (in the data segment it was simply set to 0)
|
||||
|
||||
_game.gfxMode = true;
|
||||
_text->promptRow_Set(22);
|
||||
|
||||
debug(0, "Running AGI script");
|
||||
|
||||
setFlag(VM_FLAG_ENTERED_CLI, false);
|
||||
setFlag(VM_FLAG_SAID_ACCEPTED_INPUT, false);
|
||||
setVar(VM_VAR_WORD_NOT_FOUND, 0);
|
||||
setVar(VM_VAR_KEY, 0);
|
||||
|
||||
debugC(2, kDebugLevelMain, "Entering main loop");
|
||||
bool firstLoop = !getFlag(VM_FLAG_RESTART_GAME); // Do not restore on game restart
|
||||
|
||||
if (firstLoop) {
|
||||
if (ConfMan.hasKey("save_slot")) {
|
||||
// quick restore enabled
|
||||
_game.automaticRestoreGame = true;
|
||||
}
|
||||
}
|
||||
|
||||
artificialDelay_Reset();
|
||||
|
||||
const AgiAppleIIgsDelayOverwriteGameEntry *appleIIgsDelayOverwrite = nullptr;
|
||||
if (getPlatform() == Common::kPlatformApple2GS) {
|
||||
// Look up, if there is a time delay overwrite table for the current game
|
||||
appleIIgsDelayOverwrite = getAppleIIgsDelayOverwriteGameEntry(getGameID());
|
||||
}
|
||||
|
||||
do {
|
||||
processAGIEvents();
|
||||
|
||||
inGameTimerUpdate();
|
||||
|
||||
byte timeDelay;
|
||||
if (getPlatform() == Common::kPlatformApple2) {
|
||||
// Apple II games did not have speed control. The interpreter ran as
|
||||
// fast as it could, but it was still quite slow. Game scripts still
|
||||
// set variable 10, but they do so inconsistently because they were
|
||||
// ported from other platforms and it had no effect.
|
||||
// We add speed control in `Words::handleSpeedCommands`.
|
||||
timeDelay = _game.speedLevel;
|
||||
} else if (getVersion() < 0x2000) {
|
||||
// AGIv1 uses an internal speed level, set by the `set.speed` opcode
|
||||
timeDelay = _game.speedLevel;
|
||||
} else if (getPlatform() == Common::kPlatformApple2GS) {
|
||||
byte newTimeDelay = 0xff;
|
||||
timeDelay = getAppleIIgsTimeDelay(appleIIgsDelayOverwrite, newTimeDelay);
|
||||
if (newTimeDelay != 0xff) {
|
||||
setVar(VM_VAR_TIME_DELAY, newTimeDelay);
|
||||
}
|
||||
} else {
|
||||
// AGIv2 and AGIv3 use the time delay variable set by game scripts
|
||||
timeDelay = getVar(VM_VAR_TIME_DELAY);
|
||||
}
|
||||
|
||||
// Increment the delay value by one, so that we wait for at least 1 cycle
|
||||
// In Original AGI 1 cycle was 50 milliseconds, so 20 frames per second
|
||||
// So TIME_DELAY 1 resulted in around 20 frames per second
|
||||
// 2 resulted in around 10 frames per second
|
||||
// 0 however resulted in no limits at all, so the game ran as fast as possible
|
||||
// We obviously do not want the game to run as fast as possible, so we will use 40 frames per second instead.
|
||||
timeDelay = timeDelay * 2;
|
||||
if (!timeDelay)
|
||||
timeDelay = 1;
|
||||
|
||||
// Our cycle counter runs at 25 milliseconds.
|
||||
// So time delay has to be 1 for the originally unlimited speed - for our 40 fps
|
||||
// 2 for 20 frames per second
|
||||
// 4 for 10 frames per second
|
||||
// and so on.
|
||||
|
||||
if (_passedPlayTimeCycles >= timeDelay) {
|
||||
// code to check for executed cycles
|
||||
// TimeDate time;
|
||||
// _system->getTimeAndDate(time);
|
||||
// warning("cycle %d", time.tm_sec);
|
||||
inGameTimerResetPassedCycles();
|
||||
|
||||
interpretCycle();
|
||||
|
||||
// Check if the user has asked to load a game from the command line
|
||||
// or the launcher
|
||||
if (_game.automaticRestoreGame) {
|
||||
_game.automaticRestoreGame = false;
|
||||
checkQuickLoad();
|
||||
}
|
||||
|
||||
setFlag(VM_FLAG_ENTERED_CLI, false);
|
||||
setFlag(VM_FLAG_SAID_ACCEPTED_INPUT, false);
|
||||
setVar(VM_VAR_WORD_NOT_FOUND, 0);
|
||||
setVar(VM_VAR_KEY, 0);
|
||||
}
|
||||
|
||||
} while (!(shouldQuit() || _restartGame));
|
||||
|
||||
_sound->stopSound();
|
||||
}
|
||||
|
||||
int AgiEngine::runGame() {
|
||||
int ec = errOK;
|
||||
|
||||
// Execute the game
|
||||
do {
|
||||
debugC(2, kDebugLevelMain, "game loop");
|
||||
debugC(2, kDebugLevelMain, "game version = 0x%x", getVersion());
|
||||
|
||||
ec = agiInit();
|
||||
if (ec != errOK)
|
||||
break;
|
||||
|
||||
if (_restartGame) {
|
||||
setFlag(VM_FLAG_RESTART_GAME, true);
|
||||
// do not set VM_VAR_TIME_DELAY, original AGI did not do it
|
||||
|
||||
// Reset in-game timer
|
||||
inGameTimerReset();
|
||||
|
||||
_restartGame = false;
|
||||
}
|
||||
|
||||
// Set computer type (v20 i.e. vComputer) and sound type
|
||||
switch (getPlatform()) {
|
||||
case Common::kPlatformAtariST:
|
||||
setVar(VM_VAR_COMPUTER, kAgiComputerAtariST);
|
||||
setVar(VM_VAR_SOUNDGENERATOR, kAgiSoundPC);
|
||||
break;
|
||||
case Common::kPlatformAmiga:
|
||||
setVar(VM_VAR_COMPUTER, kAgiComputerAmiga);
|
||||
setVar(VM_VAR_SOUNDGENERATOR, kAgiSoundTandy);
|
||||
break;
|
||||
case Common::kPlatformApple2:
|
||||
setVar(VM_VAR_COMPUTER, kAgiComputerApple2);
|
||||
setVar(VM_VAR_SOUNDGENERATOR, kAgiSoundPC);
|
||||
break;
|
||||
case Common::kPlatformApple2GS:
|
||||
setVar(VM_VAR_COMPUTER, kAgiComputerApple2GS);
|
||||
if (getFeatures() & GF_2GSOLDSOUND)
|
||||
setVar(VM_VAR_SOUNDGENERATOR, kAgiSound2GSOld);
|
||||
else
|
||||
setVar(VM_VAR_SOUNDGENERATOR, kAgiSoundTandy);
|
||||
break;
|
||||
case Common::kPlatformDOS:
|
||||
default:
|
||||
setVar(VM_VAR_COMPUTER, kAgiComputerPC);
|
||||
setVar(VM_VAR_SOUNDGENERATOR, kAgiSoundPC);
|
||||
break;
|
||||
}
|
||||
|
||||
// Set monitor type (v26 i.e. vMonitor)
|
||||
switch (_renderMode) {
|
||||
case Common::kRenderCGA:
|
||||
setVar(VM_VAR_MONITOR, kAgiMonitorCga);
|
||||
break;
|
||||
case Common::kRenderHercA:
|
||||
case Common::kRenderHercG:
|
||||
// Set EGA for now. Some games place text differently, when this is set to kAgiMonitorHercules.
|
||||
// Text placement was different for Hercules rendering (16x12 instead of 16x16). There also was
|
||||
// not enough space left for the prompt at the bottom. This was caused by the Hercules resolution.
|
||||
// We don't have this restriction and we also support the regular prompt for Hercules mode.
|
||||
// In theory Sierra could have had special Hercules code inside their games.
|
||||
// TODO: check this.
|
||||
setVar(VM_VAR_MONITOR, kAgiMonitorEga);
|
||||
break;
|
||||
// Don't know if Amiga AGI games use a different value than kAgiMonitorEga
|
||||
// for vMonitor so I just use kAgiMonitorEga for them (As was done before too).
|
||||
case Common::kRenderAmiga:
|
||||
case Common::kRenderApple2GS:
|
||||
case Common::kRenderAtariST:
|
||||
case Common::kRenderEGA:
|
||||
case Common::kRenderVGA:
|
||||
default:
|
||||
setVar(VM_VAR_MONITOR, kAgiMonitorEga);
|
||||
break;
|
||||
}
|
||||
|
||||
setVar(VM_VAR_FREE_PAGES, 180); // Set amount of free memory to realistic value
|
||||
setVar(VM_VAR_MAX_INPUT_CHARACTERS, 38);
|
||||
_text->promptDisable();
|
||||
|
||||
playGame();
|
||||
agiDeinit();
|
||||
} while (_restartGame);
|
||||
|
||||
delete _menu;
|
||||
_menu = nullptr;
|
||||
|
||||
releaseImageStack();
|
||||
|
||||
return ec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time delay to use for an Apple IIgs interpreter cycle.
|
||||
* Optionally returns a new value for the time delay variable (variable 10).
|
||||
*/
|
||||
byte AgiEngine::getAppleIIgsTimeDelay(
|
||||
const AgiAppleIIgsDelayOverwriteGameEntry *appleIIgsDelayOverwrite,
|
||||
byte &newTimeDelay) const {
|
||||
|
||||
byte timeDelay = _game.vars[VM_VAR_TIME_DELAY];
|
||||
timeDelay++;
|
||||
// It seems that either Apple IIgs ran very slowly or that the delay in its interpreter was not working as everywhere else
|
||||
// Most games on that platform set the delay to 0, which means no delay in DOS
|
||||
// Gold Rush! even "optimizes" itself when larger sprites are on the screen it sets TIME_DELAY to 0.
|
||||
// Normally that game runs at TIME_DELAY 1.
|
||||
// Maybe a script patch for this game would make sense.
|
||||
// TODO: needs further investigation
|
||||
|
||||
int16 timeDelayOverwrite = -99;
|
||||
|
||||
// Now check, if we got a time delay overwrite entry for current room
|
||||
if (appleIIgsDelayOverwrite->roomTable) {
|
||||
byte curRoom = _game.vars[VM_VAR_CURRENT_ROOM];
|
||||
int16 curPictureNr = _picture->getResourceNr();
|
||||
|
||||
const AgiAppleIIgsDelayOverwriteRoomEntry *appleIIgsDelayRoomOverwrite = nullptr;
|
||||
appleIIgsDelayRoomOverwrite = appleIIgsDelayOverwrite->roomTable;
|
||||
while (appleIIgsDelayRoomOverwrite->fromRoom >= 0) {
|
||||
if ((appleIIgsDelayRoomOverwrite->fromRoom <= curRoom) && (appleIIgsDelayRoomOverwrite->toRoom >= curRoom)) {
|
||||
if ((appleIIgsDelayRoomOverwrite->activePictureNr == curPictureNr) || (appleIIgsDelayRoomOverwrite->activePictureNr == -1)) {
|
||||
if (appleIIgsDelayRoomOverwrite->onlyWhenPlayerNotInControl) {
|
||||
if (_game.playerControl) {
|
||||
// Player is actually currently in control? -> then skip this entry
|
||||
appleIIgsDelayRoomOverwrite++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
timeDelayOverwrite = appleIIgsDelayRoomOverwrite->timeDelayOverwrite;
|
||||
break;
|
||||
}
|
||||
}
|
||||
appleIIgsDelayRoomOverwrite++;
|
||||
}
|
||||
}
|
||||
|
||||
if (timeDelayOverwrite == -99) {
|
||||
// use default time delay in case no room specific one was found ...
|
||||
if (_game.speedLevel == 2)
|
||||
// ... and the user set the speed to "Normal" ...
|
||||
timeDelayOverwrite = appleIIgsDelayOverwrite->defaultTimeDelayOverwrite;
|
||||
else
|
||||
// ... otherwise, use the speed the user requested (either from menu, or from text parser)
|
||||
timeDelayOverwrite = _game.speedLevel;
|
||||
}
|
||||
|
||||
|
||||
if (timeDelayOverwrite >= 0) {
|
||||
if (timeDelayOverwrite != timeDelay) {
|
||||
// delayOverwrite is not the same as the delay taken from the scripts? overwrite it
|
||||
//warning("AppleIIgs: time delay overwrite from %d to %d", timeDelay, timeDelayOverwrite);
|
||||
|
||||
newTimeDelay = timeDelayOverwrite - 1; // adjust for Apple IIgs
|
||||
timeDelay = timeDelayOverwrite;
|
||||
}
|
||||
}
|
||||
|
||||
return timeDelay;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
646
engines/agi/detection.cpp
Normal file
646
engines/agi/detection.cpp
Normal file
@@ -0,0 +1,646 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/config-manager.h"
|
||||
#include "common/system.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/md5.h"
|
||||
|
||||
#include "base/plugins.h"
|
||||
#include "engines/advancedDetector.h"
|
||||
#include "engines/metaengine.h"
|
||||
|
||||
#include "agi/detection.h"
|
||||
#include "agi/disk_image.h"
|
||||
#include "agi/wagparser.h" // for fallback detection
|
||||
#include "agi/agi.h"
|
||||
|
||||
static const DebugChannelDef debugFlagList[] = {
|
||||
{Agi::kDebugLevelMain, "Main", "Generic debug level"},
|
||||
{Agi::kDebugLevelResources, "Resources", "Resources debugging"},
|
||||
{Agi::kDebugLevelPictures, "Pictures", "Pictures debugging"},
|
||||
{Agi::kDebugLevelSprites, "Sprites", "Sprites debugging"},
|
||||
{Agi::kDebugLevelInventory, "Inventory", "Inventory debugging"},
|
||||
{Agi::kDebugLevelInput, "Input", "Input events debugging"},
|
||||
{Agi::kDebugLevelMenu, "Menu", "Menu debugging"},
|
||||
{Agi::kDebugLevelScripts, "Scripts", "Scripts debugging"},
|
||||
{Agi::kDebugLevelSound, "Sound", "Sound debugging"},
|
||||
{Agi::kDebugLevelText, "Text", "Text output debugging"},
|
||||
{Agi::kDebugLevelSavegame, "Savegame", "Saving & restoring game debugging"},
|
||||
DEBUG_CHANNEL_END
|
||||
};
|
||||
|
||||
static const PlainGameDescriptor agiGames[] = {
|
||||
{"agi", "Sierra AGI game"},
|
||||
{"agi-fanmade", "Fanmade AGI game"},
|
||||
{"agidemo", "AGI Demo"},
|
||||
{"bc", "The Black Cauldron"},
|
||||
{"caitlyn", "Caitlyn's Destiny"},
|
||||
{"ddp", "Donald Duck's Playground"},
|
||||
{"goldrush", "Gold Rush!"},
|
||||
{"kq1", "King's Quest: Quest for the Crown"},
|
||||
{"kq2", "King's Quest II: Romancing the Throne"},
|
||||
{"kq3", "King's Quest III: To Heir Is Human"},
|
||||
{"kq4", "King's Quest IV: The Perils of Rosella"},
|
||||
{"lsl1", "Leisure Suit Larry in the Land of the Lounge Lizards"},
|
||||
{"mickey", "Mickey\'s Space Adventure"},
|
||||
{"mixedup", "Mixed-Up Mother Goose"},
|
||||
{"mh1", "Manhunter: New York"},
|
||||
{"mh2", "Manhunter 2: San Francisco"},
|
||||
{"pq1", "Police Quest: In Pursuit of the Death Angel"},
|
||||
{"serguei1", "Serguei's Destiny 1"},
|
||||
{"serguei2", "Serguei's Destiny 2"},
|
||||
{"sq0", "Space Quest 0: Replicated"},
|
||||
{"sq1", "Space Quest: Chapter I - The Sarien Encounter"},
|
||||
{"sq2", "Space Quest II: Chapter II - Vohaul's Revenge"},
|
||||
{"sqx", "Space Quest: The Lost Chapter"},
|
||||
{"tetris", "AGI Tetris"},
|
||||
{"troll", "Troll\'s Tale"},
|
||||
{"winnie", "Winnie the Pooh in the Hundred Acre Wood"},
|
||||
{"xmascard", "Xmas Card"},
|
||||
|
||||
{nullptr, nullptr}
|
||||
};
|
||||
|
||||
#include "agi/detection_tables.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class AgiMetaEngineDetection : public AdvancedMetaEngineDetection<AGIGameDescription> {
|
||||
mutable Common::String _gameid;
|
||||
mutable Common::String _extra;
|
||||
|
||||
public:
|
||||
AgiMetaEngineDetection() : AdvancedMetaEngineDetection(Agi::gameDescriptions, agiGames) {
|
||||
_guiOptions = GUIO_NOSPEECH GUIO_RENDEREGA GUIO_RENDERCGA GUIO_RENDERHERCAMBER GUIO_RENDERHERCGREEN
|
||||
GUIO_RENDERAMIGA GUIO_RENDERAPPLE2GS GUIO_RENDERATARIST GUIO_RENDERMACINTOSH;
|
||||
|
||||
_maxScanDepth = 2;
|
||||
_flags = kADFlagMatchFullPaths;
|
||||
}
|
||||
|
||||
const char *getName() const override {
|
||||
return "agi";
|
||||
}
|
||||
|
||||
const char *getEngineName() const override {
|
||||
return "AGI preAGI + v2 + v3";
|
||||
}
|
||||
|
||||
const char *getOriginalCopyright() const override {
|
||||
return "Sierra AGI Engine (C) Sierra On-Line Software";
|
||||
}
|
||||
|
||||
const DebugChannelDef *getDebugChannels() const override {
|
||||
return debugFlagList;
|
||||
}
|
||||
|
||||
ADDetectedGame fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const override;
|
||||
|
||||
ADDetectedGames detectGame(const Common::FSNode &parent, const FileMap &allFiles, Common::Language language, Common::Platform platform, const Common::String &extra, uint32 skipADFlags, bool skipIncomplete) override;
|
||||
|
||||
private:
|
||||
static void getPotentialDiskImages(const FileMap &allFiles, const char * const *imageExtensions, size_t extensionCount, Common::Array<Common::Path> &imageFiles);
|
||||
|
||||
static ADDetectedGame detectPcDiskImageGame(const FileMap &allFiles, uint32 skipADFlags);
|
||||
static Common::String getLogDirHashFromPcDiskImageV1(Common::SeekableReadStream &stream);
|
||||
static Common::String getLogDirHashFromPcDiskImageV2001(Common::SeekableReadStream &stream);
|
||||
|
||||
static ADDetectedGame detectA2DiskImageGame(const FileMap &allFiles, uint32 skipADFlags);
|
||||
static Common::String getLogDirHashFromA2DiskImage(Common::SeekableReadStream &stream);
|
||||
|
||||
static Common::String getLogDirHashFromDiskImage(Common::SeekableReadStream &stream, uint32 position);
|
||||
|
||||
static Common::String getGalDirHashFromPcDiskImage(Common::SeekableReadStream &stream);
|
||||
static Common::String getGalDirHashFromA2DiskImage(Common::SeekableReadStream &stream);
|
||||
};
|
||||
|
||||
ADDetectedGame AgiMetaEngineDetection::fallbackDetect(const FileMap &allFilesXXX, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const {
|
||||
typedef Common::HashMap<Common::String, int32> IntMap;
|
||||
IntMap allFiles;
|
||||
bool matchedUsingFilenames = false;
|
||||
bool matchedUsingWag = false;
|
||||
int wagFileCount = 0;
|
||||
WagFileParser wagFileParser;
|
||||
Common::FSNode wagFileNode;
|
||||
Common::String description;
|
||||
|
||||
// // Set the defaults for gameid and extra
|
||||
_gameid = "agi-fanmade";
|
||||
_extra.clear();
|
||||
|
||||
// Set the default values for the fallback descriptor's ADGameDescription part.
|
||||
g_fallbackDesc.desc.language = Common::UNK_LANG;
|
||||
g_fallbackDesc.desc.platform = Common::kPlatformDOS;
|
||||
g_fallbackDesc.desc.flags = ADGF_NO_FLAGS;
|
||||
g_fallbackDesc.desc.guiOptions = GAMEOPTIONS_FANMADE_MOUSE;
|
||||
|
||||
// Set default values for the fallback descriptor's AGIGameDescription part.
|
||||
g_fallbackDesc.gameID = GID_FANMADE;
|
||||
g_fallbackDesc.features = GF_FANMADE;
|
||||
g_fallbackDesc.version = 0x2917;
|
||||
|
||||
// First grab all filenames and at the same time count the number of *.wag files
|
||||
for (const auto &file : fslist) {
|
||||
if (file.isDirectory()) continue;
|
||||
Common::String filename = file.getName();
|
||||
filename.toLowercase();
|
||||
allFiles[filename] = true; // Save the filename in a hash table
|
||||
|
||||
if (filename.hasSuffix(".wag")) {
|
||||
// Save latest found *.wag file's path (Can be used to open the file, the name can't)
|
||||
wagFileNode = file;
|
||||
wagFileCount++; // Count found *.wag files
|
||||
}
|
||||
}
|
||||
|
||||
if (allFiles.contains("logdir") && allFiles.contains("object") &&
|
||||
allFiles.contains("picdir") && allFiles.contains("snddir") &&
|
||||
allFiles.contains("viewdir") && allFiles.contains("vol.0") &&
|
||||
allFiles.contains("words.tok")) { // Check for v2
|
||||
|
||||
// The default AGI interpreter version 0x2917 is okay for v2 games
|
||||
// so we don't have to change it here.
|
||||
matchedUsingFilenames = true;
|
||||
|
||||
// Check for AGIPAL by checking for existence of any of the files "pal.100" - "pal.109"
|
||||
bool agipal = false;
|
||||
char agipalFile[] = "pal.xxx";
|
||||
for (uint i = 100; i <= 109; i++) {
|
||||
Common::sprintf_s(agipalFile, "pal.%d", i);
|
||||
if (allFiles.contains(agipalFile)) {
|
||||
agipal = true; // We found a file "pal.x" where 100 <= x <= 109 so it's AGIPAL
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (agipal) { // Check if it is AGIPAL
|
||||
description = "Unknown v2 AGIPAL Game";
|
||||
} else { // Not AGIPAL so just plain v2
|
||||
description = "Unknown v2 Game";
|
||||
}
|
||||
} else { // Try v3
|
||||
char name[8];
|
||||
|
||||
for (const auto &f : allFiles) {
|
||||
if (f._key.hasSuffix("vol.0")) {
|
||||
memset(name, 0, 8);
|
||||
strncpy(name, f._key.c_str(), MIN((uint)8, f._key.size() > 5 ? f._key.size() - 5 : f._key.size()));
|
||||
|
||||
if (allFiles.contains("object") && allFiles.contains("words.tok") &&
|
||||
allFiles.contains(Common::String(name) + "dir")) {
|
||||
matchedUsingFilenames = true;
|
||||
description = "Unknown v3 Game";
|
||||
g_fallbackDesc.version = 0x3149; // Set the default AGI version for an AGI v3 game
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WinAGI produces *.wag file with interpreter version, game name and other parameters.
|
||||
// If there's exactly one *.wag file and it parses successfully then we'll use its information.
|
||||
if (wagFileCount == 1 && wagFileParser.parse(wagFileNode)) {
|
||||
matchedUsingWag = true;
|
||||
|
||||
const WagProperty *wagAgiVer = wagFileParser.getProperty(WagProperty::PC_INTVERSION);
|
||||
const WagProperty *wagGameID = wagFileParser.getProperty(WagProperty::PC_GAMEID);
|
||||
const WagProperty *wagGameDesc = wagFileParser.getProperty(WagProperty::PC_GAMEDESC);
|
||||
const WagProperty *wagGameVer = wagFileParser.getProperty(WagProperty::PC_GAMEVERSION);
|
||||
const WagProperty *wagGameLastEdit = wagFileParser.getProperty(WagProperty::PC_GAMELAST);
|
||||
|
||||
// If there is an AGI version number in the *.wag file then let's use it
|
||||
if (wagAgiVer != nullptr && wagFileParser.checkAgiVersionProperty(*wagAgiVer)) {
|
||||
// TODO/FIXME: Check that version number is something we support before trying to use it.
|
||||
// If the version number is unsupported then it'll get switched to 0x2917 later.
|
||||
// But there's the possibility that file based detection has detected something else
|
||||
// than a v2 AGI game. So there's a possibility for conflicting information.
|
||||
g_fallbackDesc.version = wagFileParser.convertToAgiVersionNumber(*wagAgiVer);
|
||||
}
|
||||
|
||||
// Set gameid according to *.wag file information if it's present and it's a known value
|
||||
if (wagGameID != nullptr && findPlainGameDescriptor(wagGameID->getData(), agiGames)) {
|
||||
_gameid = wagGameID->getData();
|
||||
debug(3, "Agi::fallbackDetector: Using game id (%s) from WAG file", _gameid.c_str());
|
||||
}
|
||||
|
||||
// Set game description and extra according to *.wag file information if they're present
|
||||
if (wagGameDesc != nullptr && Common::String(wagGameDesc->getData()) != "\"\"") {
|
||||
description = wagGameDesc->getData();
|
||||
debug(3, "Agi::fallbackDetector: Game description (%s) from WAG file", wagGameDesc->getData());
|
||||
|
||||
// If there's game version in the *.wag file, set extra to it
|
||||
if (wagGameVer != nullptr && Common::String(wagGameVer->getData()) != "\"\"") {
|
||||
_extra = wagGameVer->getData();
|
||||
debug(3, "Agi::fallbackDetector: Game version (%s) from WAG file", wagGameVer->getData());
|
||||
}
|
||||
|
||||
// If there's game last edit date in the *.wag file, add it to extra
|
||||
if (wagGameLastEdit != nullptr) {
|
||||
if (!_extra.empty())
|
||||
_extra += " ";
|
||||
_extra += wagGameLastEdit->getData();
|
||||
debug(3, "Agi::fallbackDetector: Game's last edit date (%s) from WAG file", wagGameLastEdit->getData());
|
||||
}
|
||||
}
|
||||
} else if (wagFileCount > 1) { // More than one *.wag file, confusing! So let's not use them.
|
||||
warning("More than one (%d) *.wag files found. WAG files ignored", wagFileCount);
|
||||
}
|
||||
|
||||
// Check that the AGI interpreter version is a supported one
|
||||
if (!(g_fallbackDesc.version >= 0x2000 && g_fallbackDesc.version < 0x4000)) {
|
||||
warning("Unsupported AGI interpreter version 0x%x in AGI's fallback detection. Using default 0x2917", g_fallbackDesc.version);
|
||||
g_fallbackDesc.version = 0x2917;
|
||||
}
|
||||
|
||||
// Set game type (v2 or v3) according to the AGI interpreter version number
|
||||
if (g_fallbackDesc.version >= 0x2000 && g_fallbackDesc.version < 0x3000)
|
||||
g_fallbackDesc.gameType = GType_V2;
|
||||
else if (g_fallbackDesc.version >= 0x3000 && g_fallbackDesc.version < 0x4000)
|
||||
g_fallbackDesc.gameType = GType_V3;
|
||||
|
||||
// Check if we found a match with any of the fallback methods
|
||||
if (matchedUsingWag || matchedUsingFilenames) {
|
||||
_extra = description + (!_extra.empty() ? " " : "") + _extra; // Let's combine the description and extra
|
||||
|
||||
// Override the gameid & extra values in g_fallbackDesc.desc. This only works
|
||||
// until the fallback detector is called again, and while the MetaEngine instance
|
||||
// is alive (as else the string storage is modified/deleted).
|
||||
g_fallbackDesc.desc.gameId = _gameid.c_str();
|
||||
g_fallbackDesc.desc.extra = _extra.c_str();
|
||||
|
||||
Common::String fallbackWarning;
|
||||
|
||||
fallbackWarning = "Your game version has been detected using fallback matching as a\n";
|
||||
fallbackWarning += Common::String::format("variant of %s (%s).\n", g_fallbackDesc.desc.gameId, g_fallbackDesc.desc.extra);
|
||||
fallbackWarning += "If this is an original and unmodified version or new made Fanmade game,\n";
|
||||
fallbackWarning += "please report any information previously printed by ScummVM to the team.\n";
|
||||
|
||||
g_system->logMessage(LogMessageType::kWarning, fallbackWarning.c_str());
|
||||
|
||||
return ADDetectedGame(&g_fallbackDesc.desc);
|
||||
}
|
||||
|
||||
return ADDetectedGame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detection override for handling disk images after file-based detection.
|
||||
*/
|
||||
ADDetectedGames AgiMetaEngineDetection::detectGame(
|
||||
const Common::FSNode &parent,
|
||||
const FileMap &allFiles,
|
||||
Common::Language language,
|
||||
Common::Platform platform,
|
||||
const Common::String &extra,
|
||||
uint32 skipADFlags,
|
||||
bool skipIncomplete) {
|
||||
|
||||
// Run the file-based detection first, if it finds a match then do not search for disk images.
|
||||
ADDetectedGames matched = AdvancedMetaEngineDetection::detectGame(parent, allFiles, language, platform, extra, skipADFlags, skipIncomplete);
|
||||
|
||||
// Detect games within PC disk images. This detection will find one game at most.
|
||||
if (matched.empty() &&
|
||||
(language == Common::UNK_LANG || language == Common::EN_ANY) &&
|
||||
(platform == Common::kPlatformUnknown || platform == Common::kPlatformDOS)) {
|
||||
ADDetectedGame game = detectPcDiskImageGame(allFiles, skipADFlags);
|
||||
if (game.desc != nullptr) {
|
||||
matched.push_back(game);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect games within Apple II disk images. This detection will find one game at most.
|
||||
if (matched.empty() &&
|
||||
(language == Common::UNK_LANG || language == Common::EN_ANY) &&
|
||||
(platform == Common::kPlatformUnknown || platform == Common::kPlatformApple2)) {
|
||||
ADDetectedGame game = detectA2DiskImageGame(allFiles, skipADFlags);
|
||||
if (game.desc != nullptr) {
|
||||
matched.push_back(game);
|
||||
}
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
void AgiMetaEngineDetection::getPotentialDiskImages(
|
||||
const FileMap &allFiles,
|
||||
const char * const *imageExtensions,
|
||||
size_t imageExtensionCount,
|
||||
Common::Array<Common::Path> &imageFiles) {
|
||||
|
||||
// build an array of files with disk image extensions
|
||||
for (const auto &f : allFiles) {
|
||||
for (size_t i = 0; i < imageExtensionCount; i++) {
|
||||
if (f._key.baseName().hasSuffixIgnoreCase(imageExtensions[i])) {
|
||||
debug(3, "potential disk image: %s", f._key.baseName().c_str());
|
||||
imageFiles.push_back(f._key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort potential image files by name
|
||||
Common::sort(imageFiles.begin(), imageFiles.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects a PC Booter game by searching for 360k floppy images, reading LOGDIR
|
||||
* or GAL's directory, hashing, and comparing to DOS GType_V1 and GType_GAL
|
||||
* entries in the detection table.
|
||||
* See AgiLoader_v1 and GalLoader for more details.
|
||||
*/
|
||||
ADDetectedGame AgiMetaEngineDetection::detectPcDiskImageGame(const FileMap &allFiles, uint32 skipADFlags) {
|
||||
// build array of files with pc disk image extensions
|
||||
Common::Array<Common::Path> imageFiles;
|
||||
getPotentialDiskImages(allFiles, pcDiskImageExtensions, ARRAYSIZE(pcDiskImageExtensions), imageFiles);
|
||||
|
||||
// find disk one by reading potential images until a match is found
|
||||
for (const Common::Path &imageFile : imageFiles) {
|
||||
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, allFiles[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// attempt to locate and hash logdir using both possible inidir disk locations
|
||||
Common::String logdirHash1 = getLogDirHashFromPcDiskImageV1(*stream);
|
||||
Common::String logdirHash2 = getLogDirHashFromPcDiskImageV2001(*stream);
|
||||
// attempt to locate and hash GAL directory
|
||||
Common::String galDirHash = getGalDirHashFromPcDiskImage(*stream);
|
||||
delete stream;
|
||||
|
||||
if (!logdirHash1.empty()) {
|
||||
debug(3, "pc disk logdir hash: %s, %s", logdirHash1.c_str(), imageFile.baseName().c_str());
|
||||
}
|
||||
if (!logdirHash2.empty()) {
|
||||
debug(3, "pc disk logdir hash: %s, %s", logdirHash2.c_str(), imageFile.baseName().c_str());
|
||||
}
|
||||
if (!galDirHash.empty()) {
|
||||
debug(3, "pc disk gal dir hash: %s, %s", galDirHash.c_str(), imageFile.baseName().c_str());
|
||||
}
|
||||
|
||||
// if hash found then compare against hashes of DOS GType_V1 entries
|
||||
if (!logdirHash1.empty() || !logdirHash2.empty() || !galDirHash.empty()) {
|
||||
for (const AGIGameDescription *game = gameDescriptions; game->desc.gameId != nullptr; game++) {
|
||||
if (game->desc.platform == Common::kPlatformDOS &&
|
||||
(game->gameType == GType_V1 || game->gameType == GType_GAL) &&
|
||||
!(game->desc.flags & skipADFlags)) {
|
||||
|
||||
const ADGameFileDescription *file;
|
||||
for (file = game->desc.filesDescriptions; file->fileName != nullptr; file++) {
|
||||
// select the hash hash to use
|
||||
Common::String &hash = (game->gameType == GType_V1) ?
|
||||
((game->version < 0x2001) ? logdirHash1 : logdirHash2) :
|
||||
galDirHash;
|
||||
|
||||
if (file->md5 != nullptr && !hash.empty() && file->md5 == hash) {
|
||||
debug(3, "disk image match: %s, %s, %s", game->desc.gameId, game->desc.extra, imageFile.baseName().c_str());
|
||||
|
||||
// hash match found
|
||||
ADDetectedGame detectedGame(&game->desc);
|
||||
FileProperties fileProps;
|
||||
fileProps.md5 = file->md5;
|
||||
fileProps.md5prop = kMD5Archive;
|
||||
fileProps.size = PC_DISK_SIZE;
|
||||
detectedGame.matchedFiles[imageFile] = fileProps;
|
||||
return detectedGame;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ADDetectedGame();
|
||||
}
|
||||
|
||||
Common::String AgiMetaEngineDetection::getLogDirHashFromPcDiskImageV1(Common::SeekableReadStream &stream) {
|
||||
// read magic number from initdir resource header
|
||||
stream.seek(PC_INITDIR_POSITION_V1);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
if (magic != 0x1234) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// seek to initdir entry for logdir (and skip remaining 3 bytes of header)
|
||||
stream.skip(3 + (PC_INITDIR_LOGDIR_INDEX_V1 * PC_INITDIR_ENTRY_SIZE_V1));
|
||||
|
||||
// read logdir location
|
||||
byte volume = stream.readByte();
|
||||
byte head = stream.readByte();
|
||||
uint16 track = stream.readUint16LE();
|
||||
uint16 sector = stream.readUint16LE();
|
||||
uint16 offset = stream.readUint16LE();
|
||||
|
||||
// logdir volume must be one
|
||||
if (volume != 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// read logdir
|
||||
uint32 logDirPosition = PC_DISK_POSITION(head, track, sector, offset);
|
||||
return getLogDirHashFromDiskImage(stream, logDirPosition);
|
||||
}
|
||||
|
||||
Common::String AgiMetaEngineDetection::getLogDirHashFromPcDiskImageV2001(Common::SeekableReadStream &stream) {
|
||||
// seek to initdir entry for logdir
|
||||
stream.seek(PC_INITDIR_POSITION_V2001 + (PC_INITDIR_LOGDIR_INDEX_V2001 * PC_INITDIR_ENTRY_SIZE_V2001));
|
||||
|
||||
// read logdir location
|
||||
// volume 4 bits
|
||||
// position 12 bits (in half-sectors)
|
||||
byte b0 = stream.readByte();
|
||||
byte b1 = stream.readByte();
|
||||
byte volume = b0 >> 4;
|
||||
uint32 position = (((b0 & 0x0f) << 8) + b1) * 256;
|
||||
|
||||
// logdir volume must be one
|
||||
if (volume != 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// read logdir
|
||||
return getLogDirHashFromDiskImage(stream, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects an Apple II game by searching for 140k floppy images, reading LOGDIR,
|
||||
* hashing LOGDIR, and comparing to Apple II entries in the detection table.
|
||||
* See AgiLoader_A2 in loader_a2.cpp for more details.
|
||||
*/
|
||||
ADDetectedGame AgiMetaEngineDetection::detectA2DiskImageGame(const FileMap &allFiles, uint32 skipADFlags) {
|
||||
// build array of files with a2 disk image extensions
|
||||
Common::Array<Common::Path> imageFiles;
|
||||
getPotentialDiskImages(allFiles, a2DiskImageExtensions, ARRAYSIZE(a2DiskImageExtensions), imageFiles);
|
||||
|
||||
// find disk one by reading potential images until a match is found
|
||||
for (const Common::Path &imageFile : imageFiles) {
|
||||
// lazily-load disk image tracks as they're accessed.
|
||||
// prevents decoding entire disks just to read a few dynamic sectors.
|
||||
// this would create a significant delay for images in the .woz format.
|
||||
const bool loadAllTracks = false;
|
||||
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, allFiles[imageFile], loadAllTracks);
|
||||
if (stream == nullptr) {
|
||||
warning("unable to open disk image: %s", imageFile.baseName().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// attempt to locate and hash logdir by reading initdir,
|
||||
// and also known logdir locations for games without initdir.
|
||||
Common::String logdirHashInitdir = getLogDirHashFromA2DiskImage(*stream);
|
||||
Common::String logdirHashBc = getLogDirHashFromDiskImage(*stream, A2_BC_LOGDIR_POSITION);
|
||||
Common::String logdirHashKq2 = getLogDirHashFromDiskImage(*stream, A2_KQ2_LOGDIR_POSITION);
|
||||
// attempt to locate and hash GAL directory.
|
||||
Common::String logdirHashKq1 = getGalDirHashFromA2DiskImage(*stream);
|
||||
delete stream;
|
||||
|
||||
if (!logdirHashInitdir.empty()) {
|
||||
debug(3, "disk image initdir hash: %s, %s", logdirHashInitdir.c_str(), imageFile.baseName().c_str());
|
||||
}
|
||||
if (!logdirHashBc.empty()) {
|
||||
debug(3, "disk image logdir hash: %s, %s", logdirHashBc.c_str(), imageFile.baseName().c_str());
|
||||
}
|
||||
if (!logdirHashKq2.empty()) {
|
||||
debug(3, "disk image logdir hash: %s, %s", logdirHashKq2.c_str(), imageFile.baseName().c_str());
|
||||
}
|
||||
|
||||
// if logdir hash found then compare against hashes of Apple II entries
|
||||
if (!logdirHashInitdir.empty() || !logdirHashBc.empty() || !logdirHashKq2.empty() || !logdirHashKq1.empty()) {
|
||||
for (const AGIGameDescription *game = gameDescriptions; game->desc.gameId != nullptr; game++) {
|
||||
if (game->desc.platform == Common::kPlatformApple2 && !(game->desc.flags & skipADFlags)) {
|
||||
const ADGameFileDescription *file;
|
||||
for (file = game->desc.filesDescriptions; file->fileName != nullptr; file++) {
|
||||
// select the logdir hash to use
|
||||
Common::String &logdirHash = (game->gameID == GID_BC) ? logdirHashBc :
|
||||
(game->gameID == GID_KQ2) ? logdirHashKq2 :
|
||||
(game->gameID == GID_KQ1) ? logdirHashKq1 :
|
||||
logdirHashInitdir;
|
||||
if (file->md5 != nullptr && !logdirHash.empty() && file->md5 == logdirHash) {
|
||||
debug(3, "disk image match: %s, %s, %s", game->desc.gameId, game->desc.extra, imageFile.baseName().c_str());
|
||||
|
||||
// logdir hash match found
|
||||
ADDetectedGame detectedGame(&game->desc);
|
||||
FileProperties fileProps;
|
||||
fileProps.md5 = file->md5;
|
||||
fileProps.md5prop = kMD5Archive;
|
||||
fileProps.size = A2_DISK_SIZE;
|
||||
detectedGame.matchedFiles[imageFile] = fileProps;
|
||||
return detectedGame;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ADDetectedGame();
|
||||
}
|
||||
|
||||
Common::String AgiMetaEngineDetection::getLogDirHashFromA2DiskImage(Common::SeekableReadStream &stream) {
|
||||
// read magic number from initdir resource header
|
||||
stream.seek(A2_INITDIR_POSITION);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
if (magic != 0x1234) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// seek to initdir entry for logdir (and skip remaining 3 bytes of header)
|
||||
// also skip the one-byte volume number at the start of initdir
|
||||
stream.skip(3 + 1 + (A2_INITDIR_LOGDIR_INDEX * A2_INITDIR_ENTRY_SIZE));
|
||||
|
||||
// read logdir location
|
||||
byte volume = stream.readByte();
|
||||
byte track = stream.readByte();
|
||||
byte sector = stream.readByte();
|
||||
byte offset = stream.readByte();
|
||||
|
||||
// logdir volume must be one
|
||||
if (volume != 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// read logdir
|
||||
uint32 logDirPosition = A2_DISK_POSITION(track, sector, offset);
|
||||
return getLogDirHashFromDiskImage(stream, logDirPosition);
|
||||
}
|
||||
|
||||
// this works for both pc and a2 disk images
|
||||
Common::String AgiMetaEngineDetection::getLogDirHashFromDiskImage(Common::SeekableReadStream &stream, uint32 position) {
|
||||
stream.seek(position);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
if (magic != 0x1234) {
|
||||
return "";
|
||||
}
|
||||
stream.skip(1); // volume
|
||||
uint16 logDirSize = stream.readUint16LE();
|
||||
if (!(stream.pos() + logDirSize <= stream.size())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return Common::computeStreamMD5AsString(stream, logDirSize);
|
||||
}
|
||||
|
||||
Common::String AgiMetaEngineDetection::getGalDirHashFromPcDiskImage(Common::SeekableReadStream &stream) {
|
||||
static const uint16 dirPositions[] = { GAL_DIR_POSITION_PCJR, GAL_DIR_POSITION_PC };
|
||||
for (int i = 0; i < 2; i++) {
|
||||
stream.seek(dirPositions[i]);
|
||||
|
||||
// read logic 0 position
|
||||
byte b0 = stream.readByte();
|
||||
byte b1 = stream.readByte();
|
||||
byte b2 = stream.readByte();
|
||||
byte b3 = stream.readByte();
|
||||
uint16 offset = ((b1 & 0x80) << 1) | b0;
|
||||
uint16 sector = ((b2 & 0x03) << 8) | b3;
|
||||
uint32 logicPosition = (sector * 512) + offset;
|
||||
|
||||
// read logic 0 header, calculate length
|
||||
stream.seek(logicPosition);
|
||||
uint32 logicSize = 8;
|
||||
for (int j = 0; j < 4; j++) {
|
||||
logicSize += stream.readUint16LE();
|
||||
}
|
||||
if (stream.eos()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// confirm that logic ends in terminator
|
||||
stream.seek(logicPosition + logicSize -1);
|
||||
byte logicTerminator = stream.readByte();
|
||||
if (stream.eos() || logicTerminator != 0xff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// hash the directory
|
||||
stream.seek(dirPositions[i]);
|
||||
return Common::computeStreamMD5AsString(stream, GAL_DIR_SIZE);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Common::String AgiMetaEngineDetection::getGalDirHashFromA2DiskImage(Common::SeekableReadStream &stream) {
|
||||
// hash the directory
|
||||
stream.seek(GAL_A2_LOGDIR_POSITION);
|
||||
return Common::computeStreamMD5AsString(stream, GAL_A2_LOGDIR_SIZE);
|
||||
}
|
||||
|
||||
} // end of namespace Agi
|
||||
|
||||
REGISTER_PLUGIN_STATIC(AGI_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, Agi::AgiMetaEngineDetection);
|
||||
53
engines/agi/detection.h
Normal file
53
engines/agi/detection.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_DETECTION_H
|
||||
#define AGI_DETECTION_H
|
||||
|
||||
#include "engines/advancedDetector.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
struct AGIGameDescription {
|
||||
AD_GAME_DESCRIPTION_HELPERS(desc);
|
||||
|
||||
ADGameDescription desc;
|
||||
|
||||
int gameID;
|
||||
int gameType;
|
||||
uint32 features;
|
||||
uint16 version;
|
||||
};
|
||||
|
||||
#define GAMEOPTION_ORIGINAL_SAVELOAD GUIO_GAMEOPTIONS1
|
||||
#define GAMEOPTION_AMIGA_ALTERNATIVE_PALETTE GUIO_GAMEOPTIONS2
|
||||
#define GAMEOPTION_ENABLE_MOUSE GUIO_GAMEOPTIONS3
|
||||
#define GAMEOPTION_USE_HERCULES_FONT GUIO_GAMEOPTIONS4
|
||||
#define GAMEOPTION_COMMAND_PROMPT_WINDOW GUIO_GAMEOPTIONS5
|
||||
#define GAMEOPTION_APPLE2GS_ADD_SPEED_MENU GUIO_GAMEOPTIONS6
|
||||
#define GAMEOPTION_COPY_PROTECTION GUIO_GAMEOPTIONS7
|
||||
#define GAMEOPTION_ENABLE_PREDICTIVE_FOR_MOUSE GUIO_GAMEOPTIONS8
|
||||
#define GAMEOPTION_PCJR_SN76496_16BIT GUIO_GAMEOPTIONS9
|
||||
#define GAMEOPTION_TTS GUIO_GAMEOPTIONS10
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif // AGI_DETECTION_H
|
||||
1208
engines/agi/detection_tables.h
Normal file
1208
engines/agi/detection_tables.h
Normal file
File diff suppressed because it is too large
Load Diff
123
engines/agi/disk_image.cpp
Normal file
123
engines/agi/disk_image.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/formats/disk_image.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/path.h"
|
||||
#include "common/textconsole.h"
|
||||
|
||||
#include "agi/disk_image.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* DiskImageStream is a stream wrapper around Common::DiskImage.
|
||||
*
|
||||
* This allows DiskImage to lazily decode tracks as a stream is used.
|
||||
* This is important for detection, because the .woz format is noticeably
|
||||
* expensive to decode all tracks at once, and detection has to read
|
||||
* INITDIR to discover which track to read LOGDIR from.
|
||||
*/
|
||||
class DiskImageStream : virtual public Common::SeekableReadStream {
|
||||
public:
|
||||
DiskImageStream(Common::DiskImage *diskImage) : _diskImage(diskImage), _stream(_diskImage->getDiskStream()) {}
|
||||
|
||||
~DiskImageStream() {
|
||||
delete _diskImage;
|
||||
}
|
||||
|
||||
uint32 read(void *dataPtr, uint32 dataSize) override {
|
||||
return _diskImage->read(dataPtr, pos(), dataSize);
|
||||
}
|
||||
|
||||
bool eos() const override { return _stream->eos(); }
|
||||
void clearErr() override { _stream->clearErr(); }
|
||||
|
||||
int64 pos() const override{ return _stream->pos(); }
|
||||
int64 size() const override { return _stream->size(); }
|
||||
|
||||
bool seek(int64 offs, int whence = SEEK_SET) override { return _stream->seek(offs, whence); }
|
||||
|
||||
private:
|
||||
Common::DiskImage *_diskImage;
|
||||
Common::SeekableReadStream *_stream;
|
||||
};
|
||||
|
||||
Common::SeekableReadStream *openPCDiskImage(const Common::Path &path, const Common::FSNode &node) {
|
||||
Common::SeekableReadStream *stream = node.createReadStream();
|
||||
if (stream == nullptr) {
|
||||
warning("unable to open disk image: %s", path.baseName().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// validate disk size
|
||||
if (stream->size() != PC_DISK_SIZE) {
|
||||
delete stream;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *openA2DiskImage(const Common::Path &path, const Common::FSNode &node, bool loadAllTracks) {
|
||||
Common::String name = path.baseName();
|
||||
|
||||
// Open the image with Common::DiskImage, unless the file extension is ".img".
|
||||
// DiskImage expects ".img" to be a PC disk image, but it also gets used as
|
||||
// an Apple II raw sector disk image, so just open it and and read it.
|
||||
Common::SeekableReadStream *stream = nullptr;
|
||||
if (name.hasSuffixIgnoreCase(".img")) {
|
||||
stream = node.createReadStream();
|
||||
} else {
|
||||
if (loadAllTracks) {
|
||||
// when loading all tracks, open with DiskImage and take the stream.
|
||||
Common::DiskImage diskImage;
|
||||
if (diskImage.open(node)) {
|
||||
stream = diskImage.releaseStream();
|
||||
}
|
||||
} else {
|
||||
// when loading tracks as they're used, create a DiskImage with lazy
|
||||
// decoding and wrap it in a stream.
|
||||
Common::DiskImage *diskImage = new Common::DiskImage();
|
||||
diskImage->setLazyDecoding(true);
|
||||
if (diskImage->open(node)) {
|
||||
stream = new DiskImageStream(diskImage);
|
||||
} else {
|
||||
delete diskImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stream == nullptr) {
|
||||
warning("unable to open disk image: %s", path.baseName().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// validate disk size
|
||||
if (stream->size() != A2_DISK_SIZE) {
|
||||
delete stream;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
130
engines/agi/disk_image.h
Normal file
130
engines/agi/disk_image.h
Normal file
@@ -0,0 +1,130 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_DISK_IMAGE_H
|
||||
#define AGI_DISK_IMAGE_H
|
||||
|
||||
namespace Common {
|
||||
class SeekableReadStream;
|
||||
class Path;
|
||||
}
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// PC disk image values and helpers for AgiLoader_v1 and AgiMetaEngineDetection
|
||||
|
||||
// Disk image detection requires that image files have a known extension
|
||||
static const char * const pcDiskImageExtensions[] = { ".ima", ".img" };
|
||||
|
||||
#define PC_DISK_SIZE (2 * 40 * 9 * 512)
|
||||
#define PC_DISK_POSITION(h,t,s,o) (((((h + (t * 2)) * 9) + (s - 1)) * 512) + o)
|
||||
|
||||
#define PC_INITDIR_POSITION_V1 PC_DISK_POSITION(0, 0, 9, 0)
|
||||
#define PC_INITDIR_ENTRY_SIZE_V1 8
|
||||
#define PC_INITDIR_SIZE_V1 (PC_INITDIR_ENTRY_SIZE_V1 * 10)
|
||||
|
||||
#define PC_INITDIR_LOGDIR_INDEX_V1 3
|
||||
#define PC_INITDIR_PICDIR_INDEX_V1 4
|
||||
#define PC_INITDIR_VIEWDIR_INDEX_V1 5
|
||||
#define PC_INITDIR_SOUNDDIR_INDEX_V1 6
|
||||
#define PC_INITDIR_OBJECTS_INDEX_V1 1
|
||||
#define PC_INITDIR_WORDS_INDEX_V1 2
|
||||
|
||||
#define PC_INITDIR_POSITION_V2001 PC_DISK_POSITION(0, 0, 2, 0)
|
||||
#define PC_INITDIR_ENTRY_SIZE_V2001 3
|
||||
|
||||
#define PC_INITDIR_LOGDIR_INDEX_V2001 10
|
||||
#define PC_INITDIR_PICDIR_INDEX_V2001 11
|
||||
#define PC_INITDIR_VIEWDIR_INDEX_V2001 12
|
||||
#define PC_INITDIR_SOUNDDIR_INDEX_V2001 13
|
||||
#define PC_INITDIR_OBJECTS_INDEX_V2001 8
|
||||
#define PC_INITDIR_WORDS_INDEX_V2001 9
|
||||
#define PC_INITDIR_VOL0_INDEX_V2001 14
|
||||
|
||||
// A2 disk image values and helpers for AgiLoader_A2 and AgiMetaEngineDetection
|
||||
|
||||
// Disk image detection requires that image files have a known extension
|
||||
static const char * const a2DiskImageExtensions[] = { ".do", ".dsk", ".img", ".nib", ".woz" };
|
||||
|
||||
#define A2_DISK_SIZE (35 * 16 * 256)
|
||||
#define A2_DISK_POSITION(t, s, o) ((((t * 16) + s) * 256) + o)
|
||||
|
||||
#define A2_INITDIR_POSITION A2_DISK_POSITION(1, 3, 0)
|
||||
#define A2_INITDIR_ENTRY_SIZE 4
|
||||
|
||||
#define A2_INITDIR_LOGDIR_INDEX 3
|
||||
#define A2_INITDIR_PICDIR_INDEX 4
|
||||
#define A2_INITDIR_VIEWDIR_INDEX 5
|
||||
#define A2_INITDIR_SOUNDDIR_INDEX 6
|
||||
#define A2_INITDIR_OBJECTS_INDEX 1
|
||||
#define A2_INITDIR_WORDS_INDEX 2
|
||||
#define A2_INITDIR_VOLUME_MAP_POSITION (A2_INITDIR_POSITION + 5 + 33)
|
||||
|
||||
#define A2_KQ2_LOGDIR_POSITION A2_DISK_POSITION(1, 0, 0)
|
||||
#define A2_KQ2_PICDIR_POSITION A2_DISK_POSITION(1, 3, 0)
|
||||
#define A2_KQ2_VIEWDIR_POSITION A2_DISK_POSITION(1, 6, 0)
|
||||
#define A2_KQ2_SOUNDDIR_POSITION A2_DISK_POSITION(1, 9, 0)
|
||||
#define A2_KQ2_OBJECTS_POSITION A2_DISK_POSITION(2, 9, 0)
|
||||
#define A2_KQ2_WORDS_POSITION A2_DISK_POSITION(3, 0, 0)
|
||||
#define A2_KQ2_VOL0_POSITION A2_DISK_POSITION(26, 0, 0)
|
||||
#define A2_KQ2_VOL1_POSITION A2_DISK_POSITION(18, 0, 0)
|
||||
#define A2_KQ2_DISK_COUNT 5
|
||||
|
||||
#define A2_BC_LOGDIR_POSITION A2_DISK_POSITION(1, 7, 0)
|
||||
#define A2_BC_PICDIR_POSITION A2_DISK_POSITION(1, 12, 0)
|
||||
#define A2_BC_VIEWDIR_POSITION A2_DISK_POSITION(1, 9, 0)
|
||||
#define A2_BC_SOUNDDIR_POSITION A2_DISK_POSITION(1, 13, 0)
|
||||
#define A2_BC_OBJECTS_POSITION A2_DISK_POSITION(1, 3, 0)
|
||||
#define A2_BC_WORDS_POSITION A2_DISK_POSITION(1, 5, 0)
|
||||
#define A2_BC_VOLUME_MAP_POSITION A2_DISK_POSITION(7, 11, 254)
|
||||
#define A2_BC_DISK_COUNT 5
|
||||
#define A2_BC_VOLUME_COUNT 9
|
||||
|
||||
// GAL disk image values and helpers for GalLoader and AgiMetaEngineDetection
|
||||
|
||||
#define GAL_LOGIC_COUNT 84
|
||||
#define GAL_PICTURE_COUNT 84
|
||||
#define GAL_VIEW_COUNT 110
|
||||
#define GAL_SOUND_COUNT 10
|
||||
|
||||
#define GAL_DIR_POSITION_PCJR 0x0500
|
||||
#define GAL_DIR_POSITION_PC 0x1400
|
||||
#define GAL_DIR_SIZE 948
|
||||
|
||||
// GAL disk image values and helpers for GalLoader_A2 and AgiMetaEngineDetection
|
||||
|
||||
#define GAL_A2_LOGIC_COUNT 81
|
||||
#define GAL_A2_PICTURE_COUNT 85
|
||||
#define GAL_A2_VIEW_COUNT 110
|
||||
|
||||
#define GAL_A2_LOGDIR_POSITION A2_DISK_POSITION(18, 7, 2)
|
||||
#define GAL_A2_PICDIR_POSITION A2_DISK_POSITION(18, 6, 2)
|
||||
#define GAL_A2_VIEWDIR_POSITION A2_DISK_POSITION(18, 8, 2)
|
||||
#define GAL_A2_WORDS_POSITION A2_DISK_POSITION(17, 8, 0)
|
||||
#define GAL_A2_LOGDIR_SIZE (GAL_A2_LOGIC_COUNT * 3)
|
||||
#define GAL_A2_DISK_COUNT 3
|
||||
|
||||
Common::SeekableReadStream *openPCDiskImage(const Common::Path &path, const Common::FSNode &node);
|
||||
Common::SeekableReadStream *openA2DiskImage(const Common::Path &path, const Common::FSNode &node, bool loadAllTracks = true);
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_DISK_IMAGE_H */
|
||||
686
engines/agi/font.cpp
Normal file
686
engines/agi/font.cpp
Normal file
@@ -0,0 +1,686 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/config-manager.h"
|
||||
#include "common/translation.h"
|
||||
#include "gui/gui-manager.h"
|
||||
#include "gui/message.h"
|
||||
#include "graphics/fonts/amigafont.h"
|
||||
#include "graphics/fonts/dosfont.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/font.h"
|
||||
#include "agi/text.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
GfxFont::GfxFont(AgiBase *vm) {
|
||||
_vm = vm;
|
||||
|
||||
_fontData = nullptr;
|
||||
_fontDataAllocated = nullptr;
|
||||
_fontIsHires = false;
|
||||
}
|
||||
|
||||
GfxFont::~GfxFont() {
|
||||
free(_fontDataAllocated);
|
||||
}
|
||||
|
||||
// Arrow to the right character, used for original saved game dialogs
|
||||
// Needs to get patched into at least the Apple IIgs font, because the font didn't support
|
||||
// that character and original AGI on Apple IIgs used Apple II menus for saving/restoring.
|
||||
static const uint8 fontData_ArrowRightCharacter[8] = {
|
||||
0x00, 0x18, 0x0C, 0xFE, 0x0C, 0x18, 0x00, 0x00,
|
||||
};
|
||||
|
||||
void GfxFont::init() {
|
||||
if (ConfMan.getBool("herculesfont")) {
|
||||
// User wants, that we use Hercules hires font, try to load it
|
||||
loadFontHercules();
|
||||
} else {
|
||||
switch (_vm->_renderMode) {
|
||||
case Common::kRenderHercA:
|
||||
case Common::kRenderHercG:
|
||||
// Render mode is Hercules, we try to load Hercules hires font
|
||||
loadFontHercules();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_fontData) {
|
||||
switch (_vm->_renderMode) {
|
||||
case Common::kRenderAmiga:
|
||||
// Try user-file first, if that fails use our internal inaccurate topaz font
|
||||
loadFontScummVMFile("agi-font-amiga.bin");
|
||||
if (!_fontData) {
|
||||
loadFontAmigaPseudoTopaz();
|
||||
}
|
||||
break;
|
||||
case Common::kRenderApple2GS:
|
||||
// Special font, stored in file AGIFONT
|
||||
loadFontAppleIIgs();
|
||||
break;
|
||||
case Common::kRenderAtariST:
|
||||
// TODO: Atari ST uses another font
|
||||
// Seems to be the standard Atari ST 8x8 system font
|
||||
loadFontScummVMFile("agi-font-atarist.bin");
|
||||
if (!_fontData) {
|
||||
loadFontAtariST("agi-font-atarist-system.fnt");
|
||||
if (!_fontData) {
|
||||
// TODO: in case we find a recreation of the font, add it in here
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Common::kRenderHercA:
|
||||
case Common::kRenderHercG:
|
||||
case Common::kRenderCGA:
|
||||
case Common::kRenderEGA:
|
||||
case Common::kRenderVGA:
|
||||
switch (_vm->getGameID()) {
|
||||
case GID_MICKEY:
|
||||
// load mickey mouse font from interpreter file
|
||||
loadFontMickey();
|
||||
break;
|
||||
default:
|
||||
loadFontScummVMFile("agi-font-dos.bin");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_fontData) {
|
||||
// no font assigned?
|
||||
// use regular PC-BIOS font (taken from Dos-Box with a few modifications)
|
||||
_fontData = Graphics::DosFont::fontData_PCBIOS;
|
||||
debug("AGI: Using PC-BIOS font");
|
||||
}
|
||||
}
|
||||
|
||||
if (_vm->getLanguage() == Common::RU_RUS) {
|
||||
// Russian versions need special extended set
|
||||
overwriteExtendedWithRussianSet();
|
||||
}
|
||||
}
|
||||
|
||||
const byte *GfxFont::getFontData() {
|
||||
assert(_fontData);
|
||||
return _fontData;
|
||||
}
|
||||
|
||||
bool GfxFont::isFontHires() {
|
||||
return _fontIsHires;
|
||||
}
|
||||
|
||||
void GfxFont::overwriteSaveRestoreDialogCharacter() {
|
||||
// overwrite character 0x1A with the standard Sierra arrow to the right character
|
||||
// required for the original save/restore dialogs
|
||||
memcpy(_fontDataAllocated + (0x1A * 8), fontData_ArrowRightCharacter, sizeof(fontData_ArrowRightCharacter));
|
||||
}
|
||||
|
||||
// Overwrite extended character set (0x80-0xFF) with Russian characters
|
||||
void GfxFont::overwriteExtendedWithRussianSet() {
|
||||
if (_fontIsHires) {
|
||||
// TODO: Implement overwriting hires font characters too
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_fontDataAllocated) {
|
||||
// nothing allocated, we need to allocate space ourselves to be able to modify an internal font
|
||||
_fontDataAllocated = (uint8 *)calloc(256, 8);
|
||||
memcpy(_fontDataAllocated, _fontData, 128 * 8); // copy ASCII set over
|
||||
_fontData = _fontDataAllocated;
|
||||
}
|
||||
// Overwrite extended set with Russian characters
|
||||
memcpy(_fontDataAllocated + (128 * 8), Graphics::DosFont::fontData_ExtendedRussian, 128 * 8);
|
||||
|
||||
debug("AGI: Using Russian extended font set");
|
||||
}
|
||||
|
||||
// This code loads a ScummVM-specific user-supplied binary font file
|
||||
// It's assumed that it's a plain binary file, that contains 256 characters. 8 bytes per character.
|
||||
// 8x8 pixels per character. File size 2048 bytes.
|
||||
//
|
||||
// Currently used for:
|
||||
// Atari ST - "agi-font-atarist.bin" -> should be the Atari ST 8x8 system font
|
||||
// Amiga - "agi-font-amiga.bin" -> should be the Amiga 8x8 Topaz font
|
||||
// DOS - "agi-font-dos.bin"
|
||||
void GfxFont::loadFontScummVMFile(const Common::Path &fontFilename) {
|
||||
Common::File fontFile;
|
||||
if (!fontFile.open(fontFilename)) {
|
||||
// Continue, if file not found
|
||||
// These ScummVM font files are totally optional, so don't show a warning
|
||||
return;
|
||||
}
|
||||
|
||||
int32 fontFileSize = fontFile.size();
|
||||
if (fontFileSize != (256 * 8)) {
|
||||
// unexpected file size
|
||||
warning("Fontfile '%s': unexpected file size", fontFilename.toString(Common::Path::kNativeSeparator).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// allocate space for font bitmap data
|
||||
_fontDataAllocated = (uint8 *)calloc(256, 8);
|
||||
_fontData = _fontDataAllocated;
|
||||
|
||||
// read font data, is already in the format that we need (plain bitmap 8x8)
|
||||
fontFile.read(_fontDataAllocated, 256 * 8);
|
||||
|
||||
overwriteSaveRestoreDialogCharacter();
|
||||
|
||||
debug("AGI: Using user-supplied font");
|
||||
}
|
||||
|
||||
// We load the Mickey Mouse font from MICKEY.EXE
|
||||
void GfxFont::loadFontMickey() {
|
||||
Common::File interpreterFile;
|
||||
if (!interpreterFile.open("mickey.exe")) {
|
||||
// Continue, if file not found
|
||||
warning("Could not open file 'mickey.exe' for Mickey Mouse font");
|
||||
return;
|
||||
}
|
||||
|
||||
int32 interpreterFileSize = interpreterFile.size();
|
||||
if (interpreterFileSize != 55136) {
|
||||
// unexpected file size
|
||||
warning("File 'mickey.exe': unexpected file size");
|
||||
return;
|
||||
}
|
||||
interpreterFile.seek(32476); // offset of font data
|
||||
|
||||
// allocate space for font bitmap data
|
||||
byte *fontData = (uint8 *)calloc(256, 8);
|
||||
_fontData = fontData;
|
||||
_fontDataAllocated = fontData;
|
||||
|
||||
// read font data, is already in the format that we need (plain bitmap 8x8)
|
||||
interpreterFile.read(fontData, 256 * 8);
|
||||
|
||||
debug("AGI: Using Mickey Mouse font");
|
||||
}
|
||||
|
||||
// we create a bitmap out of the topaz data used in parallaction (which is normally found in staticres.cpp)
|
||||
// it's a recreation of the Amiga Topaz font but not really accurate
|
||||
void GfxFont::loadFontAmigaPseudoTopaz() {
|
||||
Graphics::Surface surf;
|
||||
surf.create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
|
||||
byte *fontData = (uint8 *)calloc(256, 8);
|
||||
|
||||
_fontData = fontData;
|
||||
_fontDataAllocated = fontData;
|
||||
|
||||
// copy first 32 PC-BIOS characters over
|
||||
memcpy(fontData, Graphics::DosFont::fontData_PCBIOS, FONT_DISPLAY_WIDTH * 32);
|
||||
fontData += FONT_DISPLAY_WIDTH * 32;
|
||||
|
||||
Graphics::AmigaFont topaz;
|
||||
|
||||
for (uint16 curChar = topaz.getLoChar(); curChar <= topaz.getHiChar(); curChar++) {
|
||||
topaz.drawChar(&surf, curChar, 0, 0, 0xff);
|
||||
|
||||
// Turn it into 1bpp
|
||||
for (int y = 0; y < 8; y++) {
|
||||
byte line = 0;
|
||||
byte *ptr = (byte *)surf.getBasePtr(0, y);
|
||||
for (int x = 0; x < 8; x++, ptr++) {
|
||||
line <<= 1;
|
||||
line |= *ptr & 0x1;
|
||||
}
|
||||
*fontData++ = line;
|
||||
}
|
||||
memset(surf.getPixels(), 0, 8 * 8);
|
||||
}
|
||||
|
||||
surf.free();
|
||||
|
||||
debug("AGI: Using recreation of Amiga Topaz font");
|
||||
}
|
||||
|
||||
void GfxFont::loadFontAppleIIgs() {
|
||||
Common::File fontFile;
|
||||
uint16 headerIIgs_OffsetMacHeader = 0;
|
||||
uint16 headerIIgs_Version = 0;
|
||||
uint16 macRecord_FirstChar = 0;
|
||||
uint16 macRecord_LastChar = 0;
|
||||
int16 macRecord_MaxKern = 0;
|
||||
uint16 macRecord_RectHeight = 0;
|
||||
uint16 macRecord_StrikeWidth = 0;
|
||||
uint16 strikeDataLen = 0;
|
||||
byte *strikeDataPtr = nullptr;
|
||||
uint16 actualCharacterCount = 0;
|
||||
uint16 totalCharacterCount = 0;
|
||||
uint16 *locationTablePtr = nullptr;
|
||||
uint16 *offsetWidthTablePtr = nullptr;
|
||||
|
||||
uint16 curCharNr = 0;
|
||||
uint16 curRow = 0;
|
||||
uint16 curLocation = 0;
|
||||
uint16 curLocationBytes = 0;
|
||||
uint16 curLocationBits = 0;
|
||||
uint16 curCharOffsetWidth = 0;
|
||||
uint16 curCharOffset = 0;
|
||||
uint16 curCharWidth = 0;
|
||||
uint16 curStrikeWidth = 0;
|
||||
|
||||
uint16 curPixelNr = 0;
|
||||
uint16 curBitMask = 0;
|
||||
int16 positionAdjust = 0;
|
||||
byte curByte = 0;
|
||||
byte fontByte = 0;
|
||||
|
||||
uint16 strikeRowOffset = 0;
|
||||
uint16 strikeCurOffset = 0;
|
||||
|
||||
byte *fontData = nullptr;
|
||||
|
||||
if (!fontFile.open("agifont")) {
|
||||
// Continue,
|
||||
// This also happens when the user selected Apple IIgs as render for the palette for non-AppleIIgs games
|
||||
warning("Could not open file 'agifont' for Apple IIgs font");
|
||||
return;
|
||||
}
|
||||
|
||||
// Apple IIgs header
|
||||
headerIIgs_OffsetMacHeader = fontFile.readUint16LE();
|
||||
fontFile.skip(2); // font family
|
||||
fontFile.skip(2); // font style
|
||||
fontFile.skip(2); // point size
|
||||
headerIIgs_Version = fontFile.readUint16LE();
|
||||
fontFile.skip(2); // bounds type
|
||||
// end of Apple IIgs header
|
||||
// Macintosh font record
|
||||
fontFile.skip(2); // font type
|
||||
macRecord_FirstChar = fontFile.readUint16LE();
|
||||
macRecord_LastChar = fontFile.readUint16LE();
|
||||
fontFile.skip(2); // max width
|
||||
macRecord_MaxKern = fontFile.readSint16LE();
|
||||
fontFile.skip(2); // negative descent
|
||||
fontFile.skip(2); // rect width
|
||||
macRecord_RectHeight = fontFile.readUint16LE();
|
||||
fontFile.skip(2); // low word ptr table
|
||||
fontFile.skip(2); // font ascent
|
||||
fontFile.skip(2); // font descent
|
||||
fontFile.skip(2); // leading
|
||||
macRecord_StrikeWidth = fontFile.readUint16LE();
|
||||
|
||||
// security-checks
|
||||
if (headerIIgs_OffsetMacHeader != 6)
|
||||
error("AppleIIgs-font: unexpected header");
|
||||
if (headerIIgs_Version != 0x0101)
|
||||
error("AppleIIgs-font: not a 1.1 font");
|
||||
if ((macRecord_FirstChar != 0) || (macRecord_LastChar != 255))
|
||||
error("AppleIIgs-font: unexpected characters");
|
||||
if (macRecord_RectHeight != 8)
|
||||
error("AppleIIgs-font: expected 8x8 font");
|
||||
|
||||
// Calculate table sizes
|
||||
strikeDataLen = macRecord_StrikeWidth * macRecord_RectHeight * 2;
|
||||
actualCharacterCount = (macRecord_LastChar - macRecord_FirstChar + 1);
|
||||
totalCharacterCount = actualCharacterCount + 2; // replacement-char + extra character
|
||||
|
||||
// Allocate memory for tables
|
||||
strikeDataPtr = (byte *)calloc(strikeDataLen, 1);
|
||||
locationTablePtr = (uint16 *)calloc(totalCharacterCount, 2); // 1 word per character
|
||||
offsetWidthTablePtr = (uint16 *)calloc(totalCharacterCount, 2); // ditto
|
||||
|
||||
// read tables
|
||||
fontFile.read(strikeDataPtr, strikeDataLen);
|
||||
for (curCharNr = 0; curCharNr < totalCharacterCount; curCharNr++) {
|
||||
locationTablePtr[curCharNr] = fontFile.readUint16LE();
|
||||
}
|
||||
for (curCharNr = 0; curCharNr < totalCharacterCount; curCharNr++) {
|
||||
offsetWidthTablePtr[curCharNr] = fontFile.readUint16LE();
|
||||
}
|
||||
fontFile.close();
|
||||
|
||||
// allocate space for font bitmap data
|
||||
fontData = (uint8 *)calloc(256, 8);
|
||||
_fontData = fontData;
|
||||
_fontDataAllocated = fontData;
|
||||
|
||||
// extract font bitmap data
|
||||
for (curCharNr = 0; curCharNr < actualCharacterCount; curCharNr++) {
|
||||
curCharOffsetWidth = offsetWidthTablePtr[curCharNr];
|
||||
curLocation = locationTablePtr[curCharNr];
|
||||
if (curCharOffsetWidth == 0xFFFF) {
|
||||
// character does not exist in font, use replacement character instead
|
||||
curCharOffsetWidth = offsetWidthTablePtr[actualCharacterCount];
|
||||
curLocation = locationTablePtr[actualCharacterCount];
|
||||
curStrikeWidth = locationTablePtr[actualCharacterCount + 1] - curLocation;
|
||||
} else {
|
||||
curStrikeWidth = locationTablePtr[curCharNr + 1] - curLocation;
|
||||
}
|
||||
|
||||
// Figure out bytes + bits location
|
||||
curLocationBytes = curLocation >> 3;
|
||||
curLocationBits = curLocation & 0x0007;
|
||||
curCharWidth = curCharOffsetWidth & 0x00FF; // isolate width
|
||||
curCharOffset = curCharOffsetWidth >> 8; // isolate offset
|
||||
|
||||
if (!curCharWidth) {
|
||||
fontData += 8; // skip over this character
|
||||
continue;
|
||||
}
|
||||
|
||||
if (curCharWidth != 8) {
|
||||
if (curCharNr != 0x3B)
|
||||
error("AppleIIgs-font: expected 8x8 font");
|
||||
}
|
||||
|
||||
// Get all rows of the current character
|
||||
strikeRowOffset = 0;
|
||||
for (curRow = 0; curRow < macRecord_RectHeight; curRow++) {
|
||||
strikeCurOffset = strikeRowOffset + curLocationBytes;
|
||||
|
||||
// Copy over bits
|
||||
fontByte = 0;
|
||||
curByte = strikeDataPtr[strikeCurOffset];
|
||||
curBitMask = 0x80 >> curLocationBits;
|
||||
|
||||
for (curPixelNr = 0; curPixelNr < curStrikeWidth; curPixelNr++) {
|
||||
fontByte = fontByte << 1;
|
||||
if (curByte & curBitMask) {
|
||||
fontByte |= 0x01;
|
||||
}
|
||||
curBitMask = curBitMask >> 1;
|
||||
if (!curBitMask) {
|
||||
curByte = strikeDataPtr[strikeCurOffset + 1];
|
||||
curBitMask = 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
// adjust, so that it's aligned to the left (starting at 0x80 bit)
|
||||
fontByte = fontByte << (8 - curStrikeWidth);
|
||||
|
||||
// now adjust according to offset + MaxKern
|
||||
positionAdjust = macRecord_MaxKern + curCharOffset;
|
||||
|
||||
// adjust may be negative for space, or 8 for "empty" characters
|
||||
if (positionAdjust > 8)
|
||||
error("AppleIIgs-font: invalid character spacing");
|
||||
|
||||
if (positionAdjust < 0) {
|
||||
// negative adjust strangely happens for empty characters like space
|
||||
if (curStrikeWidth)
|
||||
error("AppleIIgs-font: invalid character spacing");
|
||||
}
|
||||
|
||||
if (positionAdjust > 0) {
|
||||
// move the amount of pixels to the right
|
||||
fontByte = fontByte >> positionAdjust;
|
||||
}
|
||||
|
||||
*fontData = fontByte;
|
||||
fontData++;
|
||||
|
||||
strikeRowOffset += macRecord_StrikeWidth * 2;
|
||||
}
|
||||
}
|
||||
|
||||
free(offsetWidthTablePtr);
|
||||
free(locationTablePtr);
|
||||
free(strikeDataPtr);
|
||||
|
||||
overwriteSaveRestoreDialogCharacter();
|
||||
|
||||
debug("AGI: Using Apple IIgs font");
|
||||
}
|
||||
|
||||
// Loads Atari ST font file
|
||||
// It's found inside Atari ST ROMs. Just search for "8x8 system font". Font starts 4 bytes before that.
|
||||
void GfxFont::loadFontAtariST(const Common::Path &fontFilename) {
|
||||
Common::File fontFile;
|
||||
uint16 header_FirstChar = 0;
|
||||
uint16 header_LastChar = 0;
|
||||
uint16 header_MaxWidth = 0;
|
||||
uint16 header_MaxHeight = 0;
|
||||
uint16 header_Flags = 0;
|
||||
//uint32 header_OffsetOfCharOffsets = 0;
|
||||
//uint32 header_OffsetOfFontData = 0;
|
||||
uint16 header_FormWidth = 0;
|
||||
uint16 header_FormHeight = 0;
|
||||
uint16 totalCharacterCount = 0;
|
||||
uint16 *charOffsetTablePtr = nullptr;
|
||||
byte *rawDataTablePtr = nullptr;
|
||||
|
||||
uint16 curCharNr = 0;
|
||||
uint16 curCharRawOffset = 0;
|
||||
uint16 curCharDestOffset = 0;
|
||||
uint16 curRow = 0;
|
||||
|
||||
byte *fontData = nullptr;
|
||||
|
||||
if (!fontFile.open(fontFilename)) {
|
||||
// Continue, if file not found
|
||||
warning("Could not open file 'agi-font-atarist-system.bin' for Atari ST 8x8 system font");
|
||||
return;
|
||||
}
|
||||
|
||||
// Atari ST font header
|
||||
fontFile.skip(2); // face identifier
|
||||
fontFile.skip(2); // point size
|
||||
fontFile.skip(32); // font name
|
||||
header_FirstChar = fontFile.readUint16BE();
|
||||
header_LastChar = fontFile.readUint16BE();
|
||||
fontFile.skip(10); // aligntment of cells
|
||||
header_MaxWidth = fontFile.readUint16BE();
|
||||
header_MaxHeight = fontFile.readUint16BE();
|
||||
fontFile.skip(2); // left offset cel
|
||||
fontFile.skip(2); // right offset cel
|
||||
fontFile.skip(2); // number of pixels to thicken pixels
|
||||
fontFile.skip(2); // underline width
|
||||
fontFile.skip(2); // lightning mask
|
||||
fontFile.skip(2); // skewing mask
|
||||
header_Flags = fontFile.readUint16BE();
|
||||
// bit 0 - default system font
|
||||
// bit 1 - horizontal offset table (not supported)
|
||||
// bit 2 - byte orientation word is high->low
|
||||
// bit 3 - mono spaced font
|
||||
fontFile.skip(4); // horizontal table offset
|
||||
fontFile.skip(4); // header_OffsetOfCharOffsets = fontFile.readUint32BE();
|
||||
fontFile.skip(4); // header_OffsetOfFontData = fontFile.readUint32BE();
|
||||
header_FormWidth = fontFile.readUint16BE();
|
||||
header_FormHeight = fontFile.readUint16BE();
|
||||
fontFile.skip(4); // pointer to next font
|
||||
|
||||
totalCharacterCount = header_LastChar - header_FirstChar + 1;
|
||||
|
||||
// security-checks
|
||||
if (header_MaxWidth > 8)
|
||||
error("AtariST-font: not a 8x8 font");
|
||||
if (header_MaxHeight != 8)
|
||||
error("AtariST-font: not a 8x8 font");
|
||||
if (header_FormHeight != 8)
|
||||
error("AtariST-font: not a 8x8 font");
|
||||
if ((header_FirstChar != 0) || (header_LastChar != 255))
|
||||
error("AtariST-font: unexpected characters");
|
||||
if (header_FormWidth != totalCharacterCount)
|
||||
error("AtariST-font: header inconsistency");
|
||||
if (!(header_Flags & 0x04))
|
||||
error("AtariST-font: font data not in high->low order");
|
||||
if (!(header_Flags & 0x08))
|
||||
error("AtariST-font: not a mono-spaced font");
|
||||
|
||||
// Now we should normally use the offsets, but they don't make sense to me
|
||||
// So I just read the data directly. For the 8x8 system font that works
|
||||
fontFile.skip(2); // extra bytes
|
||||
|
||||
// Allocate memory for tables
|
||||
charOffsetTablePtr = (uint16 *)calloc(totalCharacterCount, 2); // 1 word per character
|
||||
rawDataTablePtr = (byte *)calloc(header_FormWidth, header_FormHeight);
|
||||
|
||||
// Char-Offset Table (2 * total number of characters)
|
||||
for (curCharNr = 0; curCharNr < totalCharacterCount; curCharNr++) {
|
||||
charOffsetTablePtr[curCharNr] = fontFile.readUint16BE();
|
||||
}
|
||||
|
||||
// Followed by actual font data
|
||||
// Attention: Atari ST fonts contain every same row of all characters after each other.
|
||||
// So it's basically like this:
|
||||
// [character data of first row of first character]
|
||||
// [character data of first row of second character]
|
||||
// ...
|
||||
// [character data of first row of last character]
|
||||
// [character data of second row of first character]
|
||||
fontFile.skip(2); // extra bytes
|
||||
fontFile.read(rawDataTablePtr, header_FormWidth * header_FormHeight);
|
||||
fontFile.close();
|
||||
|
||||
// allocate space for font bitmap data
|
||||
fontData = (uint8 *)calloc(256, 8);
|
||||
_fontData = fontData;
|
||||
_fontDataAllocated = fontData;
|
||||
|
||||
// extract font bitmap data
|
||||
for (curCharNr = 0; curCharNr < totalCharacterCount; curCharNr++) {
|
||||
// Figure out base offset from char offset table
|
||||
curCharRawOffset = charOffsetTablePtr[curCharNr] >> 3;
|
||||
curCharDestOffset = curCharNr * 8; // destination offset into our font data
|
||||
|
||||
// now copy over every row of the character
|
||||
for (curRow = 0; curRow < header_FormHeight; curRow++) {
|
||||
fontData[curCharDestOffset] = rawDataTablePtr[curCharRawOffset];
|
||||
curCharDestOffset++;
|
||||
curCharRawOffset += header_FormWidth;
|
||||
}
|
||||
}
|
||||
|
||||
free(rawDataTablePtr);
|
||||
free(charOffsetTablePtr);
|
||||
|
||||
overwriteSaveRestoreDialogCharacter();
|
||||
|
||||
debug("AGI: Using Atari ST 8x8 system font");
|
||||
}
|
||||
|
||||
// Loads a Sierra Hercules font file
|
||||
void GfxFont::loadFontHercules() {
|
||||
|
||||
if (_vm->getLanguage() == Common::RU_RUS) {
|
||||
warning("Hercules font does not contain Russian characters, switching to default");
|
||||
return;
|
||||
}
|
||||
|
||||
Common::File fontFile;
|
||||
int32 fontFileSize = 0;
|
||||
byte *fontData = nullptr;
|
||||
byte *rawData = nullptr;
|
||||
|
||||
uint16 rawDataPos = 0;
|
||||
uint16 curCharNr = 0;
|
||||
uint16 curCharLine = 0;
|
||||
|
||||
if (fontFile.open("hgc_font")) {
|
||||
// hgc_font file found, this is interleaved font data 16x12, should be 3072 bytes
|
||||
// 24 bytes per character, 128 characters
|
||||
fontFileSize = fontFile.size();
|
||||
if (fontFileSize == (128 * 24)) {
|
||||
// size seems to be fine
|
||||
fontData = (uint8 *)calloc(256, 32);
|
||||
_fontDataAllocated = fontData;
|
||||
|
||||
rawData = (byte *)calloc(128, 24);
|
||||
fontFile.read(rawData, 128 * 24);
|
||||
|
||||
// convert interleaved 16x12 -> non-interleaved 16x16
|
||||
for (curCharNr = 0; curCharNr < 128; curCharNr++) {
|
||||
fontData += 4; // skip the first 2 lines
|
||||
for (curCharLine = 0; curCharLine < 6; curCharLine++) {
|
||||
fontData[0] = rawData[rawDataPos + 2 + 0];
|
||||
fontData[1] = rawData[rawDataPos + 2 + 1];
|
||||
fontData[2] = rawData[rawDataPos + 0 + 0];
|
||||
fontData[3] = rawData[rawDataPos + 0 + 1];
|
||||
rawDataPos += 4;
|
||||
fontData += 4;
|
||||
}
|
||||
fontData += 4; // skip the last 2 lines
|
||||
}
|
||||
|
||||
free(rawData);
|
||||
} else {
|
||||
warning("Fontfile 'hgc_font': unexpected file size");
|
||||
}
|
||||
fontFile.close();
|
||||
}
|
||||
|
||||
// It seems hgc_graf.ovl holds a low-res font. It makes no real sense to use it.
|
||||
// This was only done to AGI3 games and those rendered differently (2 pixel lines -> 3 pixel lines instead of 4)
|
||||
// User could copy hgc_font from another AGI game over to get the hires font working.
|
||||
#if 0
|
||||
if (!_fontDataAllocated) {
|
||||
if (fontFile.open("hgc_graf.ovl")) {
|
||||
// hgc_graf.ovl file found, this is font data + code. non-interleaved font data, should be 3075 bytes
|
||||
// 16 bytes per character, 128 characters, 2048 bytes of font data, starting offset 21
|
||||
fontFileSize = fontFile.size();
|
||||
if (fontFileSize == 3075) {
|
||||
// size seems to be fine
|
||||
fontData = (uint8 *)calloc(256, 32);
|
||||
_fontDataAllocated = fontData;
|
||||
|
||||
fontFile.seek(21);
|
||||
rawData = (byte *)calloc(128, 16);
|
||||
fontFile.read(rawData, 128 * 16);
|
||||
|
||||
// repeat every line 2 times to get 16x16 pixels
|
||||
for (curCharNr = 0; curCharNr < 128; curCharNr++) {
|
||||
for (curCharLine = 0; curCharLine < 8; curCharLine++) {
|
||||
fontData[0] = rawData[rawDataPos + 0];
|
||||
fontData[1] = rawData[rawDataPos + 1];
|
||||
fontData[2] = rawData[rawDataPos + 0];
|
||||
fontData[3] = rawData[rawDataPos + 1];
|
||||
rawDataPos += 2;
|
||||
fontData += 4;
|
||||
}
|
||||
}
|
||||
|
||||
free(rawData);
|
||||
|
||||
} else {
|
||||
warning("Fontfile 'hgc_graf.ovl': unexpected file size");
|
||||
}
|
||||
fontFile.close();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_fontDataAllocated) {
|
||||
// font loaded
|
||||
_fontData = _fontDataAllocated;
|
||||
_fontIsHires = true;
|
||||
|
||||
debug("AGI: Using Hercules hires font");
|
||||
|
||||
} else {
|
||||
// Continue, if no file was not found
|
||||
warning("Could not open/use file 'hgc_font' for Hercules hires font");
|
||||
if (GUI::GuiManager::hasInstance()) {
|
||||
GUI::MessageDialog dialog(_("Could not open/use file 'hgc_font' for Hercules hires font.\nIf you have such file in other AGI (Sierra) game, you can copy it to the game directory"));
|
||||
dialog.runModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
58
engines/agi/font.h
Normal file
58
engines/agi/font.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_FONT_H
|
||||
#define AGI_FONT_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class GfxFont {
|
||||
public:
|
||||
GfxFont(AgiBase *vm);
|
||||
~GfxFont();
|
||||
|
||||
private:
|
||||
AgiBase *_vm;
|
||||
|
||||
public:
|
||||
void init();
|
||||
const byte *getFontData();
|
||||
bool isFontHires();
|
||||
|
||||
private:
|
||||
void overwriteSaveRestoreDialogCharacter();
|
||||
void overwriteExtendedWithRussianSet();
|
||||
|
||||
void loadFontScummVMFile(const Common::Path &fontFilename);
|
||||
void loadFontMickey();
|
||||
void loadFontAmigaPseudoTopaz();
|
||||
void loadFontAppleIIgs();
|
||||
void loadFontAtariST(const Common::Path &fontFilename);
|
||||
void loadFontHercules();
|
||||
|
||||
const uint8 *_fontData; // pointer to the currently used font
|
||||
uint8 *_fontDataAllocated;
|
||||
bool _fontIsHires;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_FONT_H */
|
||||
368
engines/agi/global.cpp
Normal file
368
engines/agi/global.cpp
Normal file
@@ -0,0 +1,368 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/config-manager.h"
|
||||
#include "audio/mixer.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define VM_VAR_GOLDRUSH_CYCLE_COUNT 156
|
||||
#define VM_VAR_GOLDRUSH_CYCLES_PER_SECOND 166
|
||||
|
||||
bool AgiBase::getFlag(int16 flagNr) {
|
||||
uint8 *flagPtr = _game.flags;
|
||||
|
||||
flagPtr += flagNr >> 3;
|
||||
return (*flagPtr & (1 << (flagNr & 0x07))) != 0;
|
||||
}
|
||||
|
||||
void AgiBase::setFlag(int16 flagNr, bool newState) {
|
||||
uint8 *flagPtr = _game.flags;
|
||||
|
||||
flagPtr += flagNr >> 3;
|
||||
if (newState)
|
||||
*flagPtr |= 1 << (flagNr & 0x07); // set bit
|
||||
else
|
||||
*flagPtr &= ~(1 << (flagNr & 0x07)); // clear bit
|
||||
}
|
||||
|
||||
void AgiBase::flipFlag(int16 flagNr) {
|
||||
uint8 *flagPtr = _game.flags;
|
||||
|
||||
flagPtr += flagNr >> 3;
|
||||
*flagPtr ^= 1 << (flagNr & 0x07); // flip bit
|
||||
}
|
||||
|
||||
void AgiBase::setFlagOrVar(int16 flagNr, bool newState) {
|
||||
if (getVersion() < 0x2000) {
|
||||
_game.vars[flagNr] = (newState ? 1 : 0);
|
||||
} else {
|
||||
setFlag(flagNr, newState);
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::setVar(int16 varNr, byte newValue) {
|
||||
byte oldValue = _game.vars[varNr];
|
||||
_game.vars[varNr] = newValue;
|
||||
|
||||
switch (varNr) {
|
||||
case VM_VAR_SECONDS:
|
||||
setVarSecondsTrigger(newValue);
|
||||
break;
|
||||
case VM_VAR_VOLUME:
|
||||
applyVolumeToMixer();
|
||||
break;
|
||||
case VM_VAR_GOLDRUSH_CYCLES_PER_SECOND:
|
||||
if (getGameID() == GID_GOLDRUSH) {
|
||||
goldRushClockTimeWorkaround_OnWriteVar(oldValue);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
byte AgiEngine::getVar(int16 varNr) {
|
||||
switch (varNr) {
|
||||
case VM_VAR_SECONDS:
|
||||
getVarSecondsHeuristicTrigger();
|
||||
// fall through
|
||||
case VM_VAR_MINUTES:
|
||||
case VM_VAR_HOURS:
|
||||
case VM_VAR_DAYS:
|
||||
// Timer Update is necessary in here, because of at least Manhunter 1 script 153
|
||||
// Sierra AGI updated the timer via a timer procedure
|
||||
inGameTimerUpdate();
|
||||
break;
|
||||
case VM_VAR_GOLDRUSH_CYCLES_PER_SECOND:
|
||||
if (getGameID() == GID_GOLDRUSH) {
|
||||
goldRushClockTimeWorkaround_OnReadVar();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return _game.vars[varNr];
|
||||
}
|
||||
|
||||
// sets volume to mixer using game variable for volume where 0 is maximum volume to 15 being mute
|
||||
void AgiEngine::applyVolumeToMixer() {
|
||||
debug(2, "applyVolumeToMixer() volume: %d _veryFirstInitialCycle: %d getFeatures(): %d gameId: %d", _game.vars[VM_VAR_VOLUME], _veryFirstInitialCycle, getFeatures(), getGameID());
|
||||
|
||||
byte gameVolume = CLIP<byte>(_game.vars[VM_VAR_VOLUME], 0, 15);
|
||||
|
||||
if (_veryFirstInitialCycle) {
|
||||
// WORKAROUND:
|
||||
// The very first cycle is currently running and volume got changed
|
||||
// This is surely the initial value. For plenty of fan games, a default of 15 is set
|
||||
// Which actually means "mute" in AGI, but AGI on PC used PC speaker, which did not use
|
||||
// volume setting. We do. So we detect such a situation and set a flag, so that the
|
||||
// volume will get interpreted "correctly" for those fan games.
|
||||
// Note: not all fan games are broken in that regard!
|
||||
// See bug #7035
|
||||
if (getFeatures() & GF_FANMADE) {
|
||||
// We only check for fan games, Sierra always did it properly of course
|
||||
if (gameVolume == 15) {
|
||||
// Volume gets set to mute at the start? Probably broken fan game detected so set flag
|
||||
debug(1, "Broken volume in fan game detected, enabling workaround");
|
||||
_setVolumeBrokenFangame = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_setVolumeBrokenFangame) {
|
||||
// In AGI 15 is mute, 0 is loudest
|
||||
// Some fan games set this incorrectly as 15 for loudest, 0 for mute
|
||||
gameVolume = 15 - gameVolume; // turn volume around
|
||||
}
|
||||
|
||||
int musicVolume = ConfMan.getInt("music_volume");
|
||||
int soundEffectVolume = ConfMan.getInt("sfx_volume");
|
||||
|
||||
musicVolume *= gameVolume;
|
||||
musicVolume /= 15;
|
||||
|
||||
soundEffectVolume *= gameVolume;
|
||||
soundEffectVolume /= 15;
|
||||
|
||||
musicVolume = CLIP<int>(musicVolume, 0, Audio::Mixer::kMaxMixerVolume);
|
||||
soundEffectVolume = CLIP<int>(soundEffectVolume, 0, Audio::Mixer::kMaxMixerVolume);
|
||||
|
||||
bool soundIsMuted = false;
|
||||
if (ConfMan.hasKey("mute")) {
|
||||
soundIsMuted = ConfMan.getBool("mute");
|
||||
}
|
||||
|
||||
_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, soundIsMuted ? 0 : musicVolume);
|
||||
_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, soundIsMuted ? 0 : soundEffectVolume);
|
||||
}
|
||||
|
||||
void AgiEngine::resetGetVarSecondsHeuristic() {
|
||||
_getVarSecondsHeuristicLastInstructionCounter = 0;
|
||||
_getVarSecondsHeuristicCounter = 0;
|
||||
}
|
||||
|
||||
// Called, when the scripts read VM_VAR_SECONDS
|
||||
void AgiEngine::getVarSecondsHeuristicTrigger() {
|
||||
uint32 counterDifference = _instructionCounter - _getVarSecondsHeuristicLastInstructionCounter;
|
||||
|
||||
if (counterDifference <= 3) {
|
||||
// Seconds were read within 3 instructions
|
||||
_getVarSecondsHeuristicCounter++;
|
||||
if (_getVarSecondsHeuristicCounter > 20) {
|
||||
// More than 20 times in a row? This really seems to be an inner loop waiting for seconds to change
|
||||
// This happens in at least:
|
||||
// Police Quest 1 - Poker game (room 75, responsible script 81)
|
||||
|
||||
// Wait a few milliseconds, get events and update screen
|
||||
// We MUST NOT process AGI events in here
|
||||
wait(10);
|
||||
processScummVMEvents();
|
||||
_gfx->updateScreen();
|
||||
|
||||
_getVarSecondsHeuristicCounter = 0;
|
||||
}
|
||||
} else {
|
||||
_getVarSecondsHeuristicCounter = 0;
|
||||
}
|
||||
_getVarSecondsHeuristicLastInstructionCounter = _instructionCounter;
|
||||
}
|
||||
|
||||
// In-Game timer, used for timer VM Variables
|
||||
void AgiEngine::inGameTimerReset(uint32 newPlayTime) {
|
||||
_lastUsedPlayTimeInCycles = newPlayTime / 50;
|
||||
_lastUsedPlayTimeInSeconds = newPlayTime / 1000;
|
||||
_playTimeInSecondsAdjust = 0; // no adjust for now
|
||||
setTotalPlayTime(newPlayTime);
|
||||
inGameTimerResetPassedCycles();
|
||||
}
|
||||
void AgiEngine::inGameTimerResetPassedCycles() {
|
||||
_passedPlayTimeCycles = 0;
|
||||
}
|
||||
uint32 AgiEngine::inGameTimerGet() {
|
||||
return getTotalPlayTime();
|
||||
}
|
||||
uint32 AgiEngine::inGameTimerGetPassedCycles() {
|
||||
return _passedPlayTimeCycles;
|
||||
}
|
||||
|
||||
// Seconds got set by the game
|
||||
// This happens in Mixed Up Mother Goose. The game syncs the songs to VM_VAR_SECONDS, but instead
|
||||
// of only reading them, it sets it to 0 and then checks if it reached a certain second.
|
||||
// The original interpreter didn't reset the internal cycles counter. Which means the timing was never accurate,
|
||||
// because the cycles counter may just overflow right after setting the seconds, which means a second
|
||||
// increase almost immediately happened. We even fix this issue by adjusting for it.
|
||||
void AgiEngine::setVarSecondsTrigger(byte newSeconds) {
|
||||
// Adjust in game timer, so that VM timer variables are accurate
|
||||
inGameTimerUpdate();
|
||||
|
||||
// Adjust VM seconds again
|
||||
_game.vars[VM_VAR_SECONDS] = newSeconds;
|
||||
|
||||
// Calculate milliseconds adjust (see comment above)
|
||||
uint32 curPlayTimeMilliseconds = inGameTimerGet();
|
||||
_playTimeInSecondsAdjust = curPlayTimeMilliseconds % 1000;
|
||||
}
|
||||
|
||||
// This is called, when one of the timer variables is read
|
||||
// We calculate the latest variables, according to current official playtime
|
||||
// This is also called in the main loop, because the game needs to be sync'd to 40 cycles per second
|
||||
void AgiEngine::inGameTimerUpdate() {
|
||||
uint32 curPlayTimeMilliseconds = inGameTimerGet();
|
||||
uint32 curPlayTimeCycles = curPlayTimeMilliseconds / 25;
|
||||
|
||||
if (curPlayTimeCycles == _lastUsedPlayTimeInCycles) {
|
||||
// No difference, skip updating
|
||||
return;
|
||||
}
|
||||
|
||||
// Increase passed cycles accordingly
|
||||
int32 playTimeCycleDelta = curPlayTimeCycles - _lastUsedPlayTimeInCycles;
|
||||
if (playTimeCycleDelta > 0) {
|
||||
_passedPlayTimeCycles += playTimeCycleDelta;
|
||||
}
|
||||
_lastUsedPlayTimeInCycles = curPlayTimeCycles;
|
||||
|
||||
// Now calculate current play time in seconds
|
||||
if (_playTimeInSecondsAdjust) {
|
||||
// Apply adjust from setVarSecondsTrigger()
|
||||
if (curPlayTimeMilliseconds >= _playTimeInSecondsAdjust) {
|
||||
curPlayTimeMilliseconds -= _playTimeInSecondsAdjust;
|
||||
} else {
|
||||
curPlayTimeMilliseconds = 0;
|
||||
}
|
||||
}
|
||||
uint32 curPlayTimeSeconds = curPlayTimeMilliseconds / 1000;
|
||||
|
||||
if (curPlayTimeSeconds == _lastUsedPlayTimeInSeconds) {
|
||||
// No difference, skip updating
|
||||
return;
|
||||
}
|
||||
|
||||
int32 playTimeSecondsDelta = curPlayTimeSeconds - _lastUsedPlayTimeInSeconds;
|
||||
|
||||
if (playTimeSecondsDelta > 0) {
|
||||
// Read and write to VM vars directly to avoid endless loop
|
||||
uint32 secondsLeft = playTimeSecondsDelta;
|
||||
byte curSeconds = _game.vars[VM_VAR_SECONDS];
|
||||
byte curMinutes = _game.vars[VM_VAR_MINUTES];
|
||||
byte curHours = _game.vars[VM_VAR_HOURS];
|
||||
byte curDays = _game.vars[VM_VAR_DAYS];
|
||||
|
||||
// Add delta to VM variables
|
||||
if (secondsLeft >= 86400) {
|
||||
curDays += secondsLeft / 86400;
|
||||
secondsLeft = secondsLeft % 86400;
|
||||
}
|
||||
if (secondsLeft >= 3600) {
|
||||
curHours += secondsLeft / 3600;
|
||||
secondsLeft = secondsLeft % 3600;
|
||||
}
|
||||
if (secondsLeft >= 60) {
|
||||
curMinutes += secondsLeft / 60;
|
||||
secondsLeft = secondsLeft % 60;
|
||||
}
|
||||
curSeconds += secondsLeft;
|
||||
|
||||
while (curSeconds > 59) {
|
||||
curSeconds -= 60;
|
||||
curMinutes++;
|
||||
}
|
||||
while (curMinutes > 59) {
|
||||
curMinutes -= 60;
|
||||
curHours++;
|
||||
}
|
||||
while (curHours > 23) {
|
||||
curHours -= 24;
|
||||
curDays++;
|
||||
}
|
||||
|
||||
// directly set them
|
||||
_game.vars[VM_VAR_SECONDS] = curSeconds;
|
||||
_game.vars[VM_VAR_MINUTES] = curMinutes;
|
||||
_game.vars[VM_VAR_HOURS] = curHours;
|
||||
_game.vars[VM_VAR_DAYS] = curDays;
|
||||
}
|
||||
|
||||
_lastUsedPlayTimeInSeconds = curPlayTimeSeconds;
|
||||
}
|
||||
|
||||
void AgiEngine::decrypt(uint8 *mem, int len) {
|
||||
const uint8 *key = (getFeatures() & GF_AGDS) ? (const uint8 *)CRYPT_KEY_AGDS
|
||||
: (const uint8 *)CRYPT_KEY_SIERRA;
|
||||
for (int i = 0; i < len; i++)
|
||||
*(mem + i) ^= *(key + (i % 11));
|
||||
}
|
||||
|
||||
// WORKAROUND: Gold Rush runs a speed test to calculate how fast the in-game
|
||||
// clock should advance at Fast and Fastest settings, based on CPU speed.
|
||||
// The goal was to produce a real-time clock, even though it's really driven
|
||||
// by game cycles. This test is incompatible with our speed throttling because
|
||||
// it runs in Fastest mode and the results are based on running unthrottled.
|
||||
// This causes an artificially poor test result, resulting in the clock running
|
||||
// much too fast at Fast and Fastest speeds. On Apple II and Apple IIgs, there
|
||||
// was no speed setting. GR ran unthrottled and the clock script was hard-coded
|
||||
// with values based on the expected CPU speed. We add speed settings to these
|
||||
// versions, so we need to replace these values and sync them with game speed.
|
||||
// We fix all of this by overriding the cycles-per-clock-second variable with
|
||||
// values that match the actual game speed. Fixes bugs #4147, #13910
|
||||
void AgiEngine::goldRushClockTimeWorkaround_OnReadVar() {
|
||||
const byte grCyclesPerSecond[4] = {
|
||||
40, // Fastest: 40 game cycles per clock second
|
||||
20, // Fast: 20 game cycles per clock second
|
||||
10, // Normal: 10 game cycles per clock second
|
||||
6 // Slow: 6 game cycles per clock second
|
||||
};
|
||||
|
||||
// Determine the correct number of game cycles per clock second
|
||||
byte cyclesPerSecond;
|
||||
switch (getPlatform()) {
|
||||
case Common::kPlatformApple2:
|
||||
case Common::kPlatformApple2GS:
|
||||
cyclesPerSecond = grCyclesPerSecond[MIN<byte>(_game.speedLevel, 3)];
|
||||
break;
|
||||
case Common::kPlatformDOS:
|
||||
setFlag(172, 0); // allow Fastest speed in version 3.0
|
||||
// fall through
|
||||
default:
|
||||
cyclesPerSecond = grCyclesPerSecond[MIN<byte>(getVar(VM_VAR_TIME_DELAY), 3)];
|
||||
break;
|
||||
}
|
||||
|
||||
// When the changing speed, reset the cycle counter. The script
|
||||
// that resets the counter was removed from A2/A2GS versions.
|
||||
if (cyclesPerSecond != _game.vars[VM_VAR_GOLDRUSH_CYCLES_PER_SECOND]) {
|
||||
_game.vars[VM_VAR_GOLDRUSH_CYCLES_PER_SECOND] = cyclesPerSecond;
|
||||
_game.vars[VM_VAR_GOLDRUSH_CYCLE_COUNT] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::goldRushClockTimeWorkaround_OnWriteVar(byte oldValue) {
|
||||
// Ignore attempts from game scripts to set the cycles per second.
|
||||
// Apple II sets it to 10 on every cycle, and that would confuse
|
||||
// the change detection in goldRushClockTimeWorkaround_OnReadVar.
|
||||
_game.vars[VM_VAR_GOLDRUSH_CYCLES_PER_SECOND] = oldValue;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
1570
engines/agi/graphics.cpp
Normal file
1570
engines/agi/graphics.cpp
Normal file
File diff suppressed because it is too large
Load Diff
221
engines/agi/graphics.h
Normal file
221
engines/agi/graphics.h
Normal file
@@ -0,0 +1,221 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_GRAPHICS_H
|
||||
#define AGI_GRAPHICS_H
|
||||
|
||||
#include "agi/font.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define SCRIPT_WIDTH 160
|
||||
#define SCRIPT_HEIGHT 168
|
||||
#define VISUAL_WIDTH 160
|
||||
#define VISUAL_HEIGHT 200
|
||||
#define DISPLAY_DEFAULT_WIDTH 320
|
||||
#define DISPLAY_DEFAULT_HEIGHT 200
|
||||
|
||||
enum GfxScreenUpscaledMode {
|
||||
DISPLAY_UPSCALED_DISABLED = 0,
|
||||
DISPLAY_UPSCALED_640x400 = 1
|
||||
};
|
||||
|
||||
class AgiBase;
|
||||
|
||||
enum GfxScreenMasks {
|
||||
GFX_SCREEN_MASK_VISUAL = 1,
|
||||
GFX_SCREEN_MASK_PRIORITY = 2,
|
||||
GFX_SCREEN_MASK_ALL = GFX_SCREEN_MASK_VISUAL | GFX_SCREEN_MASK_PRIORITY
|
||||
};
|
||||
|
||||
struct MouseCursorData {
|
||||
const byte *bitmapData;
|
||||
byte *bitmapDataAllocated;
|
||||
uint16 width;
|
||||
uint16 height;
|
||||
int hotspotX;
|
||||
int hotspotY;
|
||||
};
|
||||
|
||||
class GfxMgr {
|
||||
private:
|
||||
AgiBase *_vm;
|
||||
GfxFont *_font;
|
||||
|
||||
uint8 _paletteGfxMode[256 * 3];
|
||||
uint8 _paletteTextMode[256 * 3];
|
||||
|
||||
uint8 _agipalPalette[16 * 3];
|
||||
int _agipalFileNum;
|
||||
|
||||
public:
|
||||
GfxMgr(AgiBase *vm, GfxFont *font);
|
||||
|
||||
void initVideo();
|
||||
void deinitVideo();
|
||||
static void initPalette(uint8 *destPalette, const uint8 *paletteData, uint colorCount = 16, uint fromBits = 6, uint toBits = 8);
|
||||
static void initPaletteCLUT(uint8 *destPalette, const uint16 *paletteCLUTData, uint colorCount = 16);
|
||||
void setAGIPal(int);
|
||||
int getAGIPalFileNum() const;
|
||||
void setPalette(bool GfxModePalette);
|
||||
|
||||
void initMouseCursor(MouseCursorData *mouseCursor, const byte *bitmapData, uint16 width, uint16 height, int hotspotX, int hotspotY);
|
||||
void setMouseCursor(bool busy = false);
|
||||
|
||||
void setRenderStartOffset(uint16 offsetY);
|
||||
uint16 getRenderStartDisplayOffsetY() const;
|
||||
|
||||
void translateGamePosToDisplayScreen(int16 &x, int16 &y) const;
|
||||
void translateVisualPosToDisplayScreen(int16 &x, int16 &y) const;
|
||||
void translateDisplayPosToGameScreen(int16 &x, int16 &y) const;
|
||||
|
||||
void translateVisualDimensionToDisplayScreen(int16 &width, int16 &height) const;
|
||||
void translateDisplayDimensionToVisualScreen(int16 &width, int16 &height) const;
|
||||
|
||||
void translateGameRectToDisplayScreen(int16 &x, int16 &y, int16 &width, int16 &height) const;
|
||||
void translateVisualRectToDisplayScreen(int16 &x, int16 &y, int16 &width, int16 &height) const;
|
||||
|
||||
uint32 getDisplayOffsetToGameScreenPos(int16 x, int16 y) const;
|
||||
uint32 getDisplayOffsetToVisualScreenPos(int16 x, int16 y) const;
|
||||
|
||||
void copyDisplayRectToScreen(int16 x, int16 y, int16 width, int16 height);
|
||||
void copyDisplayRectToScreen(int16 x, int16 adjX, int16 y, int16 adjY, int16 width, int16 adjWidth, int16 height, int16 adjHeight);
|
||||
void copyDisplayRectToScreenUsingGamePos(int16 x, int16 y, int16 width, int16 height);
|
||||
void copyDisplayRectToScreenUsingVisualPos(int16 x, int16 y, int16 width, int16 height);
|
||||
void copyDisplayToScreen();
|
||||
|
||||
void translateFontPosToDisplayScreen(int16 &x, int16 &y) const;
|
||||
void translateDisplayPosToFontScreen(int16 &x, int16 &y) const;
|
||||
void translateFontDimensionToDisplayScreen(int16 &width, int16 &height) const;
|
||||
void translateFontRectToDisplayScreen(int16 &x, int16 &y, int16 &width, int16 &height) const;
|
||||
Common::Rect getFontRectForDisplayScreen(int16 column, int16 row, int16 width, int16 height) const;
|
||||
|
||||
private:
|
||||
uint _pixels;
|
||||
uint _displayPixels;
|
||||
|
||||
byte *_activeScreen;
|
||||
byte *_gameScreen; // 160x168 - screen, where the actual game content is drawn to (actual graphics, not including status line, prompt, etc.)
|
||||
byte *_priorityScreen; // 160x168 - screen contains priority information of the game screen
|
||||
// the term "visual screen" is effectively the display screen, but at 160x200 resolution. Used for coordinate translation
|
||||
byte *_displayScreen; // 320x200 or 640x400 - screen, that the game is rendered to and which is then copied to framebuffer
|
||||
|
||||
uint16 _displayScreenWidth;
|
||||
uint16 _displayScreenHeight;
|
||||
|
||||
uint16 _displayFontWidth;
|
||||
uint16 _displayFontHeight;
|
||||
|
||||
uint16 _displayWidthMulAdjust;
|
||||
uint16 _displayHeightMulAdjust;
|
||||
|
||||
/**
|
||||
* This variable defines, if upscaled hires is active and what upscaled mode
|
||||
* is used.
|
||||
*/
|
||||
GfxScreenUpscaledMode _upscaledHires;
|
||||
|
||||
bool _priorityTableSet;
|
||||
uint8 _priorityTable[SCRIPT_HEIGHT]; /**< priority table */
|
||||
|
||||
MouseCursorData _mouseCursor;
|
||||
MouseCursorData _mouseCursorBusy;
|
||||
|
||||
uint16 _renderStartVisualOffsetY;
|
||||
uint16 _renderStartDisplayOffsetY;
|
||||
|
||||
public:
|
||||
uint16 getDisplayScreenWidth() const {
|
||||
return _displayScreenWidth;
|
||||
}
|
||||
uint16 getDisplayFontWidth() const {
|
||||
return _displayFontWidth;
|
||||
}
|
||||
uint16 getDisplayFontHeight() const {
|
||||
return _displayFontHeight;
|
||||
}
|
||||
|
||||
GfxScreenUpscaledMode getUpscaledHires() const {
|
||||
return _upscaledHires;
|
||||
}
|
||||
|
||||
void debugShowMap(int mapNr);
|
||||
|
||||
void clear(byte color, byte priority);
|
||||
void clearDisplay(byte color, bool copyToScreen = true);
|
||||
void putPixel(int16 x, int16 y, byte drawMask, byte color, byte priority);
|
||||
void putPixelOnDisplay(int16 x, int16 y, byte color);
|
||||
void putPixelOnDisplay(int16 x, int16 adjX, int16 y, int16 adjY, byte color);
|
||||
void putFontPixelOnDisplay(int16 baseX, int16 baseY, int16 addX, int16 addY, byte color, bool isHires);
|
||||
|
||||
byte getColor(int16 x, int16 y) const;
|
||||
byte getPriority(int16 x, int16 y) const;
|
||||
bool checkControlPixel(int16 x, int16 y, byte newPriority) const;
|
||||
|
||||
byte getCGAMixtureColor(byte color) const;
|
||||
|
||||
void render_Block(int16 x, int16 y, int16 width, int16 height, bool copyToScreen = true);
|
||||
|
||||
private:
|
||||
static bool render_Clip(int16 &x, int16 &y, int16 &width, int16 &height, const int16 minY, const int16 clipAgainstWidth, const int16 clipAgainstHeight);
|
||||
void render_BlockEGA(int16 x, int16 y, int16 width, int16 height);
|
||||
void render_BlockCGA(int16 x, int16 y, int16 width, int16 height);
|
||||
void render_BlockHercules(int16 x, int16 y, int16 width, int16 height);
|
||||
|
||||
public:
|
||||
void transition_Amiga();
|
||||
void transition_AtariSt();
|
||||
|
||||
void block_save(int16 x, int16 y, int16 width, int16 height, byte *bufferPtr) const;
|
||||
void block_restore(int16 x, int16 y, int16 width, int16 height, byte *bufferPtr);
|
||||
|
||||
void drawBox(int16 x, int16 y, int16 width, int16 height, byte backgroundColor, byte lineColor);
|
||||
void drawDisplayRect(int16 x, int16 y, int16 width, int16 height, byte color, bool copyToScreen = true);
|
||||
void drawDisplayRect(int16 x, int16 adjX, int16 y, int16 adjY, int16 width, int16 adjWidth, int16 height, int16 adjHeight, byte color, bool copyToScreen = true);
|
||||
private:
|
||||
void drawDisplayRectEGA(int16 x, int16 y, int16 width, int16 height, byte color);
|
||||
void drawDisplayRectCGA(int16 x, int16 y, int16 width, int16 height, byte color);
|
||||
|
||||
public:
|
||||
void drawCharacter(int16 row, int16 column, byte character, byte foreground, byte background, bool disabledLook);
|
||||
void drawStringOnDisplay(int16 x, int16 y, const char *text, byte foreground, byte background);
|
||||
void drawStringOnDisplay(int16 x, int16 adjX, int16 y, int16 adjY, const char *text, byte foregroundColor, byte backgroundColor);
|
||||
void drawCharacterOnDisplay(int16 x, int16 y, byte character, byte foreground, byte background, byte transformXOR = 0, byte transformOR = 0);
|
||||
|
||||
void shakeScreen(int16 repeatCount);
|
||||
void updateScreen();
|
||||
|
||||
void initPriorityTable();
|
||||
static void createDefaultPriorityTable(uint8 *priorityTable);
|
||||
void setPriorityTable(int16 priorityBase);
|
||||
bool saveLoadWasPriorityTableModified() const;
|
||||
int16 saveLoadGetPriority(int16 yPos) const;
|
||||
void saveLoadSetPriorityTableModifiedBool(bool wasModified);
|
||||
void saveLoadSetPriority(int16 yPos, int16 priority);
|
||||
void saveLoadFigureOutPriorityTableModifiedBool();
|
||||
|
||||
int16 priorityToY(int16 priority) const;
|
||||
int16 priorityFromY(int16 yPos) const;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_GRAPHICS_H */
|
||||
260
engines/agi/inv.cpp
Normal file
260
engines/agi/inv.cpp
Normal file
@@ -0,0 +1,260 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/inv.h"
|
||||
#include "agi/text.h"
|
||||
#include "agi/keyboard.h"
|
||||
#include "agi/systemui.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
InventoryMgr::InventoryMgr(AgiEngine *agi, GfxMgr *gfx, TextMgr *text, SystemUI *systemUI) {
|
||||
_vm = agi;
|
||||
_gfx = gfx;
|
||||
_text = text;
|
||||
_systemUI = systemUI;
|
||||
|
||||
_activeItemNr = -1;
|
||||
}
|
||||
|
||||
InventoryMgr::~InventoryMgr() {
|
||||
}
|
||||
|
||||
void InventoryMgr::getPlayerInventory() {
|
||||
AgiGame game = _vm->_game;
|
||||
int16 selectedInventoryItem = _vm->getVar(VM_VAR_SELECTED_INVENTORY_ITEM);
|
||||
int16 curRow = 2; // starting at position 2,1
|
||||
int16 curColumn = 1;
|
||||
|
||||
_array.clear();
|
||||
_activeItemNr = 0;
|
||||
|
||||
for (uint16 objectNr = 0; objectNr < game.numObjects; objectNr++) {
|
||||
if (_vm->objectGetLocation(objectNr) == EGO_OWNED) {
|
||||
// item is in the possession of ego, so add it to our internal list
|
||||
if (objectNr == selectedInventoryItem) {
|
||||
// it's the currently selected inventory item, remember that
|
||||
_activeItemNr = _array.size();
|
||||
}
|
||||
|
||||
InventoryEntry inventoryEntry;
|
||||
|
||||
inventoryEntry.objectNr = objectNr;
|
||||
inventoryEntry.name = _vm->objectName(objectNr);
|
||||
inventoryEntry.row = curRow;
|
||||
inventoryEntry.column = curColumn;
|
||||
if (!_vm->isLanguageRTL()) {
|
||||
if (inventoryEntry.column > 1) {
|
||||
// right side, adjust column accordingly
|
||||
inventoryEntry.column -= Common::strnlen(inventoryEntry.name, FONT_COLUMN_CHARACTERS);
|
||||
}
|
||||
} else {
|
||||
// mirror the sides
|
||||
if (inventoryEntry.column == 1) {
|
||||
// right side, adjust column accordingly
|
||||
inventoryEntry.column = FONT_COLUMN_CHARACTERS - 1 - Common::strnlen(inventoryEntry.name, FONT_COLUMN_CHARACTERS);
|
||||
} else {
|
||||
// left side, adjust column accordingly
|
||||
inventoryEntry.column = 1;
|
||||
}
|
||||
}
|
||||
_array.push_back(inventoryEntry);
|
||||
|
||||
// go to next position
|
||||
if (curColumn == 1) {
|
||||
// current position is left side, go to right side
|
||||
curColumn = 39;
|
||||
} else {
|
||||
// current position is right side, so go to left side again and new row
|
||||
curColumn = 1;
|
||||
curRow++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_array.size() == 0) {
|
||||
// empty inventory
|
||||
InventoryEntry inventoryEntry;
|
||||
|
||||
inventoryEntry.objectNr = 0;
|
||||
inventoryEntry.name = _systemUI->getInventoryTextNothing();
|
||||
inventoryEntry.row = 2;
|
||||
inventoryEntry.column = 19 - (strlen(inventoryEntry.name) / 2);
|
||||
_array.push_back(inventoryEntry);
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryMgr::drawAll() {
|
||||
int16 inventoryCount = _array.size();
|
||||
|
||||
_text->charPos_Set(0, 11);
|
||||
_text->displayText(_systemUI->getInventoryTextYouAreCarrying());
|
||||
#ifdef USE_TTS
|
||||
_vm->sayText(_systemUI->getInventoryTextYouAreCarrying(), Common::TextToSpeechManager::INTERRUPT, false);
|
||||
#endif
|
||||
|
||||
for (int16 inventoryNr = 0; inventoryNr < inventoryCount; inventoryNr++) {
|
||||
drawItem(inventoryNr);
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryMgr::drawItem(int16 itemNr) {
|
||||
if (itemNr == _activeItemNr) {
|
||||
_text->charAttrib_Set(15, 0);
|
||||
|
||||
#ifdef USE_TTS
|
||||
if (_vm->_queueNextText) {
|
||||
_vm->sayText(_array[itemNr].name, Common::TextToSpeechManager::QUEUE, false);
|
||||
_vm->_queueNextText = false;
|
||||
} else {
|
||||
_vm->sayText(_array[itemNr].name, Common::TextToSpeechManager::INTERRUPT, false);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
_text->charAttrib_Set(0, 15);
|
||||
|
||||
#ifdef USE_TTS
|
||||
if (_activeItemNr == -1) {
|
||||
_vm->sayText(_array[itemNr].name, Common::TextToSpeechManager::QUEUE, false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
_text->charPos_Set(_array[itemNr].row, _array[itemNr].column);
|
||||
// original interpreter used printf here
|
||||
// this doesn't really make sense, because for length calculation it's using strlen without printf
|
||||
// which means right-aligned inventory items on the right side would not be displayed properly
|
||||
// in case printf-formatting was actually used
|
||||
// I have to assume that no game uses this, because behavior in original interpreter would have been buggy.
|
||||
_text->displayText(_array[itemNr].name);
|
||||
}
|
||||
|
||||
void InventoryMgr::show() {
|
||||
bool selectItems = false;
|
||||
|
||||
// figure out current inventory of the player
|
||||
getPlayerInventory();
|
||||
|
||||
if (_vm->getFlag(VM_FLAG_STATUS_SELECTS_ITEMS)) {
|
||||
selectItems = true;
|
||||
} else {
|
||||
_activeItemNr = -1; // so that none is shown as active
|
||||
}
|
||||
|
||||
#ifdef USE_TTS
|
||||
_vm->_queueNextText = true;
|
||||
#endif
|
||||
drawAll();
|
||||
|
||||
_text->charAttrib_Set(0, 15);
|
||||
if (selectItems) {
|
||||
_text->charPos_Set(24, 2);
|
||||
_text->displayText(_systemUI->getInventoryTextSelectItems());
|
||||
#ifdef USE_TTS
|
||||
_vm->sayText(_systemUI->getInventoryTextSelectItems());
|
||||
#endif
|
||||
} else {
|
||||
_text->charPos_Set(24, 4);
|
||||
_text->displayText(_systemUI->getInventoryTextReturnToGame());
|
||||
#ifdef USE_TTS
|
||||
_vm->sayText(_systemUI->getInventoryTextReturnToGame());
|
||||
#endif
|
||||
}
|
||||
|
||||
if (selectItems) {
|
||||
_vm->cycleInnerLoopActive(CYCLE_INNERLOOP_INVENTORY);
|
||||
|
||||
do {
|
||||
_vm->processAGIEvents();
|
||||
} while (_vm->cycleInnerLoopIsActive() && !(_vm->shouldQuit() || _vm->_restartGame));
|
||||
|
||||
if (_activeItemNr >= 0) {
|
||||
// pass selected object number
|
||||
_vm->setVar(VM_VAR_SELECTED_INVENTORY_ITEM, _array[_activeItemNr].objectNr);
|
||||
} else {
|
||||
// nothing was selected
|
||||
_vm->setVar(VM_VAR_SELECTED_INVENTORY_ITEM, 0xff);
|
||||
}
|
||||
#ifdef USE_TTS
|
||||
_vm->stopTextToSpeech();
|
||||
#endif
|
||||
|
||||
} else {
|
||||
// no selection is supposed to be possible, just wait for key and exit
|
||||
_vm->waitAnyKey();
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryMgr::keyPress(uint16 newKey) {
|
||||
switch (newKey) {
|
||||
case AGI_KEY_ENTER: {
|
||||
_vm->cycleInnerLoopInactive(); // exit show-loop
|
||||
break;
|
||||
}
|
||||
|
||||
case AGI_KEY_ESCAPE: {
|
||||
_vm->cycleInnerLoopInactive(); // exit show-loop
|
||||
_activeItemNr = -1; // no item selected
|
||||
break;
|
||||
}
|
||||
|
||||
case AGI_KEY_UP:
|
||||
changeActiveItem(-2);
|
||||
break;
|
||||
case AGI_KEY_DOWN:
|
||||
changeActiveItem(+2);
|
||||
break;
|
||||
case AGI_KEY_LEFT:
|
||||
if (!_vm->isLanguageRTL())
|
||||
changeActiveItem(-1);
|
||||
else
|
||||
changeActiveItem(+1);
|
||||
break;
|
||||
case AGI_KEY_RIGHT:
|
||||
if (!_vm->isLanguageRTL())
|
||||
changeActiveItem(+1);
|
||||
else
|
||||
changeActiveItem(-1);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void InventoryMgr::changeActiveItem(int16 direction) {
|
||||
int16 orgItemNr = _activeItemNr;
|
||||
|
||||
_activeItemNr += direction;
|
||||
|
||||
if ((_activeItemNr >= 0) && (_activeItemNr < (int16)_array.size())) {
|
||||
// within bounds
|
||||
drawItem(orgItemNr);
|
||||
drawItem(_activeItemNr);
|
||||
} else {
|
||||
// out of bounds, revert change
|
||||
_activeItemNr = orgItemNr;
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
60
engines/agi/inv.h
Normal file
60
engines/agi/inv.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_INV_H
|
||||
#define AGI_INV_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
struct InventoryEntry {
|
||||
uint16 objectNr;
|
||||
int16 row;
|
||||
int16 column;
|
||||
const char *name;
|
||||
};
|
||||
typedef Common::Array<InventoryEntry> InventoryArray;
|
||||
|
||||
class InventoryMgr {
|
||||
private:
|
||||
GfxMgr *_gfx;
|
||||
TextMgr *_text;
|
||||
AgiEngine *_vm;
|
||||
SystemUI *_systemUI;
|
||||
|
||||
InventoryArray _array;
|
||||
int16 _activeItemNr;
|
||||
|
||||
public:
|
||||
InventoryMgr(AgiEngine *agi, GfxMgr *gfx, TextMgr *text, SystemUI *systemUI);
|
||||
~InventoryMgr();
|
||||
|
||||
void getPlayerInventory();
|
||||
void drawAll();
|
||||
void drawItem(int16 itemNr);
|
||||
void show();
|
||||
|
||||
void keyPress(uint16 newKey);
|
||||
void changeActiveItem(int16 direction);
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_INV_H */
|
||||
718
engines/agi/keyboard.cpp
Normal file
718
engines/agi/keyboard.cpp
Normal file
@@ -0,0 +1,718 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/events.h"
|
||||
#include "gui/predictivedialog.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/keyboard.h"
|
||||
#include "agi/menu.h"
|
||||
#include "agi/text.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
//
|
||||
// IBM-PC keyboard scancodes
|
||||
//
|
||||
const uint8 scancodeTable[26] = {
|
||||
30, // A
|
||||
48, // B
|
||||
46, // C
|
||||
32, // D
|
||||
18, // E
|
||||
33, // F
|
||||
34, // G
|
||||
35, // H
|
||||
23, // I
|
||||
36, // J
|
||||
37, // K
|
||||
38, // L
|
||||
50, // M
|
||||
49, // N
|
||||
24, // O
|
||||
25, // P
|
||||
16, // Q
|
||||
19, // R
|
||||
31, // S
|
||||
20, // T
|
||||
22, // U
|
||||
47, // V
|
||||
17, // W
|
||||
45, // X
|
||||
21, // Y
|
||||
44 // Z
|
||||
};
|
||||
|
||||
void AgiEngine::processScummVMEvents() {
|
||||
Common::Event event;
|
||||
int key = 0;
|
||||
|
||||
while (_eventMan->pollEvent(event)) {
|
||||
switch (event.type) {
|
||||
case Common::EVENT_PREDICTIVE_DIALOG:
|
||||
showPredictiveDialog();
|
||||
break;
|
||||
case Common::EVENT_LBUTTONDOWN:
|
||||
if (_game.mouseEnabled) {
|
||||
key = AGI_MOUSE_BUTTON_LEFT;
|
||||
_mouse.button = kAgiMouseButtonLeft;
|
||||
keyEnqueue(key);
|
||||
_mouse.pos.x = event.mouse.x;
|
||||
_mouse.pos.y = event.mouse.y;
|
||||
}
|
||||
break;
|
||||
case Common::EVENT_RBUTTONDOWN:
|
||||
if (_game.mouseEnabled) {
|
||||
key = AGI_MOUSE_BUTTON_RIGHT;
|
||||
_mouse.button = kAgiMouseButtonRight;
|
||||
keyEnqueue(key);
|
||||
_mouse.pos.x = event.mouse.x;
|
||||
_mouse.pos.y = event.mouse.y;
|
||||
}
|
||||
break;
|
||||
case Common::EVENT_WHEELUP:
|
||||
if (_game.mouseEnabled) {
|
||||
key = AGI_MOUSE_WHEEL_UP;
|
||||
keyEnqueue(key);
|
||||
}
|
||||
break;
|
||||
case Common::EVENT_WHEELDOWN:
|
||||
if (_game.mouseEnabled) {
|
||||
key = AGI_MOUSE_WHEEL_DOWN;
|
||||
keyEnqueue(key);
|
||||
}
|
||||
break;
|
||||
case Common::EVENT_MOUSEMOVE:
|
||||
if (_game.mouseEnabled) {
|
||||
_mouse.pos.x = event.mouse.x;
|
||||
_mouse.pos.y = event.mouse.y;
|
||||
|
||||
if (!_game.mouseFence.isEmpty()) {
|
||||
if (_mouse.pos.x < _game.mouseFence.left)
|
||||
_mouse.pos.x = _game.mouseFence.left;
|
||||
if (_mouse.pos.x > _game.mouseFence.right)
|
||||
_mouse.pos.x = _game.mouseFence.right;
|
||||
if (_mouse.pos.y < _game.mouseFence.top)
|
||||
_mouse.pos.y = _game.mouseFence.top;
|
||||
if (_mouse.pos.y > _game.mouseFence.bottom)
|
||||
_mouse.pos.y = _game.mouseFence.bottom;
|
||||
|
||||
_system->warpMouse(_mouse.pos.x, _mouse.pos.y);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case Common::EVENT_LBUTTONUP:
|
||||
case Common::EVENT_RBUTTONUP:
|
||||
if (_game.mouseEnabled) {
|
||||
_mouse.button = kAgiMouseButtonUp;
|
||||
_mouse.pos.x = event.mouse.x;
|
||||
_mouse.pos.y = event.mouse.y;
|
||||
}
|
||||
break;
|
||||
case Common::EVENT_KEYDOWN:
|
||||
key = event.kbd.ascii;
|
||||
if (event.kbd.keycode >= Common::KEYCODE_KP0 && event.kbd.keycode <= Common::KEYCODE_KP9) {
|
||||
if (!(event.kbd.flags & Common::KBD_NUM)) {
|
||||
// HACK: Num-Lock not enabled
|
||||
// We shouldn't get a valid ascii code in these cases. We fix it here, so that cursor keys
|
||||
// on the numpad work properly.
|
||||
key = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (_game._vm->getLanguage() == Common::HE_ISR && key >= 0x05d0 && key <= 0x05ea) {
|
||||
// convert to WIN-1255
|
||||
key = key - 0x05d0 + 0xe0;
|
||||
}
|
||||
|
||||
if (_game._vm->getLanguage() == Common::RU_RUS) {
|
||||
// Convert UTF16 to CP866
|
||||
if (key >= 0x400 && key <= 0x4ff) {
|
||||
if (key >= 0x440)
|
||||
key = key - 0x410 + 0xb0;
|
||||
else
|
||||
key = key - 0x410 + 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
if (_game._vm->getLanguage() == Common::FR_FRA) {
|
||||
// Convert to CP858
|
||||
if (key >= 0x80 && key <= 0xff) {
|
||||
switch (key) {
|
||||
case 0xe9:
|
||||
key = 0x82;
|
||||
break;
|
||||
case 0xe8:
|
||||
key = 0x8a;
|
||||
break;
|
||||
case 0xe7:
|
||||
key = 0x87;
|
||||
break;
|
||||
case 0xe0:
|
||||
key = 0x85;
|
||||
break;
|
||||
case 0xf9:
|
||||
key = 0x97;
|
||||
break;
|
||||
case 0xf4:
|
||||
key = 0x93;
|
||||
break;
|
||||
case 0xee:
|
||||
key = 0x8c;
|
||||
break;
|
||||
case 0xef:
|
||||
key = 0x8b;
|
||||
break;
|
||||
case 0xea:
|
||||
key = 0x88;
|
||||
break;
|
||||
case 0xeb:
|
||||
key = 0x89;
|
||||
break;
|
||||
case 0xe2:
|
||||
key = 0x83;
|
||||
break;
|
||||
case 0xe4:
|
||||
key = 0x84;
|
||||
break;
|
||||
case 0xfb:
|
||||
key = 0x96;
|
||||
break;
|
||||
case 0xfc:
|
||||
key = 0x81;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((key) && (key <= 0xFF)) {
|
||||
// No special key, directly accept it
|
||||
// Is ISO-8859-1, we need lower 128 characters only, which is plain ASCII, so no mapping required
|
||||
if (Common::isAlpha(key)) {
|
||||
// Key is A-Z.
|
||||
// Map Ctrl-A to 1, Ctrl-B to 2, etc.
|
||||
if (event.kbd.flags & Common::KBD_CTRL) {
|
||||
key = toupper(key) - 'A' + 1;
|
||||
} else if (event.kbd.flags & Common::KBD_ALT) {
|
||||
// Map Alt-A, Alt-B etc. to special scancode values according to an internal scancode table.
|
||||
key = scancodeTable[toupper(key) - 'A'] << 8;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
key = 0;
|
||||
switch (event.kbd.keycode) {
|
||||
case Common::KEYCODE_LEFT:
|
||||
case Common::KEYCODE_KP4:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_LEFT;
|
||||
break;
|
||||
case Common::KEYCODE_RIGHT:
|
||||
case Common::KEYCODE_KP6:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_RIGHT;
|
||||
break;
|
||||
case Common::KEYCODE_UP:
|
||||
case Common::KEYCODE_KP8:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_UP;
|
||||
break;
|
||||
case Common::KEYCODE_DOWN:
|
||||
case Common::KEYCODE_KP2:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_DOWN;
|
||||
break;
|
||||
case Common::KEYCODE_PAGEUP:
|
||||
case Common::KEYCODE_KP9:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_UP_RIGHT;
|
||||
break;
|
||||
case Common::KEYCODE_PAGEDOWN:
|
||||
case Common::KEYCODE_KP3:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_DOWN_RIGHT;
|
||||
break;
|
||||
case Common::KEYCODE_HOME:
|
||||
case Common::KEYCODE_KP7:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_UP_LEFT;
|
||||
break;
|
||||
case Common::KEYCODE_END:
|
||||
case Common::KEYCODE_KP1:
|
||||
if (_allowSynthetic || !event.kbdRepeat)
|
||||
key = AGI_KEY_DOWN_LEFT;
|
||||
break;
|
||||
case Common::KEYCODE_KP5:
|
||||
key = AGI_KEY_STATIONARY;
|
||||
break;
|
||||
case Common::KEYCODE_F1:
|
||||
key = AGI_KEY_F1;
|
||||
break;
|
||||
case Common::KEYCODE_F2:
|
||||
key = AGI_KEY_F2;
|
||||
break;
|
||||
case Common::KEYCODE_F3:
|
||||
key = AGI_KEY_F3;
|
||||
break;
|
||||
case Common::KEYCODE_F4:
|
||||
key = AGI_KEY_F4;
|
||||
break;
|
||||
case Common::KEYCODE_F5:
|
||||
key = AGI_KEY_F5;
|
||||
break;
|
||||
case Common::KEYCODE_F6:
|
||||
key = AGI_KEY_F6;
|
||||
break;
|
||||
case Common::KEYCODE_F7:
|
||||
key = AGI_KEY_F7;
|
||||
break;
|
||||
case Common::KEYCODE_F8:
|
||||
key = AGI_KEY_F8;
|
||||
break;
|
||||
case Common::KEYCODE_F9:
|
||||
key = AGI_KEY_F9;
|
||||
break;
|
||||
case Common::KEYCODE_F10:
|
||||
key = AGI_KEY_F10;
|
||||
break;
|
||||
case Common::KEYCODE_F11:
|
||||
key = AGI_KEY_F11;
|
||||
break;
|
||||
case Common::KEYCODE_F12:
|
||||
key = AGI_KEY_F12;
|
||||
break;
|
||||
case Common::KEYCODE_KP_ENTER:
|
||||
key = AGI_KEY_ENTER;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (event.kbd.keycode) {
|
||||
case Common::KEYCODE_LEFT:
|
||||
case Common::KEYCODE_RIGHT:
|
||||
case Common::KEYCODE_UP:
|
||||
case Common::KEYCODE_DOWN:
|
||||
case Common::KEYCODE_HOME:
|
||||
case Common::KEYCODE_END:
|
||||
case Common::KEYCODE_PAGEUP:
|
||||
case Common::KEYCODE_PAGEDOWN:
|
||||
case Common::KEYCODE_KP4:
|
||||
case Common::KEYCODE_KP6:
|
||||
case Common::KEYCODE_KP8:
|
||||
case Common::KEYCODE_KP2:
|
||||
case Common::KEYCODE_KP9:
|
||||
case Common::KEYCODE_KP3:
|
||||
case Common::KEYCODE_KP7:
|
||||
case Common::KEYCODE_KP1:
|
||||
_keyHoldModeLastKey = event.kbd.keycode;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (key)
|
||||
keyEnqueue(key);
|
||||
break;
|
||||
|
||||
case Common::EVENT_KEYUP:
|
||||
if (_keyHoldMode) {
|
||||
// Original AGI actually created direction events in here
|
||||
// but only in case the last pressed cursor key was released, in other cases it did nothing.
|
||||
// So when you pressed and held down left and then pressed up, and then released left,
|
||||
// direction wouldn't be changed at all.
|
||||
//
|
||||
// We don't create direction events in here, that's why we create a stationary event instead,
|
||||
// which will result in a direction change to 0 in handleController().
|
||||
switch (event.kbd.keycode) {
|
||||
case Common::KEYCODE_LEFT:
|
||||
case Common::KEYCODE_RIGHT:
|
||||
case Common::KEYCODE_UP:
|
||||
case Common::KEYCODE_DOWN:
|
||||
case Common::KEYCODE_HOME:
|
||||
case Common::KEYCODE_END:
|
||||
case Common::KEYCODE_PAGEUP:
|
||||
case Common::KEYCODE_PAGEDOWN:
|
||||
case Common::KEYCODE_KP4:
|
||||
case Common::KEYCODE_KP6:
|
||||
case Common::KEYCODE_KP8:
|
||||
case Common::KEYCODE_KP2:
|
||||
case Common::KEYCODE_KP9:
|
||||
case Common::KEYCODE_KP3:
|
||||
case Common::KEYCODE_KP7:
|
||||
case Common::KEYCODE_KP1:
|
||||
if (_keyHoldModeLastKey == event.kbd.keycode) {
|
||||
keyEnqueue(AGI_KEY_STATIONARY);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw key grabber.
|
||||
* poll_keyboard() is the raw key grabber (above the gfx driver, that is).
|
||||
* It handles console keys and insulates AGI from the console. In the main
|
||||
* loop, handle_keys() handles keyboard input and ego movement.
|
||||
*/
|
||||
int AgiEngine::doPollKeyboard() {
|
||||
int key = 0;
|
||||
|
||||
// If a key is ready, rip it
|
||||
if (isKeypress()) {
|
||||
key = getKeypress();
|
||||
|
||||
debugC(3, kDebugLevelInput, "key %02x pressed", key);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
bool AgiEngine::handleMouseClicks(uint16 &key) {
|
||||
// No mouse click? -> exit
|
||||
if (key != AGI_MOUSE_BUTTON_LEFT)
|
||||
return false;
|
||||
|
||||
if (!cycleInnerLoopIsActive()) {
|
||||
// Only do this, when no inner loop is currently active
|
||||
Common::Rect displayLineRect = _gfx->getFontRectForDisplayScreen(0, 0, FONT_COLUMN_CHARACTERS, 1);
|
||||
// Common::Rect displayLineRect(_gfx->getDisplayScreenWidth(), _gfx->getDisplayFontHeight());
|
||||
|
||||
if (displayLineRect.contains(_mouse.pos)) {
|
||||
// Mouse is inside first line of the screen
|
||||
if (getFlag(VM_FLAG_MENUS_ACCESSIBLE) && _menu->isAvailable()) {
|
||||
_menu->delayedExecuteViaMouse();
|
||||
key = 0; // eat event
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (_text->promptIsEnabled()) {
|
||||
// Prompt is currently enabled
|
||||
int16 promptRow = _text->promptRow_Get();
|
||||
|
||||
displayLineRect.moveTo(0, promptRow * _gfx->getDisplayFontHeight());
|
||||
|
||||
if (displayLineRect.contains(_mouse.pos)) {
|
||||
// and user clicked within the line of the prompt
|
||||
if (_game.predictiveDlgOnMouseClick) {
|
||||
showPredictiveDialog();
|
||||
}
|
||||
|
||||
key = 0; // eat event
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cycleInnerLoopIsActive()) {
|
||||
// inner loop active, check what kind of loop it is. Then process / forward it
|
||||
switch (_game.cycleInnerLoopType) {
|
||||
case CYCLE_INNERLOOP_GETSTRING:
|
||||
case CYCLE_INNERLOOP_GETNUMBER: {
|
||||
// process in here
|
||||
int16 stringRow, stringColumn, stringMaxLen;
|
||||
|
||||
_text->stringPos_Get(stringRow, stringColumn);
|
||||
stringMaxLen = _text->stringGetMaxLen();
|
||||
|
||||
Common::Rect displayRect = _gfx->getFontRectForDisplayScreen(stringColumn, stringRow, stringMaxLen, 1);
|
||||
if (displayRect.contains(_mouse.pos)) {
|
||||
// user clicked inside the input space
|
||||
if (_game.predictiveDlgOnMouseClick) {
|
||||
showPredictiveDialog();
|
||||
}
|
||||
|
||||
key = 0; // eat event
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CYCLE_INNERLOOP_INVENTORY:
|
||||
// TODO: forward
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_MENU_VIA_KEYBOARD:
|
||||
_menu->mouseEvent(key);
|
||||
key = 0; // eat event
|
||||
break;
|
||||
|
||||
case CYCLE_INNERLOOP_SYSTEMUI_SELECTSAVEDGAMESLOT:
|
||||
// TODO: forward
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AgiEngine::handleController(uint16 key) {
|
||||
ScreenObjEntry *screenObjEgo = &_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY];
|
||||
|
||||
if (key == 0) // nothing pressed
|
||||
return false;
|
||||
|
||||
// This previously skipped processing, when ESC was pressed and called menu directly.
|
||||
// This original approach was bad, because games check different flags before actually allowing the
|
||||
// user to enter the menu. We checked a few common flags, like for example the availability of the prompt.
|
||||
// But this stopped the user being able to enter the menu, when the original interpreter actually allowed it.
|
||||
// We now instead implement this feature using another way for those platforms.
|
||||
if (key == AGI_KEY_ESCAPE) {
|
||||
// Escape pressed, user probably wants to trigger the menu
|
||||
// For PC, just passing ASCII code for ESC will normally trigger a controller
|
||||
// and the scripts will then trigger the menu
|
||||
switch (getPlatform()) {
|
||||
case Common::kPlatformAmiga:
|
||||
case Common::kPlatformApple2GS:
|
||||
case Common::kPlatformAtariST:
|
||||
// For these platforms, the button ESC normally triggered "pause"
|
||||
// But users could at the same time trigger the menu by clicking on the status line
|
||||
// We check, if menu is currently available and supposed to be accessible.
|
||||
// If yes, we do a delayed trigger now, otherwise we continue processing the key just like normal.
|
||||
//
|
||||
// This is probably the solution with the highest compatibility.
|
||||
// Several games also look for special keys see AGI_MENU_TRIGGER_*
|
||||
// And then there's also Mixed Up Mother Goose, which actually hooks the ESC key for the regular menu
|
||||
//
|
||||
// We risk in here of course, that we let the user access the menu, when it shouldn't be possible.
|
||||
// I'm not 100% sure if those other interpreters really only check VM_FLAG_MENUS_ACCESSIBLE
|
||||
// Needs further investigation.
|
||||
if (getFlag(VM_FLAG_MENUS_ACCESSIBLE) && _menu->isAvailable()) {
|
||||
// menu is supposed to be accessible and is also available
|
||||
_menu->delayedExecuteViaKeyboard();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Otherwise go on and look for the ESC controller
|
||||
}
|
||||
|
||||
if ((getGameID() == GID_MH1 || getGameID() == GID_MH2) && (key == AGI_KEY_ENTER) &&
|
||||
(!_text->promptIsEnabled())) {
|
||||
key = 0x20; // Set Enter key to Space in Manhunter when prompt is disabled
|
||||
}
|
||||
|
||||
debugC(3, kDebugLevelInput, "key = %04x", key);
|
||||
|
||||
for (uint16 curMapping = 0; curMapping < MAX_CONTROLLER_KEYMAPPINGS; curMapping++) {
|
||||
if (_game.controllerKeyMapping[curMapping].keycode == key) {
|
||||
debugC(3, kDebugLevelInput, "event %d: key press", _game.controllerKeyMapping[curMapping].controllerSlot);
|
||||
_game.controllerOccurred[_game.controllerKeyMapping[curMapping].controllerSlot] = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int16 newDirection = 0;
|
||||
|
||||
switch (key) {
|
||||
case AGI_KEY_UP:
|
||||
newDirection = 1;
|
||||
break;
|
||||
case AGI_KEY_DOWN:
|
||||
newDirection = 5;
|
||||
break;
|
||||
case AGI_KEY_LEFT:
|
||||
newDirection = 7;
|
||||
break;
|
||||
case AGI_KEY_RIGHT:
|
||||
newDirection = 3;
|
||||
break;
|
||||
case AGI_KEY_UP_RIGHT:
|
||||
newDirection = 2;
|
||||
break;
|
||||
case AGI_KEY_DOWN_RIGHT:
|
||||
newDirection = 4;
|
||||
break;
|
||||
case AGI_KEY_UP_LEFT:
|
||||
newDirection = 8;
|
||||
break;
|
||||
case AGI_KEY_DOWN_LEFT:
|
||||
newDirection = 6;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (_game.playerControl) {
|
||||
if (!(getFeatures() & GF_AGIMOUSE)) {
|
||||
// Handle mouse button events
|
||||
if (!_game.mouseHidden) {
|
||||
if (key == AGI_MOUSE_BUTTON_LEFT) {
|
||||
if (getGameID() == GID_PQ1 && getVar(VM_VAR_CURRENT_ROOM) == 116) {
|
||||
// WORKAROUND: Special handling for mouse clicks in the newspaper
|
||||
// screen of PQ1. Fixes bug #4908.
|
||||
newDirection = 3; // fake a right arrow key (next page)
|
||||
|
||||
} else {
|
||||
// Click-to-walk mouse interface
|
||||
//v->flags |= fAdjEgoXY;
|
||||
// setting fAdjEgoXY here will at least break "climbing the log" in SQ2
|
||||
// in case you walked to the log by using the mouse, so don't!!!
|
||||
int16 egoDestinationX = _mouse.pos.x;
|
||||
int16 egoDestinationY = _mouse.pos.y;
|
||||
_gfx->translateDisplayPosToGameScreen(egoDestinationX, egoDestinationY);
|
||||
|
||||
screenObjEgo->motionType = kMotionEgo;
|
||||
if (egoDestinationX < (screenObjEgo->xSize / 2)) {
|
||||
screenObjEgo->move_x = -1;
|
||||
} else {
|
||||
screenObjEgo->move_x = egoDestinationX - (screenObjEgo->xSize / 2);
|
||||
}
|
||||
screenObjEgo->move_y = egoDestinationY;
|
||||
screenObjEgo->move_stepSize = screenObjEgo->stepSize;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newDirection || key == AGI_KEY_STATIONARY) {
|
||||
// TODO: not sure, what original AGI did with AdjEgoXY
|
||||
screenObjEgo->flags &= ~fAdjEgoXY;
|
||||
if (screenObjEgo->direction == newDirection) {
|
||||
setVar(VM_VAR_EGO_DIRECTION, 0);
|
||||
} else {
|
||||
setVar(VM_VAR_EGO_DIRECTION, newDirection);
|
||||
}
|
||||
if (_game.playerControl) {
|
||||
screenObjEgo->motionType = kMotionNormal;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AgiEngine::showPredictiveDialog() {
|
||||
GUI::PredictiveDialog predictiveDialog;
|
||||
|
||||
runDialog(predictiveDialog);
|
||||
|
||||
Common::String predictiveResult(predictiveDialog.getResult());
|
||||
uint16 predictiveResultLen = predictiveResult.size();
|
||||
if (predictiveResultLen) {
|
||||
// User actually entered something
|
||||
for (int16 resultPos = 0; resultPos < predictiveResultLen; resultPos++) {
|
||||
keyEnqueue(predictiveResult[resultPos]);
|
||||
}
|
||||
if (!cycleInnerLoopIsActive()) {
|
||||
if (_text->promptIsEnabled()) {
|
||||
// add ENTER, when the input is probably meant for the prompt
|
||||
keyEnqueue(AGI_KEY_ENTER);
|
||||
}
|
||||
} else {
|
||||
switch (_game.cycleInnerLoopType) {
|
||||
case CYCLE_INNERLOOP_GETSTRING:
|
||||
case CYCLE_INNERLOOP_GETNUMBER:
|
||||
// add ENTER, when the input is probably meant for GetString/GetNumber
|
||||
keyEnqueue(AGI_KEY_ENTER);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int AgiEngine::waitKey() {
|
||||
int key = 0;
|
||||
|
||||
clearKeyQueue();
|
||||
|
||||
debugC(3, kDebugLevelInput, "waiting...");
|
||||
while (!(shouldQuit() || _restartGame || getFlag(VM_FLAG_RESTORE_JUST_RAN))) {
|
||||
wait(10);
|
||||
key = doPollKeyboard();
|
||||
if (key == AGI_KEY_ENTER || key == AGI_KEY_ESCAPE || key == AGI_MOUSE_BUTTON_LEFT)
|
||||
break;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
int AgiEngine::waitAnyKey() {
|
||||
int key = 0;
|
||||
|
||||
clearKeyQueue();
|
||||
|
||||
debugC(3, kDebugLevelInput, "waiting... (any key)");
|
||||
while (!(shouldQuit() || _restartGame)) {
|
||||
wait(10);
|
||||
key = doPollKeyboard();
|
||||
if (key)
|
||||
break;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits on any key to be pressed or for a finished sound.
|
||||
* This is used on platforms where sound playback would block the
|
||||
* interpreter until the sound finished or was interrupted.
|
||||
*/
|
||||
void AgiEngine::waitAnyKeyOrFinishedSound() {
|
||||
clearKeyQueue();
|
||||
|
||||
while (!(shouldQuit() || _restartGame || !_sound->isPlaying())) {
|
||||
wait(10);
|
||||
if (doPollKeyboard()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AgiEngine::isKeypress() {
|
||||
processScummVMEvents();
|
||||
return _keyQueueStart != _keyQueueEnd;
|
||||
}
|
||||
|
||||
int AgiEngine::getKeypress() {
|
||||
int k;
|
||||
|
||||
while (_keyQueueStart == _keyQueueEnd) // block
|
||||
wait(10);
|
||||
|
||||
keyDequeue(k);
|
||||
|
||||
return k;
|
||||
}
|
||||
|
||||
void AgiEngine::clearKeyQueue() {
|
||||
while (isKeypress()) {
|
||||
getKeypress();
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
103
engines/agi/keyboard.h
Normal file
103
engines/agi/keyboard.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_KEYBOARD_H
|
||||
#define AGI_KEYBOARD_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define keyEnqueue(k) \
|
||||
do { \
|
||||
_keyQueue[_keyQueueEnd++] = (k); \
|
||||
_keyQueueEnd %= KEY_QUEUE_SIZE; \
|
||||
} while (0)
|
||||
#define keyDequeue(k) \
|
||||
do { \
|
||||
(k) = _keyQueue[_keyQueueStart++]; \
|
||||
_keyQueueStart %= KEY_QUEUE_SIZE; \
|
||||
} while (0)
|
||||
|
||||
// Class to turn on synthetic events temporarily. Usually until the end of the
|
||||
// current function.
|
||||
|
||||
class AllowSyntheticEvents {
|
||||
private:
|
||||
AgiEngine *_vm;
|
||||
public:
|
||||
AllowSyntheticEvents(AgiEngine *vm) : _vm(vm) {
|
||||
_vm->allowSynthetic(true);
|
||||
}
|
||||
|
||||
~AllowSyntheticEvents() {
|
||||
_vm->allowSynthetic(false);
|
||||
}
|
||||
};
|
||||
|
||||
#define AGI_KEY_BACKSPACE 0x08
|
||||
#define AGI_KEY_ESCAPE 0x1B
|
||||
#define AGI_KEY_ENTER 0x0D
|
||||
#define AGI_KEY_UP 0x4800
|
||||
#define AGI_KEY_DOWN 0x5000
|
||||
#define AGI_KEY_LEFT 0x4B00
|
||||
#define AGI_KEY_STATIONARY 0x4C00
|
||||
#define AGI_KEY_RIGHT 0x4D00
|
||||
|
||||
#define AGI_KEY_DOWN_LEFT 0x4F00
|
||||
#define AGI_KEY_DOWN_RIGHT 0x5100
|
||||
#define AGI_KEY_UP_LEFT 0x4700
|
||||
#define AGI_KEY_UP_RIGHT 0x4900
|
||||
|
||||
#define AGI_KEY_F1 0x3B00
|
||||
#define AGI_KEY_F2 0x3C00
|
||||
#define AGI_KEY_F3 0x3D00
|
||||
#define AGI_KEY_F4 0x3E00
|
||||
#define AGI_KEY_F5 0x3F00
|
||||
#define AGI_KEY_F6 0x4000
|
||||
#define AGI_KEY_F7 0x4100
|
||||
#define AGI_KEY_F8 0x4200
|
||||
#define AGI_KEY_F9 0x4300
|
||||
#define AGI_KEY_F10 0x4400
|
||||
#define AGI_KEY_F11 0xd900 // F11
|
||||
#define AGI_KEY_F12 0xda00 // F12
|
||||
|
||||
#define AGI_KEY_PAGE_UP 0x4900 // Page Up (fixed by Ziv Barber)
|
||||
#define AGI_KEY_PAGE_DOWN 0x5100 // Page Down
|
||||
#define AGI_KEY_HOME 0x4700 // Home
|
||||
#define AGI_KEY_END 0x4f00 // End *
|
||||
|
||||
#define AGI_MOUSE_BUTTON_LEFT 0xF101 // Left mouse button
|
||||
#define AGI_MOUSE_BUTTON_RIGHT 0xF202 // Right mouse button
|
||||
#define AGI_MOUSE_WHEEL_UP 0xF203 // Mouse wheel up
|
||||
#define AGI_MOUSE_WHEEL_DOWN 0xF204 // Mouse wheel down
|
||||
|
||||
// special menu triggers
|
||||
// Attention: at least Mixed Up Mother Goose on Apple IIgs actually hooks ESC for menu only
|
||||
// Which is why we have to check, if the corresponding trigger is hooked before changing it
|
||||
// And otherwise simply use the regular ESC.
|
||||
#define AGI_MENU_TRIGGER_PC 0x001B // will trigger menu for PC
|
||||
#define AGI_MENU_TRIGGER_APPLE2GS 0x0301 // will trigger menu for AppleIIgs + Amiga
|
||||
#define AGI_MENU_TRIGGER_ATARIST 0x0101 // will trigger menu for Atari ST
|
||||
|
||||
extern const uint8 scancodeTable[];
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_KEYBOARD_H */
|
||||
62
engines/agi/loader.cpp
Normal file
62
engines/agi/loader.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/loader.h"
|
||||
|
||||
#include "common/config-manager.h"
|
||||
#include "common/fs.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
void AgiLoader::getPotentialDiskImages(
|
||||
const char * const *imageExtensions,
|
||||
size_t imageExtensionCount,
|
||||
Common::Array<Common::Path> &imageFiles,
|
||||
FileMap &fileMap) {
|
||||
|
||||
// get all files in game directory
|
||||
Common::FSList allFiles;
|
||||
Common::FSNode dir(ConfMan.getPath("path"));
|
||||
if (!dir.getChildren(allFiles, Common::FSNode::kListFilesOnly)) {
|
||||
warning("invalid game path: %s", dir.getPath().toString(Common::Path::kNativeSeparator).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// build array of files with provided disk image extensions
|
||||
for (const Common::FSNode &file : allFiles) {
|
||||
for (size_t i = 0; i < imageExtensionCount; i++) {
|
||||
if (file.getName().hasSuffixIgnoreCase(imageExtensions[i])) {
|
||||
Common::Path path = file.getPath();
|
||||
imageFiles.push_back(path);
|
||||
fileMap[path] = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort potential image files by name.
|
||||
// this is an important step for consistent results,
|
||||
// and because the first disk is likely to be first.
|
||||
Common::sort(imageFiles.begin(), imageFiles.end());
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
218
engines/agi/loader.h
Normal file
218
engines/agi/loader.h
Normal file
@@ -0,0 +1,218 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_LOADER_H
|
||||
#define AGI_LOADER_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class AgiLoader {
|
||||
public:
|
||||
AgiLoader(AgiEngine *vm) : _vm(vm) {}
|
||||
virtual ~AgiLoader() {}
|
||||
|
||||
/**
|
||||
* Performs one-time initializations, such as locating files
|
||||
* with dynamic names.
|
||||
*/
|
||||
virtual void init() {}
|
||||
|
||||
/**
|
||||
* Loads all AGI directory entries from disk and and populates
|
||||
* the AgiDir arrays in AgiGame with them.
|
||||
*/
|
||||
virtual int loadDirs() = 0;
|
||||
|
||||
/**
|
||||
* Loads a volume resource from disk.
|
||||
*/
|
||||
virtual uint8 *loadVolumeResource(AgiDir *agid) = 0;
|
||||
|
||||
/**
|
||||
* Loads AgiEngine::_objects from disk.
|
||||
*/
|
||||
virtual int loadObjects() = 0;
|
||||
|
||||
/**
|
||||
* Loads AgiBase::_words from disk.
|
||||
*/
|
||||
virtual int loadWords() = 0;
|
||||
|
||||
protected:
|
||||
AgiEngine *_vm;
|
||||
|
||||
typedef Common::HashMap<Common::Path, Common::FSNode, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> FileMap;
|
||||
static void getPotentialDiskImages(
|
||||
const char * const *imageExtensions,
|
||||
size_t imageExtensionCount,
|
||||
Common::Array<Common::Path> &imageFiles,
|
||||
FileMap &fileMap);
|
||||
};
|
||||
|
||||
struct AgiDiskVolume {
|
||||
uint32 disk;
|
||||
uint32 offset;
|
||||
|
||||
AgiDiskVolume() : disk(_EMPTY), offset(0) {}
|
||||
AgiDiskVolume(uint32 d, uint32 o) : disk(d), offset(o) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Apple II version of the format for LOGDIR, VIEWDIR, etc.
|
||||
* See AgiLoader_A2::loadDir for more details.
|
||||
*/
|
||||
enum A2DirVersion {
|
||||
A2DirVersionOld, // 4 bits for volume, 8 for track
|
||||
A2DirVersionNew, // 5 bits for volume, 7 for track
|
||||
};
|
||||
|
||||
class AgiLoader_A2 : public AgiLoader {
|
||||
public:
|
||||
AgiLoader_A2(AgiEngine *vm) : AgiLoader(vm) {}
|
||||
~AgiLoader_A2() override;
|
||||
|
||||
void init() override;
|
||||
int loadDirs() override;
|
||||
uint8 *loadVolumeResource(AgiDir *agid) override;
|
||||
int loadObjects() override;
|
||||
int loadWords() override;
|
||||
|
||||
private:
|
||||
Common::Array<Common::SeekableReadStream *> _disks;
|
||||
Common::Array<AgiDiskVolume> _volumes;
|
||||
AgiDir _logDir;
|
||||
AgiDir _picDir;
|
||||
AgiDir _viewDir;
|
||||
AgiDir _soundDir;
|
||||
AgiDir _objects;
|
||||
AgiDir _words;
|
||||
|
||||
int readDiskOne(Common::SeekableReadStream &stream, Common::Array<uint32> &volumeMap);
|
||||
static bool readInitDir(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
|
||||
static bool readDir(Common::SeekableReadStream &stream, int position, AgiDir &agid);
|
||||
static bool readVolumeMap(Common::SeekableReadStream &stream, uint32 position, uint32 bufferLength, Common::Array<uint32> &volumeMap);
|
||||
|
||||
A2DirVersion detectDirVersion(Common::SeekableReadStream &stream) const;
|
||||
static bool loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion);
|
||||
};
|
||||
|
||||
class AgiLoader_v1 : public AgiLoader {
|
||||
public:
|
||||
AgiLoader_v1(AgiEngine *vm) : AgiLoader(vm) {}
|
||||
|
||||
void init() override;
|
||||
int loadDirs() override;
|
||||
uint8 *loadVolumeResource(AgiDir *agid) override;
|
||||
int loadObjects() override;
|
||||
int loadWords() override;
|
||||
|
||||
private:
|
||||
Common::Array<Common::String> _imageFiles;
|
||||
Common::Array<AgiDiskVolume> _volumes;
|
||||
AgiDir _logDir;
|
||||
AgiDir _picDir;
|
||||
AgiDir _viewDir;
|
||||
AgiDir _soundDir;
|
||||
AgiDir _objects;
|
||||
AgiDir _words;
|
||||
|
||||
bool readDiskOneV1(Common::SeekableReadStream &stream);
|
||||
bool readDiskOneV2001(Common::SeekableReadStream &stream, int &vol0Offset);
|
||||
static bool readInitDirV1(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
|
||||
static bool readInitDirV2001(Common::SeekableReadStream &stream, byte index, AgiDir &agid);
|
||||
|
||||
bool loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength);
|
||||
};
|
||||
|
||||
class AgiLoader_v2 : public AgiLoader {
|
||||
private:
|
||||
bool _hasV3VolumeFormat;
|
||||
|
||||
int loadDir(AgiDir *agid, const char *fname);
|
||||
bool detectV3VolumeFormat();
|
||||
|
||||
public:
|
||||
AgiLoader_v2(AgiEngine *vm) : _hasV3VolumeFormat(false), AgiLoader(vm) {}
|
||||
|
||||
int loadDirs() override;
|
||||
uint8 *loadVolumeResource(AgiDir *agid) override;
|
||||
int loadObjects() override;
|
||||
int loadWords() override;
|
||||
};
|
||||
|
||||
class AgiLoader_v3 : public AgiLoader {
|
||||
private:
|
||||
Common::String _name; /**< prefix in directory and/or volume file names (`GR' for goldrush) */
|
||||
|
||||
int loadDir(AgiDir *agid, Common::File *fp, uint32 offs, uint32 len);
|
||||
|
||||
public:
|
||||
AgiLoader_v3(AgiEngine *vm) : AgiLoader(vm) {}
|
||||
|
||||
void init() override;
|
||||
int loadDirs() override;
|
||||
uint8 *loadVolumeResource(AgiDir *agid) override;
|
||||
int loadObjects() override;
|
||||
int loadWords() override;
|
||||
};
|
||||
|
||||
class GalLoader : public AgiLoader {
|
||||
public:
|
||||
GalLoader(AgiEngine *vm) : _dirOffset(0), AgiLoader(vm) {}
|
||||
|
||||
void init() override;
|
||||
int loadDirs() override;
|
||||
uint8 *loadVolumeResource(AgiDir *agid) override;
|
||||
int loadObjects() override;
|
||||
int loadWords() override;
|
||||
|
||||
private:
|
||||
Common::String _imageFile;
|
||||
int _dirOffset;
|
||||
|
||||
static bool isDirectory(Common::SeekableReadStream &stream, uint32 dirOffset);
|
||||
static uint32 readDirectoryEntry(Common::SeekableReadStream &stream, uint32 *sectorCount);
|
||||
};
|
||||
|
||||
class GalLoader_A2 : public AgiLoader {
|
||||
public:
|
||||
GalLoader_A2(AgiEngine *vm) : AgiLoader(vm) {}
|
||||
~GalLoader_A2();
|
||||
|
||||
void init() override;
|
||||
int loadDirs() override;
|
||||
uint8 *loadVolumeResource(AgiDir *agid) override;
|
||||
int loadObjects() override;
|
||||
int loadWords() override;
|
||||
|
||||
private:
|
||||
Common::Array<Common::SeekableReadStream *> _disks;
|
||||
|
||||
static bool readDiskOne(Common::SeekableReadStream &disk, AgiDir *logicDir);
|
||||
static bool readDirectoryEntry(Common::SeekableReadStream &stream, AgiDir &dirEntry);
|
||||
static bool validateDisk(Common::SeekableReadStream &disk, byte diskIndex, AgiDir *logicDir);
|
||||
|
||||
static bool loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirCount);
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_LOADER_H */
|
||||
463
engines/agi/loader_a2.cpp
Normal file
463
engines/agi/loader_a2.cpp
Normal file
@@ -0,0 +1,463 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/disk_image.h"
|
||||
#include "agi/loader.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "common/formats/disk_image.h"
|
||||
#include "common/fs.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/substream.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// AgiLoader_A2 reads Apple II floppy disk images.
|
||||
//
|
||||
// Floppy disks have two sides; each side is a disk with its own image file.
|
||||
// All disk sides are 140k with 35 tracks and 16 sectors per track.
|
||||
//
|
||||
// Multiple disk image formats are supported; see Common::DiskImage. The file
|
||||
// extension determines the format. For example: .do, .dsk, .nib, .woz.
|
||||
//
|
||||
// The disks do not use a standard file system. Instead, file locations are
|
||||
// stored in an INITDIR structure at a fixed location. KQ2 and BC don't have
|
||||
// INITDIR, so we use the known locations of their files.
|
||||
//
|
||||
// Almost every AGI game was released on Apple II. Due to the small disk size,
|
||||
// games can have many image files. KQ4 and Gold Rush each have eight physical
|
||||
// floppies, for a total of sixteen disks. Disk one contains the disk count and
|
||||
// a volume map with the location of each volume on each disk. Disks can contain
|
||||
// multiple volumes. Volumes can appear on multiple disks in any location.
|
||||
// Later games have so many volumes that Sierra had to change the DIR format.
|
||||
//
|
||||
// File detection is done a little differently. Instead of requiring hard-coded
|
||||
// names for the image files, we scan the game directory for the first usable
|
||||
// image of disk one, and then continue scanning until all disks are found.
|
||||
// The volume map from disk one is used to identify each disk by its content.
|
||||
// The only naming requirement is that the images have a known file extension.
|
||||
//
|
||||
// AgiMetaEngineDetection also scans for usable disk images. It finds the LOGDIR
|
||||
// file inside disk one, hashes LOGDIR, and matches against the detection table.
|
||||
|
||||
AgiLoader_A2::~AgiLoader_A2() {
|
||||
for (uint d = 0; d < _disks.size(); d++) {
|
||||
delete _disks[d];
|
||||
}
|
||||
}
|
||||
|
||||
void AgiLoader_A2::init() {
|
||||
// build sorted array of files with image extensions
|
||||
Common::Array<Common::Path> imageFiles;
|
||||
FileMap fileMap;
|
||||
getPotentialDiskImages(a2DiskImageExtensions, ARRAYSIZE(a2DiskImageExtensions), imageFiles, fileMap);
|
||||
|
||||
// find disk one by reading potential images until successful
|
||||
int diskCount = 0;
|
||||
Common::Array<uint32> volumeMap;
|
||||
uint diskOneIndex;
|
||||
for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
|
||||
const Common::Path &imageFile = imageFiles[diskOneIndex];
|
||||
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
warning("AgiLoader_A2: unable to open disk image: %s", imageFile.baseName().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// read image as disk one
|
||||
diskCount = readDiskOne(*stream, volumeMap);
|
||||
if (diskCount > 0) {
|
||||
debugC(3, kDebugLevelResources, "AgiLoader_A2: disk one found: %s", imageFile.baseName().c_str());
|
||||
_disks.resize(diskCount);
|
||||
_disks[0] = stream;
|
||||
break;
|
||||
} else {
|
||||
delete stream;
|
||||
}
|
||||
}
|
||||
|
||||
// if disk one wasn't found, we're done
|
||||
if (diskCount <= 0) {
|
||||
warning("AgiLoader_A2: disk one not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// find all other disks by comparing their contents to the volume map.
|
||||
// if every volume that's supposed to be on a disk has a valid header
|
||||
// at that location, then it's a match. continue until all disks are found.
|
||||
// since the potential image file list is sorted, begin with the file after
|
||||
// disk one and try until successful.
|
||||
int volumeCount = volumeMap.size() / diskCount;
|
||||
int disksFound = 1;
|
||||
for (uint i = 1; i < imageFiles.size() && disksFound < diskCount; i++) {
|
||||
uint imageFileIndex = (diskOneIndex + i) % imageFiles.size();
|
||||
Common::Path &imageFile = imageFiles[imageFileIndex];
|
||||
|
||||
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check each disk
|
||||
bool diskFound = false;
|
||||
for (int d = 1; d < diskCount; d++) {
|
||||
// has disk already been found?
|
||||
if (_disks[d] != nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool match = false;
|
||||
for (int v = 0; v < volumeCount; v++) {
|
||||
uint32 offset = volumeMap[(v * diskCount) + d];
|
||||
if (offset == _EMPTY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// test for expected resource header
|
||||
stream->seek(offset);
|
||||
uint16 magic = stream->readUint16BE();
|
||||
byte volume = stream->readByte();
|
||||
uint16 size = stream->readUint16LE();
|
||||
if (magic == 0x1234 && volume == v && stream->pos() + size <= stream->size()) {
|
||||
match = true;
|
||||
} else {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
_disks[d] = stream;
|
||||
disksFound++;
|
||||
diskFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!diskFound) {
|
||||
delete stream;
|
||||
}
|
||||
}
|
||||
|
||||
// populate _volumes with the locations of the ones we will use.
|
||||
// for each volume, select the one on the first available disk.
|
||||
_volumes.resize(volumeCount);
|
||||
for (uint32 i = 0; i < volumeMap.size(); i++) {
|
||||
int volume = i / diskCount;
|
||||
int disk = i % diskCount;
|
||||
if (volumeMap[i] != _EMPTY) {
|
||||
// use this disk's copy of the volume
|
||||
_volumes[volume].disk = disk;
|
||||
_volumes[volume].offset = volumeMap[i];
|
||||
|
||||
// skip to next volume
|
||||
i = ((volume + 1) * diskCount) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns disk count on success, 0 on failure
|
||||
int AgiLoader_A2::readDiskOne(Common::SeekableReadStream &stream, Common::Array<uint32> &volumeMap) {
|
||||
// INITDIR is located at track 1, sector 3, for games that have it.
|
||||
int diskCount;
|
||||
bool success = true;
|
||||
if (_vm->getGameID() == GID_KQ2) {
|
||||
// KQ2 doesn't have INITDIR. Use known locations.
|
||||
diskCount = A2_KQ2_DISK_COUNT;
|
||||
success &= readDir(stream, A2_KQ2_LOGDIR_POSITION, _logDir);
|
||||
success &= readDir(stream, A2_KQ2_PICDIR_POSITION, _picDir);
|
||||
success &= readDir(stream, A2_KQ2_VIEWDIR_POSITION, _viewDir);
|
||||
success &= readDir(stream, A2_KQ2_SOUNDDIR_POSITION, _soundDir);
|
||||
success &= readDir(stream, A2_KQ2_OBJECTS_POSITION, _objects);
|
||||
success &= readDir(stream, A2_KQ2_WORDS_POSITION, _words);
|
||||
// KQ2 doesn't have a volume map, probably because all the
|
||||
// volumes on the data disks start at the first sector.
|
||||
// Create one with known values so that it can also be
|
||||
// used for disk detection.
|
||||
volumeMap.clear();
|
||||
volumeMap.resize(A2_KQ2_DISK_COUNT * (A2_KQ2_DISK_COUNT + 1), _EMPTY);
|
||||
volumeMap[0 * diskCount + 0] = A2_KQ2_VOL0_POSITION;
|
||||
volumeMap[1 * diskCount + 0] = A2_KQ2_VOL1_POSITION;
|
||||
volumeMap[2 * diskCount + 1] = 0;
|
||||
volumeMap[3 * diskCount + 2] = 0;
|
||||
volumeMap[4 * diskCount + 3] = 0;
|
||||
volumeMap[5 * diskCount + 4] = 0;
|
||||
} else if (_vm->getGameID() == GID_BC) {
|
||||
// BC doesn't have INITDIR. Use known locations.
|
||||
diskCount = A2_BC_DISK_COUNT;
|
||||
success &= readDir(stream, A2_BC_LOGDIR_POSITION, _logDir);
|
||||
success &= readDir(stream, A2_BC_PICDIR_POSITION, _picDir);
|
||||
success &= readDir(stream, A2_BC_VIEWDIR_POSITION, _viewDir);
|
||||
success &= readDir(stream, A2_BC_SOUNDDIR_POSITION, _soundDir);
|
||||
success &= readDir(stream, A2_BC_OBJECTS_POSITION, _objects);
|
||||
success &= readDir(stream, A2_BC_WORDS_POSITION, _words);
|
||||
// BC has a volume map even though it doesn't have INITDIR.
|
||||
// The uint16 in front of it might be volume count.
|
||||
int volumeMapBufferSize = A2_BC_DISK_COUNT * A2_BC_VOLUME_COUNT * 2;
|
||||
success &= readVolumeMap(stream, A2_BC_VOLUME_MAP_POSITION, volumeMapBufferSize, volumeMap);
|
||||
} else {
|
||||
stream.seek(A2_INITDIR_POSITION);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
byte volume = stream.readByte();
|
||||
uint16 size = stream.readUint16LE();
|
||||
if (!(magic == 0x1234 && volume == 0)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
diskCount = stream.readByte(); // first byte of INITDIR
|
||||
success &= readInitDir(stream, A2_INITDIR_LOGDIR_INDEX, _logDir);
|
||||
success &= readInitDir(stream, A2_INITDIR_PICDIR_INDEX, _picDir);
|
||||
success &= readInitDir(stream, A2_INITDIR_VIEWDIR_INDEX, _viewDir);
|
||||
success &= readInitDir(stream, A2_INITDIR_SOUNDDIR_INDEX, _soundDir);
|
||||
success &= readInitDir(stream, A2_INITDIR_OBJECTS_INDEX, _objects);
|
||||
success &= readInitDir(stream, A2_INITDIR_WORDS_INDEX, _words);
|
||||
// volume map begins at byte 33 of INITDIR and runs until the end.
|
||||
int volumeMapBufferSize = size - 33;
|
||||
success &= readVolumeMap(stream, A2_INITDIR_VOLUME_MAP_POSITION, volumeMapBufferSize, volumeMap);
|
||||
}
|
||||
|
||||
return success ? diskCount : 0;
|
||||
}
|
||||
|
||||
bool AgiLoader_A2::readInitDir(Common::SeekableReadStream &stream, byte index, AgiDir &agid) {
|
||||
// read INITDIR entry
|
||||
stream.seek(A2_INITDIR_POSITION + 5 + 1 + (index * A2_INITDIR_ENTRY_SIZE));
|
||||
byte volume = stream.readByte();
|
||||
byte track = stream.readByte();
|
||||
byte sector = stream.readByte();
|
||||
byte offset = stream.readByte();
|
||||
if (stream.eos() || stream.err()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// resource must be on disk one
|
||||
if (!(volume == 0 || volume == 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int position = A2_DISK_POSITION(track, sector, offset);
|
||||
return readDir(stream, position, agid);
|
||||
}
|
||||
|
||||
bool AgiLoader_A2::readDir(Common::SeekableReadStream &stream, int position, AgiDir &agid) {
|
||||
// resource begins with a 5-byte header
|
||||
stream.seek(position);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
byte volume = stream.readByte();
|
||||
uint16 size = stream.readUint16LE();
|
||||
if (!(magic == 0x1234 && (volume == 0 || volume == 1))) {
|
||||
return false;
|
||||
}
|
||||
if (!(stream.pos() + size <= stream.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// resource found
|
||||
agid.volume = volume;
|
||||
agid.offset = stream.pos();
|
||||
agid.len = size;
|
||||
agid.clen = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AgiLoader_A2::readVolumeMap(
|
||||
Common::SeekableReadStream &stream,
|
||||
uint32 position,
|
||||
uint32 bufferLength,
|
||||
Common::Array<uint32> &volumeMap) {
|
||||
|
||||
// Volume map contains the location of every volume on every disk.
|
||||
// Each entry is the location in sectors. Volumes can appear on
|
||||
// multiple disks.
|
||||
// ## ## location of VOL.0 on disk 1. FF FF if empty.
|
||||
// ## ## location of VOL.0 on disk 2. FF FF if empty.
|
||||
// ...
|
||||
// ## ## location of VOL.1 on disk 1. FF FF if empty.
|
||||
stream.seek(position);
|
||||
uint32 entryCount = bufferLength / 2;
|
||||
volumeMap.clear();
|
||||
volumeMap.resize(entryCount, _EMPTY);
|
||||
for (uint32 i = 0; i < entryCount; i++) {
|
||||
uint16 sectors = stream.readUint16LE();
|
||||
if (sectors != 0xffff) {
|
||||
volumeMap[i] = A2_DISK_POSITION(0, sectors, 0);
|
||||
}
|
||||
}
|
||||
return !stream.eos() && !stream.err();
|
||||
}
|
||||
|
||||
|
||||
int AgiLoader_A2::loadDirs() {
|
||||
// if init didn't find disks then fail
|
||||
if (_disks.empty()) {
|
||||
return errFilesNotFound;
|
||||
}
|
||||
for (uint d = 0; d < _disks.size(); d++) {
|
||||
if (_disks[d] == nullptr) {
|
||||
warning("AgiLoader_A2: disk %d not found", d);
|
||||
return errFilesNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
// all dirs are on disk one
|
||||
Common::SeekableReadStream &disk = *_disks[0];
|
||||
|
||||
// detect dir format
|
||||
A2DirVersion dirVersion = detectDirVersion(disk);
|
||||
|
||||
// load each directory
|
||||
bool success = true;
|
||||
success &= loadDir(_vm->_game.dirLogic, disk, _logDir.offset, _logDir.len, dirVersion);
|
||||
success &= loadDir(_vm->_game.dirPic, disk, _picDir.offset, _picDir.len, dirVersion);
|
||||
success &= loadDir(_vm->_game.dirView, disk, _viewDir.offset, _viewDir.len, dirVersion);
|
||||
success &= loadDir(_vm->_game.dirSound, disk, _soundDir.offset, _soundDir.len, dirVersion);
|
||||
return success ? errOK : errBadResource;
|
||||
}
|
||||
|
||||
A2DirVersion AgiLoader_A2::detectDirVersion(Common::SeekableReadStream &stream) const {
|
||||
// A2 DIR format:
|
||||
// old new
|
||||
// volume 4 bits 5 bits
|
||||
// track 8 bits 7 bits
|
||||
// sector 4 bits 4 bits
|
||||
// offset 8 bits 8 bits
|
||||
//
|
||||
// This can be detected by scanning all dirs for entry 08 00 00.
|
||||
// It must exist in the new format, but can't exist in the old.
|
||||
// In the new format it's the first resource in volume 1.
|
||||
// In the old format it would be track 128, which is invalid.
|
||||
const AgiDir *dirs[4] = { &_logDir, &_picDir, &_viewDir, &_soundDir };
|
||||
for (int d = 0; d < 4; d++) {
|
||||
stream.seek(dirs[d]->offset);
|
||||
uint16 dirEntryCount = MIN<uint32>(dirs[d]->len / 3, MAX_DIRECTORY_ENTRIES);
|
||||
for (uint16 i = 0; i < dirEntryCount; i++) {
|
||||
byte b0 = stream.readByte();
|
||||
byte b1 = stream.readByte();
|
||||
byte b2 = stream.readByte();
|
||||
if (b0 == 0x08 && b1 == 0x00 && b2 == 0x00) {
|
||||
return A2DirVersionNew;
|
||||
}
|
||||
}
|
||||
}
|
||||
return A2DirVersionOld;
|
||||
}
|
||||
|
||||
bool AgiLoader_A2::loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirLength, A2DirVersion dirVersion) {
|
||||
// seek to directory on disk
|
||||
disk.seek(dirOffset);
|
||||
|
||||
// re-validate length from initdir
|
||||
if (!(disk.pos() + dirLength <= disk.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// read directory entries
|
||||
uint16 dirEntryCount = MIN<uint32>(dirLength / 3, MAX_DIRECTORY_ENTRIES);
|
||||
for (uint16 i = 0; i < dirEntryCount; i++) {
|
||||
byte b0 = disk.readByte();
|
||||
byte b1 = disk.readByte();
|
||||
byte b2 = disk.readByte();
|
||||
if (b0 == 0xff && b1 == 0xff && b2 == 0xff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A2 DIR format:
|
||||
// old new
|
||||
// volume 4 bits 5 bits
|
||||
// track 8 bits 7 bits
|
||||
// sector 4 bits 4 bits
|
||||
// offset 8 bits 8 bits
|
||||
// position is relative to the start of volume
|
||||
byte track;
|
||||
if (dirVersion == A2DirVersionOld) {
|
||||
dir[i].volume = b0 >> 4;
|
||||
track = ((b0 & 0x0f) << 4) | (b1 >> 4);
|
||||
} else {
|
||||
dir[i].volume = b0 >> 3;
|
||||
track = ((b0 & 0x07) << 4) | (b1 >> 4);
|
||||
}
|
||||
byte sector = b1 & 0x0f;
|
||||
byte offset = b2;
|
||||
dir[i].offset = A2_DISK_POSITION(track, sector, offset);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8 *AgiLoader_A2::loadVolumeResource(AgiDir *agid) {
|
||||
if (agid->volume >= _volumes.size()) {
|
||||
warning("AgiLoader_A2: invalid volume: %d", agid->volume);
|
||||
return nullptr;
|
||||
}
|
||||
if (_volumes[agid->volume].disk == _EMPTY) {
|
||||
warning("AgiLoader_A2: volume not found: %d", agid->volume);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int diskIndex = _volumes[agid->volume].disk;
|
||||
Common::SeekableReadStream &disk = *_disks[diskIndex];
|
||||
|
||||
// seek to resource and validate header
|
||||
int offset = _volumes[agid->volume].offset + agid->offset;
|
||||
disk.seek(offset);
|
||||
uint16 magic = disk.readUint16BE();
|
||||
if (magic != 0x1234) {
|
||||
warning("AgiLoader_A2: no resource at volume %d offset %d", agid->volume, agid->offset);
|
||||
return nullptr;
|
||||
}
|
||||
disk.skip(1); // volume
|
||||
agid->len = disk.readUint16LE();
|
||||
|
||||
uint8 *data = (uint8 *)calloc(1, agid->len + 32); // why the extra 32 bytes?
|
||||
if (disk.read(data, agid->len) != agid->len) {
|
||||
warning("AgiLoader_A2: error reading %d bytes at volume %d offset %d", agid->len, agid->volume, agid->offset);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
int AgiLoader_A2::loadObjects() {
|
||||
if (_disks.empty()) {
|
||||
return errFilesNotFound;
|
||||
}
|
||||
|
||||
Common::SeekableReadStream &disk = *_disks[0];
|
||||
disk.seek(_objects.offset);
|
||||
return _vm->loadObjects(disk, _objects.len);
|
||||
}
|
||||
|
||||
int AgiLoader_A2::loadWords() {
|
||||
if (_disks.empty()) {
|
||||
return errFilesNotFound;
|
||||
}
|
||||
|
||||
Common::SeekableSubReadStream words(_disks[0], _words.offset, _words.offset + _words.len);
|
||||
if (_vm->getVersion() < 0x2000) {
|
||||
return _vm->_words->loadDictionary_v1(words);
|
||||
} else {
|
||||
return _vm->_words->loadDictionary(words);
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
256
engines/agi/loader_gal.cpp
Normal file
256
engines/agi/loader_gal.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/disk_image.h"
|
||||
#include "agi/loader.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "common/fs.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// GalLoader reads KQ1 PC Booter floppy disk images.
|
||||
//
|
||||
// All disks are 360k. The only supported image format is "raw". There are no
|
||||
// headers, footers, or metadata. Each image file must be exactly 368,640 bytes.
|
||||
//
|
||||
// All KQ1 PC booter versions are only one disk.
|
||||
//
|
||||
// The disks do not use a standard file system. Instead, file locations are
|
||||
// stored in a directory structure at known locations.
|
||||
//
|
||||
// File detection is done a little differently. Instead of requiring hard-coded
|
||||
// names for the image files, we scan the game directory for the first usable
|
||||
// disk image file. The only naming requirement is that the image has a known
|
||||
// file extension.
|
||||
//
|
||||
// AgiMetaEngineDetection also scans for usable disk images. It finds and hashes
|
||||
// the logic directory inside the disk, and matches against the detection table.
|
||||
|
||||
/**
|
||||
* Locates the disk image and the disk offset of the resource directory
|
||||
*/
|
||||
void GalLoader::init() {
|
||||
// build sorted array of files with image extensions
|
||||
Common::Array<Common::Path> imageFiles;
|
||||
FileMap fileMap;
|
||||
getPotentialDiskImages(pcDiskImageExtensions, ARRAYSIZE(pcDiskImageExtensions), imageFiles, fileMap);
|
||||
|
||||
// find the disk by reading potential images until successful
|
||||
for (uint i = 0; i < imageFiles.size(); i++) {
|
||||
const Common::Path &imageFile = imageFiles[i];
|
||||
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// look for the directory in both locations
|
||||
if (isDirectory(*stream, GAL_DIR_POSITION_PCJR)) {
|
||||
_imageFile = imageFile.baseName();
|
||||
_dirOffset = GAL_DIR_POSITION_PCJR;
|
||||
} else if (isDirectory(*stream, GAL_DIR_POSITION_PC)) {
|
||||
_imageFile = imageFile.baseName();
|
||||
_dirOffset = GAL_DIR_POSITION_PC;
|
||||
}
|
||||
|
||||
delete stream;
|
||||
if (!_imageFile.empty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_imageFile.empty()) {
|
||||
warning("GalLoader: disk not found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the directory by validating the first few logic entries
|
||||
*/
|
||||
bool GalLoader::isDirectory(Common::SeekableReadStream &stream, uint32 dirOffset) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
stream.seek(dirOffset + (i * 4));
|
||||
|
||||
uint32 sectorCount;
|
||||
uint32 logicOffset = readDirectoryEntry(stream, §orCount);
|
||||
|
||||
stream.seek(logicOffset);
|
||||
uint32 logicSize = 8;
|
||||
for (int j = 0; j < 4; j++) {
|
||||
logicSize += stream.readUint16LE();
|
||||
}
|
||||
|
||||
if (stream.eos()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream.seek(logicOffset + logicSize - 1);
|
||||
byte logicTerminator = stream.readByte();
|
||||
if (stream.eos() || logicTerminator != 0xff) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a directory entry.
|
||||
*
|
||||
* Returns the disk offset and the resource size in sectors.
|
||||
*/
|
||||
uint32 GalLoader::readDirectoryEntry(Common::SeekableReadStream &stream, uint32 *sectorCount) {
|
||||
// 9 bit offset (last bit is MSB)
|
||||
// 6 bit zero
|
||||
// 5 bit sector count
|
||||
// 2 bit zero
|
||||
// 10 bit sector
|
||||
byte b0 = stream.readByte();
|
||||
byte b1 = stream.readByte();
|
||||
byte b2 = stream.readByte();
|
||||
byte b3 = stream.readByte();
|
||||
|
||||
uint16 offset = ((b1 & 0x80) << 1) | b0;
|
||||
uint16 sector = ((b2 & 0x03) << 8) | b3;
|
||||
|
||||
*sectorCount = ((b1 & 0x01) << 4) | (b2 >> 4);
|
||||
return (sector * 512) + offset;
|
||||
}
|
||||
|
||||
int GalLoader::loadDirs() {
|
||||
// if init didn't find disk then fail
|
||||
if (_imageFile.empty()) {
|
||||
return errFilesNotFound;
|
||||
}
|
||||
|
||||
// open disk
|
||||
Common::File disk;
|
||||
if (!disk.open(Common::Path(_imageFile))) {
|
||||
return errBadFileOpen;
|
||||
}
|
||||
|
||||
// load logic and picture directory entries.
|
||||
// pictures do not have directory entries. each picture immediately follows
|
||||
// its logic. if there is no real picture then it is just the FF terminator.
|
||||
uint32 sectorCount;
|
||||
for (int i = 0; i < 84; i++) {
|
||||
disk.seek(_dirOffset + (i * 4));
|
||||
uint32 logicOffset = readDirectoryEntry(disk, §orCount);
|
||||
|
||||
// seek to logic and calculate length from header
|
||||
disk.seek(logicOffset);
|
||||
uint32 logicLength = 8;
|
||||
for (int j = 0; j < 4; j++) {
|
||||
logicLength += disk.readUint16LE();
|
||||
}
|
||||
if (disk.eos()) {
|
||||
return errBadResource;
|
||||
}
|
||||
|
||||
// scan for picture terminator after logic
|
||||
uint32 pictureOffset = logicOffset + logicLength;
|
||||
disk.seek(pictureOffset);
|
||||
uint32 pictureLength = 0;
|
||||
while (true) {
|
||||
byte terminator = disk.readByte();
|
||||
if (disk.eos()) {
|
||||
return errBadResource;
|
||||
}
|
||||
if (terminator == 0xff) {
|
||||
pictureLength = disk.pos() - pictureOffset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_vm->_game.dirLogic[i].offset = logicOffset;
|
||||
_vm->_game.dirLogic[i].len = logicLength;
|
||||
_vm->_game.dirPic[i].offset = pictureOffset;
|
||||
_vm->_game.dirPic[i].len = pictureLength;
|
||||
}
|
||||
|
||||
// load sound directory entries
|
||||
for (int i = 0; i < 10; i++) {
|
||||
disk.seek(_dirOffset + ((90 + i) * 4));
|
||||
uint32 soundOffset = readDirectoryEntry(disk, §orCount);
|
||||
|
||||
// seek to sound and calculate length from header
|
||||
disk.seek(soundOffset);
|
||||
uint32 soundLength = 8;
|
||||
for (int j = 0; j < 4; j++) {
|
||||
soundLength += disk.readUint16LE();
|
||||
}
|
||||
if (disk.eos()) {
|
||||
return errBadResource;
|
||||
}
|
||||
|
||||
_vm->_game.dirSound[i].offset = soundOffset;
|
||||
_vm->_game.dirSound[i].len = soundLength;
|
||||
}
|
||||
|
||||
// load view directory entries
|
||||
for (int i = 0; i < 110; i++) {
|
||||
disk.seek(_dirOffset + ((128 + i) * 4));
|
||||
uint32 viewOffset = readDirectoryEntry(disk, §orCount);
|
||||
|
||||
// seek to view and calculate length from header
|
||||
disk.seek(viewOffset);
|
||||
uint32 viewLength = 2 + disk.readUint16LE();
|
||||
if (disk.eos()) {
|
||||
return errBadResource;
|
||||
}
|
||||
|
||||
_vm->_game.dirView[i].offset = viewOffset;
|
||||
_vm->_game.dirView[i].len = viewLength;
|
||||
}
|
||||
|
||||
return errOK;
|
||||
}
|
||||
|
||||
uint8 *GalLoader::loadVolumeResource(AgiDir *agid) {
|
||||
Common::File disk;
|
||||
if (!disk.open(Common::Path(_imageFile))) {
|
||||
warning("GalLoader: unable to open disk image: %s", _imageFile.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// read resource
|
||||
uint8 *data = (uint8 *)calloc(1, agid->len);
|
||||
disk.seek(agid->offset);
|
||||
if (disk.read(data, agid->len) != agid->len) {
|
||||
warning("GalLoader: error reading %d bytes at offset %d", agid->len, agid->offset);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO
|
||||
int GalLoader::loadObjects() {
|
||||
return errOK;
|
||||
}
|
||||
|
||||
// TODO
|
||||
int GalLoader::loadWords() {
|
||||
return errOK;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
297
engines/agi/loader_gal_a2.cpp
Normal file
297
engines/agi/loader_gal_a2.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/disk_image.h"
|
||||
#include "agi/loader.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "common/fs.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// GalLoader_A2 reads KQ1 Apple II floppy disk images.
|
||||
//
|
||||
// Floppy disks have two sides; each side is a disk with its own image file.
|
||||
// All disk sides are 140k with 35 tracks and 16 sectors per track.
|
||||
//
|
||||
// KQ1 has three disk sides (labeled A, B, C) on two physical disks.
|
||||
//
|
||||
// Multiple disk image formats are supported; see Common::DiskImage. The file
|
||||
// extension determines the format. For example: .do, .dsk, .nib, .woz.
|
||||
//
|
||||
// The disks do not use a standard file system. Instead, file locations are
|
||||
// stored in directory structures at known locations.
|
||||
//
|
||||
// File detection is done a little differently. Instead of requiring hard-coded
|
||||
// names for the image files, we scan the game directory for the first usable
|
||||
// image of disk one, and then continue scanning until all disks are found.
|
||||
// The directory from disk one is used to identify each disk by its content.
|
||||
// The only naming requirement is that the images have a known file extension.
|
||||
//
|
||||
// AgiMetaEngineDetection also scans for usable disk images. It finds and hashes
|
||||
// the logic directory inside disk one, and matches against the detection table.
|
||||
|
||||
GalLoader_A2::~GalLoader_A2() {
|
||||
for (uint d = 0; d < _disks.size(); d++) {
|
||||
delete _disks[d];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the three disk images.
|
||||
*/
|
||||
void GalLoader_A2::init() {
|
||||
// build sorted array of files with image extensions
|
||||
Common::Array<Common::Path> imageFiles;
|
||||
FileMap fileMap;
|
||||
getPotentialDiskImages(a2DiskImageExtensions, ARRAYSIZE(a2DiskImageExtensions), imageFiles, fileMap);
|
||||
|
||||
// find disk one by reading potential images until successful
|
||||
_disks.clear();
|
||||
AgiDir logicDir[GAL_A2_LOGIC_COUNT];
|
||||
uint diskOneIndex;
|
||||
for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
|
||||
const Common::Path &imageFile = imageFiles[diskOneIndex];
|
||||
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
warning("GalLoader_A2: unable to open disk image: %s", imageFile.baseName().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// read image as disk one
|
||||
if (readDiskOne(*stream, logicDir)) {
|
||||
debugC(3, kDebugLevelResources, "GalLoader_A2: disk one found: %s", imageFile.baseName().c_str());
|
||||
_disks.resize(GAL_A2_DISK_COUNT);
|
||||
_disks[0] = stream;
|
||||
break;
|
||||
} else {
|
||||
delete stream;
|
||||
}
|
||||
}
|
||||
|
||||
// if disk one wasn't found, we're done
|
||||
if (_disks.empty()) {
|
||||
warning("GalLoader_A2: disk one not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// find all other disks by comparing their contents to the logic directory.
|
||||
int disksFound = 1;
|
||||
for (uint i = 1; i < imageFiles.size() && disksFound < GAL_A2_DISK_COUNT; i++) {
|
||||
uint imageFileIndex = (diskOneIndex + i) % imageFiles.size();
|
||||
Common::Path &imageFile = imageFiles[imageFileIndex];
|
||||
|
||||
Common::SeekableReadStream *stream = openA2DiskImage(imageFile, fileMap[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check each disk
|
||||
bool diskFound = false;
|
||||
for (int d = 1; d < GAL_A2_DISK_COUNT; d++) {
|
||||
// has disk already been found?
|
||||
if (_disks[d] != nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (validateDisk(*stream, d, logicDir)) {
|
||||
_disks[d] = stream;
|
||||
disksFound++;
|
||||
diskFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!diskFound) {
|
||||
delete stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a disk image as disk one by attempting to parse the logic directory
|
||||
* and then validating that all the expected logic resources exist.
|
||||
*/
|
||||
bool GalLoader_A2::readDiskOne(Common::SeekableReadStream &disk, AgiDir *logicDir) {
|
||||
disk.seek(GAL_A2_LOGDIR_POSITION);
|
||||
|
||||
// attempt to read logic directory
|
||||
for (int i = 0; i < GAL_A2_LOGIC_COUNT; i++) {
|
||||
if (!readDirectoryEntry(disk, logicDir[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// validate that all disk one logics exist
|
||||
return validateDisk(disk, 0, logicDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a directory entry.
|
||||
*/
|
||||
bool GalLoader_A2::readDirectoryEntry(Common::SeekableReadStream &stream, AgiDir &dirEntry) {
|
||||
// GAL A2 DIR format:
|
||||
// track 8 bits
|
||||
// disk 4 bits (0 for all disks, else 1-3)
|
||||
// sector 4 bits
|
||||
// offset 8 bits
|
||||
byte b0 = stream.readByte();
|
||||
byte b1 = stream.readByte();
|
||||
byte b2 = stream.readByte();
|
||||
|
||||
byte disk = b1 >> 4;
|
||||
byte sector = b1 & 0x0f;
|
||||
uint32 position = A2_DISK_POSITION(b0, sector, b2);
|
||||
|
||||
// use the first disk for resources that are on all disks
|
||||
if (disk > 0) {
|
||||
disk--;
|
||||
}
|
||||
|
||||
// validate entry
|
||||
if (!(disk <= 2 && position < A2_DISK_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dirEntry.volume = disk;
|
||||
dirEntry.offset = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a disk contains all of the expected logic resources.
|
||||
*/
|
||||
bool GalLoader_A2::validateDisk(Common::SeekableReadStream &disk, byte diskIndex, AgiDir *logicDir) {
|
||||
for (int i = 0; i < GAL_A2_LOGIC_COUNT; i++) {
|
||||
// Only validate logics on this disk
|
||||
if (logicDir[i].volume != diskIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not use logic 64 to validate a disk. Its logic header contains
|
||||
// an incorrect length that is one byte too large. This would fail our
|
||||
// validation method below of comparing the resource length in the A2
|
||||
// header to the length in the logic header.
|
||||
if (i == 64) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A2 resources begin with a header consisting of the resource length.
|
||||
// Logic resources begin with a header consisting of four lengths; one
|
||||
// for each section of the logic. If a logic exists at this location,
|
||||
// then the A2 length will equal the calculated logic length.
|
||||
disk.seek(logicDir[i].offset);
|
||||
uint16 resourceLength = disk.readUint16LE();
|
||||
uint32 logicLength = 8;
|
||||
for (int j = 0; j < 4; j++) {
|
||||
logicLength += disk.readUint16LE();
|
||||
}
|
||||
if (disk.eos() ||
|
||||
resourceLength != logicLength ||
|
||||
!(logicDir[i].offset + 2 + resourceLength <= A2_DISK_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load logic, pic, and view directories. KQ1-A2 has no sound resources.
|
||||
*/
|
||||
int GalLoader_A2::loadDirs() {
|
||||
// if init didn't find disks then fail
|
||||
if (_disks.empty()) {
|
||||
return errFilesNotFound;
|
||||
}
|
||||
for (uint d = 0; d < _disks.size(); d++) {
|
||||
if (_disks[d] == nullptr) {
|
||||
warning("AgiLoader_A2: disk %d not found", d);
|
||||
return errFilesNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
// directories are on disk one
|
||||
Common::SeekableReadStream &disk = *_disks[0];
|
||||
|
||||
bool success = true;
|
||||
success &= loadDir(_vm->_game.dirLogic, disk, GAL_A2_LOGDIR_POSITION, GAL_A2_LOGIC_COUNT);
|
||||
success &= loadDir(_vm->_game.dirPic, disk, GAL_A2_PICDIR_POSITION, GAL_A2_PICTURE_COUNT);
|
||||
success &= loadDir(_vm->_game.dirView, disk, GAL_A2_VIEWDIR_POSITION, GAL_A2_VIEW_COUNT);
|
||||
return success ? errOK : errBadResource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a resource directory.
|
||||
*/
|
||||
bool GalLoader_A2::loadDir(AgiDir *dir, Common::SeekableReadStream &disk, uint32 dirOffset, uint32 dirCount) {
|
||||
disk.seek(dirOffset);
|
||||
for (uint32 i = 0; i < dirCount; i++) {
|
||||
// Skip pictures 0 and 81. These pictures do not exist, but the entries
|
||||
// contain junk bytes. This did not matter in the original because they
|
||||
// never loaded, but if we read them then they will fail validation.
|
||||
if ((i == 0 || i == 81) && dirOffset == GAL_A2_PICDIR_POSITION) {
|
||||
disk.skip(3);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!readDirectoryEntry(disk, dir[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8 *GalLoader_A2::loadVolumeResource(AgiDir *agid) {
|
||||
if (agid->volume >= _disks.size()) {
|
||||
warning("GalLoader_A2: invalid volume: %d", agid->volume);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Common::SeekableReadStream &disk = *_disks[agid->volume];
|
||||
|
||||
// seek to resource and read header (resource length)
|
||||
disk.seek(agid->offset);
|
||||
agid->len = disk.readUint16LE();
|
||||
|
||||
uint8 *data = (uint8 *)calloc(1, agid->len);
|
||||
if (disk.read(data, agid->len) != agid->len) {
|
||||
warning("GalLoader_A2: error reading %d bytes at volume %d offset %d", agid->len, agid->volume, agid->offset);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO
|
||||
int GalLoader_A2::loadObjects() {
|
||||
return errOK;
|
||||
}
|
||||
|
||||
// TODO
|
||||
int GalLoader_A2::loadWords() {
|
||||
// words location: GAL_A2_WORDS_POSITION
|
||||
// two byte header with resource length.
|
||||
return errOK;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
388
engines/agi/loader_v1.cpp
Normal file
388
engines/agi/loader_v1.cpp
Normal file
@@ -0,0 +1,388 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/disk_image.h"
|
||||
#include "agi/loader.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "common/fs.h"
|
||||
#include "common/substream.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// AgiLoader_v1 reads PC Booter floppy disk images.
|
||||
//
|
||||
// - King's Quest II V1 2 disks
|
||||
// - The Black Cauldron V1 2 disks
|
||||
// - Donald Duck's Playground V2.001 1 disk
|
||||
//
|
||||
// All disks are 360k. The only supported image format is "raw". There are no
|
||||
// headers, footers, or metadata. Each image file must be exactly 368,640 bytes.
|
||||
//
|
||||
// The disks do not use a standard file system. Instead, file locations are
|
||||
// stored in an INITDIR structure at a fixed location. The interpreter version
|
||||
// determines the location and format of INITDIR.
|
||||
//
|
||||
// File detection is done a little differently. Instead of requiring hard-coded
|
||||
// names for the image files, we scan the game directory for the first usable
|
||||
// image of disk one, and then scan for disk two. The only naming requirement is
|
||||
// that the images have a known file extension.
|
||||
//
|
||||
// AgiMetaEngineDetection also scans for usable disk images. It finds the LOGDIR
|
||||
// file inside disk one, hashes LOGDIR, and matches against the detection table.
|
||||
|
||||
void AgiLoader_v1::init() {
|
||||
// build sorted array of files with image extensions
|
||||
Common::Array<Common::Path> imageFiles;
|
||||
FileMap fileMap;
|
||||
getPotentialDiskImages(pcDiskImageExtensions, ARRAYSIZE(pcDiskImageExtensions), imageFiles, fileMap);
|
||||
|
||||
// find disk one by reading potential images until successful
|
||||
uint diskOneIndex;
|
||||
for (diskOneIndex = 0; diskOneIndex < imageFiles.size(); diskOneIndex++) {
|
||||
const Common::Path &imageFile = imageFiles[diskOneIndex];
|
||||
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// read image as disk one
|
||||
bool success;
|
||||
int vol0Offset = 0;
|
||||
if (_vm->getVersion() < 0x2001) {
|
||||
success = readDiskOneV1(*stream);
|
||||
} else {
|
||||
success = readDiskOneV2001(*stream, vol0Offset);
|
||||
}
|
||||
delete stream;
|
||||
|
||||
if (success) {
|
||||
debugC(3, kDebugLevelResources, "AgiLoader_v1: disk one found: %s", imageFile.baseName().c_str());
|
||||
_imageFiles.push_back(imageFile.baseName());
|
||||
if (_vm->getVersion() < 0x2001) {
|
||||
// the first disk contains volumes 0 and 1.
|
||||
// there is no volume offset, resource
|
||||
// directories use absolute disk positions.
|
||||
_volumes.push_back(AgiDiskVolume(0, 0));
|
||||
_volumes.push_back(AgiDiskVolume(0, 0));
|
||||
} else {
|
||||
// the first disk contains volume 0.
|
||||
// resource offsets are relative to its location.
|
||||
_volumes.push_back(AgiDiskVolume(0, vol0Offset));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if disk one wasn't found, we're done
|
||||
if (_imageFiles.empty()) {
|
||||
warning("AgiLoader_v1: disk one not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// two games have a second disk
|
||||
if (!(_vm->getGameID() == GID_KQ2 || _vm->getGameID() == GID_BC)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// find disk two by locating the next image file that begins with a resource
|
||||
// header with a volume number set to two. since the potential image file list
|
||||
// is sorted, begin with the file after disk one and try until successful.
|
||||
for (uint i = 1; i < imageFiles.size(); i++) {
|
||||
uint diskTwoIndex = (diskOneIndex + i) % imageFiles.size();
|
||||
Common::Path &imageFile = imageFiles[diskTwoIndex];
|
||||
|
||||
Common::SeekableReadStream *stream = openPCDiskImage(imageFile, fileMap[imageFile]);
|
||||
if (stream == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// read resource header
|
||||
uint16 magic = stream->readUint16BE();
|
||||
byte volume = stream->readByte();
|
||||
delete stream;
|
||||
|
||||
if (magic == 0x1234 && volume == 2) {
|
||||
debugC(3, kDebugLevelResources, "AgiLoader_v1: disk two found: %s", imageFile.baseName().c_str());
|
||||
_imageFiles.push_back(imageFile.baseName());
|
||||
_volumes.push_back(AgiDiskVolume(_imageFiles.size() - 1, 0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFiles.size() < 2) {
|
||||
warning("AviLoader_v1: disk two not found");
|
||||
}
|
||||
}
|
||||
|
||||
bool AgiLoader_v1::readDiskOneV1(Common::SeekableReadStream &stream) {
|
||||
// INITDIR V1 is located at the 9th sector after the 5-byte resource header.
|
||||
// Each entry is 10 bytes and there are always 8.
|
||||
stream.seek(PC_INITDIR_POSITION_V1);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
byte volume = stream.readByte();
|
||||
uint16 size = stream.readUint16LE();
|
||||
if (!(magic == 0x1234 && volume == 1 && size == PC_INITDIR_SIZE_V1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
success &= readInitDirV1(stream, PC_INITDIR_LOGDIR_INDEX_V1, _logDir);
|
||||
success &= readInitDirV1(stream, PC_INITDIR_PICDIR_INDEX_V1, _picDir);
|
||||
success &= readInitDirV1(stream, PC_INITDIR_VIEWDIR_INDEX_V1, _viewDir);
|
||||
success &= readInitDirV1(stream, PC_INITDIR_SOUNDDIR_INDEX_V1, _soundDir);
|
||||
success &= readInitDirV1(stream, PC_INITDIR_OBJECTS_INDEX_V1, _objects);
|
||||
success &= readInitDirV1(stream, PC_INITDIR_WORDS_INDEX_V1, _words);
|
||||
return success;
|
||||
}
|
||||
|
||||
bool AgiLoader_v1::readInitDirV1(Common::SeekableReadStream &stream, byte index, AgiDir &agid) {
|
||||
// read INITDIR entry
|
||||
stream.seek(PC_INITDIR_POSITION_V1 + 5 + (index * PC_INITDIR_ENTRY_SIZE_V1));
|
||||
byte volume = stream.readByte();
|
||||
byte head = stream.readByte();
|
||||
uint16 track = stream.readUint16LE();
|
||||
uint16 sector = stream.readUint16LE();
|
||||
uint16 offset = stream.readUint16LE();
|
||||
if (stream.eos() || stream.err()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// resource must be on disk one
|
||||
if (!(volume == 0 || volume == 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// resource begins with a 5-byte header
|
||||
uint32 position = PC_DISK_POSITION(head, track, sector, offset);
|
||||
stream.seek(position);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
volume = stream.readByte();
|
||||
uint16 size = stream.readUint16LE();
|
||||
if (!(magic == 0x1234 && (volume == 0 || volume == 1))) {
|
||||
return false;
|
||||
}
|
||||
if (!(stream.pos() + size <= stream.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// resource found
|
||||
agid.volume = volume;
|
||||
agid.offset = stream.pos();
|
||||
agid.len = size;
|
||||
agid.clen = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AgiLoader_v1::readDiskOneV2001(Common::SeekableReadStream &stream, int &vol0Offset) {
|
||||
// INITDIR V2001 is located at the 2nd sector with no resource header.
|
||||
// Each entry is 3 bytes. The number of entries is technically variable,
|
||||
// because the list ends in an entry for each volume followed by FF FF FF.
|
||||
// But since there was only one V2001 game (Donald Duck's Playground),
|
||||
// and it only has one disk, there is really only ever one volume.
|
||||
|
||||
bool success = true;
|
||||
success &= readInitDirV2001(stream, PC_INITDIR_LOGDIR_INDEX_V2001, _logDir);
|
||||
success &= readInitDirV2001(stream, PC_INITDIR_PICDIR_INDEX_V2001, _picDir);
|
||||
success &= readInitDirV2001(stream, PC_INITDIR_VIEWDIR_INDEX_V2001, _viewDir);
|
||||
success &= readInitDirV2001(stream, PC_INITDIR_SOUNDDIR_INDEX_V2001, _soundDir);
|
||||
success &= readInitDirV2001(stream, PC_INITDIR_OBJECTS_INDEX_V2001, _objects);
|
||||
success &= readInitDirV2001(stream, PC_INITDIR_WORDS_INDEX_V2001, _words);
|
||||
|
||||
// V2001 directories (LOGDIR, etc) contain resource offsets relative to
|
||||
// the start of their volume on disk. All volumes start at the beginning
|
||||
// of the disk, except for volume 0.
|
||||
AgiDir vol0;
|
||||
success &= readInitDirV2001(stream, PC_INITDIR_VOL0_INDEX_V2001, vol0);
|
||||
vol0Offset = vol0.offset - 5;
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool AgiLoader_v1::readInitDirV2001(Common::SeekableReadStream &stream, byte index, AgiDir &agid) {
|
||||
// read INITDIR entry
|
||||
stream.seek(PC_INITDIR_POSITION_V2001 + (index * PC_INITDIR_ENTRY_SIZE_V2001));
|
||||
byte b0 = stream.readByte();
|
||||
byte b1 = stream.readByte();
|
||||
|
||||
// volume 4 bits
|
||||
// position 12 bits (in half-sectors)
|
||||
byte volume = b0 >> 4;
|
||||
uint32 position = (((b0 & 0x0f) << 8) + b1) * 256;
|
||||
|
||||
// resource must be on disk one (because the only V2001 game is one disk)
|
||||
if (!(volume == 0 || volume == 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// resource begins with a 5-byte header
|
||||
stream.seek(position);
|
||||
uint16 magic = stream.readUint16BE();
|
||||
volume = stream.readByte();
|
||||
uint16 size = stream.readUint16LE();
|
||||
if (!(magic == 0x1234 && (volume == 0 || volume == 1))) {
|
||||
return false;
|
||||
}
|
||||
if (!(stream.pos() + size <= stream.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// resource found
|
||||
agid.volume = volume;
|
||||
agid.offset = stream.pos();
|
||||
agid.len = size;
|
||||
agid.clen = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
int AgiLoader_v1::loadDirs() {
|
||||
// if init didn't find disks then fail
|
||||
if (_imageFiles.empty()) {
|
||||
return errFilesNotFound;
|
||||
}
|
||||
|
||||
// open disk one
|
||||
Common::File disk;
|
||||
if (!disk.open(Common::Path(_imageFiles[0]))) {
|
||||
return errBadFileOpen;
|
||||
}
|
||||
|
||||
// load each directory
|
||||
bool success = true;
|
||||
success &= loadDir(_vm->_game.dirLogic, disk, _logDir.offset, _logDir.len);
|
||||
success &= loadDir(_vm->_game.dirPic, disk, _picDir.offset, _picDir.len);
|
||||
success &= loadDir(_vm->_game.dirView, disk, _viewDir.offset, _viewDir.len);
|
||||
success &= loadDir(_vm->_game.dirSound, disk, _soundDir.offset, _soundDir.len);
|
||||
return success ? errOK : errBadResource;
|
||||
}
|
||||
|
||||
bool AgiLoader_v1::loadDir(AgiDir *dir, Common::File &disk, uint32 dirOffset, uint32 dirLength) {
|
||||
// seek to directory on disk
|
||||
disk.seek(dirOffset);
|
||||
|
||||
// re-validate length from initdir
|
||||
if (!(disk.pos() + dirLength <= disk.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// read directory entries
|
||||
uint16 dirEntryCount = MIN<uint32>(dirLength / 3, MAX_DIRECTORY_ENTRIES);
|
||||
for (uint16 i = 0; i < dirEntryCount; i++) {
|
||||
byte b0 = disk.readByte();
|
||||
byte b1 = disk.readByte();
|
||||
byte b2 = disk.readByte();
|
||||
if (b0 == 0xff && b1 == 0xff && b2 == 0xff) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_vm->getVersion() < 0x2001) {
|
||||
// volume 2 bits
|
||||
// track 6 bits
|
||||
// sector 6 bits (one based)
|
||||
// head 1 bit
|
||||
// offset 9 bits
|
||||
dir[i].volume = b0 >> 6;
|
||||
byte track = b0 & 0x3f;
|
||||
byte sector = b1 >> 2;
|
||||
byte head = (b1 >> 1) & 1;
|
||||
uint16 offset = ((b1 & 1) << 8) | b2;
|
||||
dir[i].offset = PC_DISK_POSITION(head, track, sector, offset);
|
||||
} else {
|
||||
// volume 4 bits
|
||||
// sector 11 bits (zero based)
|
||||
// offset 9 bits
|
||||
// position is relative to the start of volume
|
||||
dir[i].volume = b0 >> 4;
|
||||
uint16 sector = ((b0 & 0x0f) << 7) | (b1 >> 1);
|
||||
uint16 offset = ((b1 & 0x01) << 8) | b2;
|
||||
dir[i].offset = PC_DISK_POSITION(0, 0, sector + 1, offset);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8 *AgiLoader_v1::loadVolumeResource(AgiDir *agid) {
|
||||
if (agid->volume >= _volumes.size()) {
|
||||
warning("AgiLoader_v1: invalid volume: %d", agid->volume);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Common::File disk;
|
||||
int diskIndex = _volumes[agid->volume].disk;
|
||||
if (!disk.open(Common::Path(_imageFiles[diskIndex]))) {
|
||||
warning("AgiLoader_v1: unable to open disk image: %s", _imageFiles[diskIndex].c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// seek to resource and validate header
|
||||
int offset = _volumes[agid->volume].offset + agid->offset;
|
||||
disk.seek(offset);
|
||||
uint16 magic = disk.readUint16BE();
|
||||
if (magic != 0x1234) {
|
||||
warning("AgiLoader_v1: no resource at volume %d offset %d", agid->volume, agid->offset);
|
||||
return nullptr;
|
||||
}
|
||||
disk.skip(1); // volume
|
||||
agid->len = disk.readUint16LE();
|
||||
|
||||
uint8 *data = (uint8 *)calloc(1, agid->len + 32); // why the extra 32 bytes?
|
||||
if (disk.read(data, agid->len) != agid->len) {
|
||||
warning("AgiLoader_v1: error reading %d bytes at volume %d offset %d", agid->len, agid->volume, agid->offset);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
int AgiLoader_v1::loadObjects() {
|
||||
// DDP has an empty-ish objects resource but doesn't use it
|
||||
if (_vm->getGameID() == GID_DDP) {
|
||||
return errOK;
|
||||
}
|
||||
|
||||
Common::File disk;
|
||||
if (!disk.open(Common::Path(_imageFiles[0]))) {
|
||||
return errBadFileOpen;
|
||||
}
|
||||
|
||||
disk.seek(_objects.offset);
|
||||
return _vm->loadObjects(disk, _objects.len);
|
||||
}
|
||||
|
||||
int AgiLoader_v1::loadWords() {
|
||||
// DDP has an empty-ish words resource but doesn't use it
|
||||
if (_vm->getGameID() == GID_DDP) {
|
||||
return errOK;
|
||||
}
|
||||
|
||||
Common::File disk;
|
||||
if (!disk.open(Common::Path(_imageFiles[0]))) {
|
||||
return errBadFileOpen;
|
||||
}
|
||||
|
||||
Common::SeekableSubReadStream words(&disk, _words.offset, _words.offset + _words.len);
|
||||
return _vm->_words->loadDictionary_v1(words);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
198
engines/agi/loader_v2.cpp
Normal file
198
engines/agi/loader_v2.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/textconsole.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/loader.h"
|
||||
#include "agi/lzw.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
int AgiLoader_v2::loadDir(AgiDir *agid, const char *fname) {
|
||||
debug(0, "Loading directory: %s", fname);
|
||||
|
||||
Common::File fp;
|
||||
if (!fp.open(fname)) {
|
||||
return errBadFileOpen;
|
||||
}
|
||||
|
||||
fp.seek(0, SEEK_END);
|
||||
uint32 flen = fp.pos();
|
||||
fp.seek(0, SEEK_SET);
|
||||
|
||||
uint8 *mem = (uint8 *)malloc(flen);
|
||||
if (mem == nullptr) {
|
||||
return errNotEnoughMemory;
|
||||
}
|
||||
|
||||
fp.read(mem, flen);
|
||||
|
||||
// read directory entries
|
||||
for (uint32 i = 0; i + 2 < flen; i += 3) {
|
||||
agid[i / 3].volume = *(mem + i) >> 4;
|
||||
agid[i / 3].offset = READ_BE_UINT24(mem + i) & (uint32) _EMPTY;
|
||||
debugC(3, kDebugLevelResources, "%d: volume %d, offset 0x%05x", i / 3, agid[i / 3].volume, agid[i / 3].offset);
|
||||
}
|
||||
|
||||
free(mem);
|
||||
return errOK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the volume format is really V3.
|
||||
*
|
||||
* The CoCo3 version of Leisure Suit Larry uses a V3 volume, even
|
||||
* though it is a V2 game with V2 directory files. Sierra's other
|
||||
* CoCo3 release, King's Quest III, uses regular V2 volumes.
|
||||
* Fan ports of DOS games to CoCo3 use V3 volumes; presumably they
|
||||
* used the Leisure Suit Larry interpreter.
|
||||
*
|
||||
* Returns true if Logic 0's volume matches the V3 format.
|
||||
*/
|
||||
bool AgiLoader_v2::detectV3VolumeFormat() {
|
||||
uint8 volume = _vm->_game.dirLogic[0].volume;
|
||||
Common::Path path(Common::String::format("vol.%i", volume));
|
||||
Common::File volumeFile;
|
||||
if (!volumeFile.open(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// read the first few entries and see if they match the 7 byte header
|
||||
uint8 volumeHeader[7];
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (volumeFile.read(&volumeHeader, 7) != 7) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// signature
|
||||
if (READ_BE_UINT16(volumeHeader) != 0x1234) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// volume number (high bit == pic compression)
|
||||
if ((volumeHeader[2] & 0x7f) != volume) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// uncompressed and compressed resource length.
|
||||
// we can't validate these values against each other.
|
||||
// uncompressed should always be greater than or equal
|
||||
// to compressed, but fan tools compressed small resources
|
||||
// even when the result was larger than uncompressed.
|
||||
// (Coco3 fan ports of KQ4, MH1, MH2)
|
||||
uint16 compressedResourceLength = READ_LE_UINT16(volumeHeader + 5);
|
||||
|
||||
if (!volumeFile.seek(compressedResourceLength, SEEK_CUR)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int AgiLoader_v2::loadDirs() {
|
||||
int ec = errOK;
|
||||
|
||||
// load directory files
|
||||
ec = loadDir(_vm->_game.dirLogic, LOGDIR);
|
||||
if (ec == errOK)
|
||||
ec = loadDir(_vm->_game.dirPic, PICDIR);
|
||||
if (ec == errOK)
|
||||
ec = loadDir(_vm->_game.dirView, VIEWDIR);
|
||||
if (ec == errOK)
|
||||
ec = loadDir(_vm->_game.dirSound, SNDDIR);
|
||||
if (ec == errOK)
|
||||
_hasV3VolumeFormat = detectV3VolumeFormat();
|
||||
|
||||
return ec;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loads a raw resource into memory,
|
||||
* if further decoding is required, it must be done by another
|
||||
* routine. NULL is returned if unsuccessful.
|
||||
*/
|
||||
uint8 *AgiLoader_v2::loadVolumeResource(AgiDir *agid) {
|
||||
uint8 *data = nullptr;
|
||||
uint8 volumeHeader[7];
|
||||
Common::File fp;
|
||||
Common::Path path(Common::String::format("vol.%i", agid->volume));
|
||||
|
||||
debugC(3, kDebugLevelResources, "Vol res: path = %s", path.toString().c_str());
|
||||
|
||||
if (agid->offset != _EMPTY && fp.open(path)) {
|
||||
debugC(3, kDebugLevelResources, "loading resource at offset %d", agid->offset);
|
||||
fp.seek(agid->offset, SEEK_SET);
|
||||
fp.read(&volumeHeader, _hasV3VolumeFormat ? 7 : 5);
|
||||
uint16 signature = READ_BE_UINT16(volumeHeader);
|
||||
if (signature != 0x1234) {
|
||||
warning("AgiLoader_v2::loadVolRes: bad signature %04x", signature);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
agid->len = READ_LE_UINT16(volumeHeader + 3);
|
||||
if (_hasV3VolumeFormat) {
|
||||
agid->clen = READ_LE_UINT16(volumeHeader + 5);
|
||||
} else {
|
||||
agid->clen = agid->len;
|
||||
}
|
||||
|
||||
uint8 *compBuffer = (uint8 *)calloc(1, agid->clen + 32); // why the extra 32 bytes?
|
||||
fp.read(compBuffer, agid->clen);
|
||||
|
||||
if ((volumeHeader[2] & 0x80) && _hasV3VolumeFormat) { // compressed pic
|
||||
// effectively uncompressed, but having only 4-bit parameters for F0 / F2 commands
|
||||
data = compBuffer;
|
||||
agid->flags |= RES_PICTURE_V3_NIBBLE_PARM;
|
||||
} else if (agid->len == agid->clen) {
|
||||
// do not decompress
|
||||
data = compBuffer;
|
||||
} else {
|
||||
// it is compressed
|
||||
data = (uint8 *)calloc(1, agid->len + 32); // why the extra 32 bytes?
|
||||
lzwExpand(compBuffer, data, agid->len);
|
||||
free(compBuffer);
|
||||
agid->flags |= RES_COMPRESSED;
|
||||
}
|
||||
} else {
|
||||
// we have a bad volume resource
|
||||
// set that resource to NA
|
||||
agid->offset = _EMPTY;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
int AgiLoader_v2::loadObjects() {
|
||||
return _vm->loadObjects(OBJECTS);
|
||||
}
|
||||
|
||||
int AgiLoader_v2::loadWords() {
|
||||
// Use a fan-made extended dictionary file for translations if present.
|
||||
if (Common::File::exists(EXTENDED_DICTIONARY_FILENAME)) {
|
||||
return _vm->_words->loadExtendedDictionary(EXTENDED_DICTIONARY_FILENAME);
|
||||
} else {
|
||||
return _vm->_words->loadDictionary(WORDS);
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
196
engines/agi/loader_v3.cpp
Normal file
196
engines/agi/loader_v3.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/loader.h"
|
||||
#include "agi/lzw.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "common/config-manager.h"
|
||||
#include "common/fs.h"
|
||||
#include "common/textconsole.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
void AgiLoader_v3::init() {
|
||||
// Find the game's name by locating a file that ends in "dir".
|
||||
// Amiga games don't use the game's name as a prefix.
|
||||
_name.clear();
|
||||
Common::FSList fslist;
|
||||
Common::FSNode dir(ConfMan.getPath("path"));
|
||||
|
||||
if (!dir.getChildren(fslist, Common::FSNode::kListFilesOnly)) {
|
||||
warning("AgiLoader_v3: invalid game path '%s'", dir.getPath().toString(Common::Path::kNativeSeparator).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &file : fslist) {
|
||||
Common::String fileName = file.getName();
|
||||
if (fileName.size() > 3 && fileName.hasSuffixIgnoreCase("dir")) {
|
||||
_name = fileName.substr(0, fileName.size() - 3);
|
||||
_name.toLowercase();
|
||||
debugC(3, kDebugLevelResources, "game name: %s", _name.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int AgiLoader_v3::loadDir(AgiDir *agid, Common::File *fp, uint32 offs, uint32 len) {
|
||||
fp->seek(offs, SEEK_SET);
|
||||
uint8 *mem = (uint8 *)malloc(len);
|
||||
if (mem == nullptr) {
|
||||
return errNotEnoughMemory;
|
||||
}
|
||||
|
||||
fp->read(mem, len);
|
||||
|
||||
// read directory entries
|
||||
for (uint32 i = 0; i + 2 < len; i += 3) {
|
||||
agid[i / 3].volume = *(mem + i) >> 4;
|
||||
agid[i / 3].offset = READ_BE_UINT24(mem + i) & (uint32) _EMPTY;
|
||||
debugC(3, kDebugLevelResources, "%d: volume %d, offset 0x%05x", i / 3, agid[i / 3].volume, agid[i / 3].offset);
|
||||
}
|
||||
|
||||
free(mem);
|
||||
return errOK;
|
||||
}
|
||||
|
||||
int AgiLoader_v3::loadDirs() {
|
||||
int ec = errOK;
|
||||
uint8 fileHeader[8];
|
||||
Common::File fp;
|
||||
Common::Path path;
|
||||
|
||||
if (_vm->getPlatform() == Common::kPlatformAmiga) {
|
||||
// Amiga directory file is always named "dirs"
|
||||
path = Common::Path("dirs");
|
||||
} else {
|
||||
if (_name.empty()) {
|
||||
warning("AgiLoader_v3: directory file not found");
|
||||
return errBadResource;
|
||||
}
|
||||
path = Common::Path(_name + DIR_);
|
||||
}
|
||||
|
||||
if (!fp.open(path)) {
|
||||
warning("Failed to open '%s'", path.toString().c_str());
|
||||
return errBadFileOpen;
|
||||
}
|
||||
// build offset table for v3 directory format
|
||||
fp.read(&fileHeader, 8);
|
||||
fp.seek(0, SEEK_END);
|
||||
|
||||
uint16 dirOffsets[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
dirOffsets[i] = READ_LE_UINT16(&fileHeader[i * 2]);
|
||||
|
||||
uint32 dirLengths[4];
|
||||
dirLengths[0] = dirOffsets[1] - dirOffsets[0];
|
||||
dirLengths[1] = dirOffsets[2] - dirOffsets[1];
|
||||
dirLengths[2] = dirOffsets[3] - dirOffsets[2];
|
||||
dirLengths[3] = fp.pos() - dirOffsets[3];
|
||||
|
||||
if (dirLengths[3] > 256 * 3)
|
||||
dirLengths[3] = 256 * 3;
|
||||
|
||||
fp.seek(0, SEEK_SET);
|
||||
|
||||
// read in directory files
|
||||
ec = loadDir(_vm->_game.dirLogic, &fp, dirOffsets[0], dirLengths[0]);
|
||||
if (ec == errOK)
|
||||
ec = loadDir(_vm->_game.dirPic, &fp, dirOffsets[1], dirLengths[1]);
|
||||
if (ec == errOK)
|
||||
ec = loadDir(_vm->_game.dirView, &fp, dirOffsets[2], dirLengths[2]);
|
||||
if (ec == errOK)
|
||||
ec = loadDir(_vm->_game.dirSound, &fp, dirOffsets[3], dirLengths[3]);
|
||||
|
||||
return ec;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loads a raw resource into memory.
|
||||
* If further decoding is required, it must be done by another
|
||||
* routine.
|
||||
*
|
||||
* NULL is returned if unsuccessful.
|
||||
*/
|
||||
uint8 *AgiLoader_v3::loadVolumeResource(AgiDir *agid) {
|
||||
uint8 volumeHeader[7];
|
||||
uint8 *data = nullptr;
|
||||
Common::File fp;
|
||||
Common::Path path;
|
||||
|
||||
debugC(3, kDebugLevelResources, "(%p)", (void *)agid);
|
||||
if (_vm->getPlatform() == Common::kPlatformMacintosh) {
|
||||
path = Common::String::format("vol.%i", agid->volume);
|
||||
} else {
|
||||
path = Common::String::format("%svol.%i", _name.c_str(), agid->volume);
|
||||
}
|
||||
|
||||
if (agid->offset != _EMPTY && fp.open(path)) {
|
||||
fp.seek(agid->offset, SEEK_SET);
|
||||
fp.read(&volumeHeader, 7);
|
||||
|
||||
uint16 signature = READ_BE_UINT16(volumeHeader);
|
||||
if (signature != 0x1234) {
|
||||
warning("AgiLoader_v3::loadVolRes: bad signature %04x", signature);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
agid->len = READ_LE_UINT16(volumeHeader + 3); // uncompressed size
|
||||
agid->clen = READ_LE_UINT16(volumeHeader + 5); // compressed len
|
||||
|
||||
uint8 *compBuffer = (uint8 *)calloc(1, agid->clen + 32); // why the extra 32 bytes?
|
||||
fp.read(compBuffer, agid->clen);
|
||||
|
||||
if (volumeHeader[2] & 0x80) { // compressed pic
|
||||
// effectively uncompressed, but having only 4-bit parameters for F0 / F2 commands
|
||||
// Manhunter 2 uses such pictures
|
||||
data = compBuffer;
|
||||
agid->flags |= RES_PICTURE_V3_NIBBLE_PARM;
|
||||
} else if (agid->len == agid->clen) {
|
||||
// do not decompress
|
||||
data = compBuffer;
|
||||
} else {
|
||||
// it is compressed
|
||||
data = (uint8 *)calloc(1, agid->len + 32); // why the extra 32 bytes?
|
||||
lzwExpand(compBuffer, data, agid->len);
|
||||
free(compBuffer);
|
||||
agid->flags |= RES_COMPRESSED;
|
||||
}
|
||||
} else {
|
||||
// we have a bad volume resource
|
||||
// set that resource to NA
|
||||
agid->offset = _EMPTY;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
int AgiLoader_v3::loadObjects() {
|
||||
return _vm->loadObjects(OBJECTS);
|
||||
}
|
||||
|
||||
int AgiLoader_v3::loadWords() {
|
||||
return _vm->_words->loadDictionary(WORDS);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
115
engines/agi/logic.cpp
Normal file
115
engines/agi/logic.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* Decode logic resource
|
||||
* This function decodes messages from the specified raw logic resource
|
||||
* into a message list.
|
||||
* @param logicNr The number of the logic resource to decode.
|
||||
*/
|
||||
int AgiEngine::decodeLogic(int16 logicNr) {
|
||||
AgiLogic &logic = _game.logics[logicNr];
|
||||
AgiDir &dirLogic = _game.dirLogic[logicNr];
|
||||
|
||||
// bytecode section:
|
||||
// u16 bytecode size
|
||||
// u8[] bytecode
|
||||
uint16 bytecodeSize = READ_LE_UINT16(logic.data);
|
||||
|
||||
// message section:
|
||||
// u8 message count
|
||||
// u16 messages size (2 + offsets + strings)
|
||||
// u16[] string offsets (relative to message section + 1)
|
||||
// string[] strings (null terminated, possibly encrypted)
|
||||
int messageSectionPos = 2 + bytecodeSize;
|
||||
uint8 messageCount = logic.data[messageSectionPos];
|
||||
uint16 messagesSize = READ_LE_UINT16(logic.data + messageSectionPos + 1);
|
||||
int stringOffsetsPos = messageSectionPos + 3;
|
||||
int stringsPos = stringOffsetsPos + (2 * messageCount);
|
||||
int stringsSize = messagesSize - 2 - (2 * messageCount);
|
||||
|
||||
// decrypt the message strings if the logic was not compressed
|
||||
// and the logic has messages.
|
||||
if ((~dirLogic.flags & RES_COMPRESSED) && messageCount > 0) {
|
||||
decrypt(logic.data + stringsPos, stringsSize);
|
||||
}
|
||||
|
||||
// reset logic pointers
|
||||
logic.sIP = 2;
|
||||
logic.cIP = 2;
|
||||
logic.size = messageSectionPos; // exclude messages from logic size
|
||||
|
||||
// allocate list of pointers to message texts. last entry is null.
|
||||
logic.numTexts = messageCount;
|
||||
logic.texts = (const char **)calloc(1 + logic.numTexts, sizeof(char *));
|
||||
if (logic.texts == nullptr) {
|
||||
free(logic.data);
|
||||
logic.data = nullptr;
|
||||
logic.numTexts = 0;
|
||||
return errNotEnoughMemory;
|
||||
}
|
||||
|
||||
// populate list of pointers to message texts
|
||||
for (int i = 0; i < messageCount; i++) {
|
||||
int stringOffset = READ_LE_UINT16(logic.data + stringOffsetsPos + (i * 2));
|
||||
if (stringOffset != 0) {
|
||||
// offset is relative to the message section + 1
|
||||
stringOffset += messageSectionPos + 1;
|
||||
logic.texts[i] = (const char *)(logic.data + stringOffset);
|
||||
} else {
|
||||
// TODO: does this happen? when is a string offset zero?
|
||||
logic.texts[i] = "";
|
||||
}
|
||||
}
|
||||
|
||||
// set loaded flag
|
||||
dirLogic.flags |= RES_LOADED;
|
||||
return errOK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload logic resource
|
||||
* This function unloads the specified logic resource, freeing any
|
||||
* memory chunks allocated for this resource.
|
||||
* @param logicNr The number of the logic resource to unload
|
||||
*/
|
||||
void AgiEngine::unloadLogic(int16 logicNr) {
|
||||
AgiLogic &logic = _game.logics[logicNr];
|
||||
AgiDir &dirLogic = _game.dirLogic[logicNr];
|
||||
|
||||
if (dirLogic.flags & RES_LOADED) {
|
||||
free(logic.data);
|
||||
logic.data = nullptr;
|
||||
free(logic.texts);
|
||||
logic.texts = nullptr;
|
||||
logic.numTexts = 0;
|
||||
dirLogic.flags &= ~RES_LOADED;
|
||||
}
|
||||
|
||||
logic.sIP = 2;
|
||||
logic.cIP = 2;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
52
engines/agi/logic.h
Normal file
52
engines/agi/logic.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_LOGIC_H
|
||||
#define AGI_LOGIC_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* AGI logic resource structure.
|
||||
*/
|
||||
struct AgiLogic {
|
||||
uint8 *data; /**< raw resource data */
|
||||
int size; /**< size of data (excluding message section) */
|
||||
int sIP; /**< saved IP */
|
||||
int cIP; /**< current IP */
|
||||
int numTexts; /**< number of messages */
|
||||
const char **texts; /**< message list */
|
||||
|
||||
void reset() {
|
||||
data = nullptr;
|
||||
size = 0;
|
||||
sIP = 0;
|
||||
cIP = 0;
|
||||
numTexts = 0;
|
||||
texts = nullptr;
|
||||
}
|
||||
|
||||
AgiLogic() { reset(); }
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_LOGIC_H */
|
||||
200
engines/agi/lzw.cpp
Normal file
200
engines/agi/lzw.cpp
Normal file
@@ -0,0 +1,200 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/***************************************************************************
|
||||
** decomp.c
|
||||
**
|
||||
** Routines that deal with AGI version 3 specific features.
|
||||
** The original LZW code is from DJJ, October 1989, p.86.
|
||||
** It has been modified to handle AGI compression.
|
||||
**
|
||||
** (c) 1997 Lance Ewing
|
||||
***************************************************************************/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/lzw.h"
|
||||
|
||||
#include "common/textconsole.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
|
||||
class LZWDecoder {
|
||||
private:
|
||||
|
||||
enum {
|
||||
MAXBITS = 12,
|
||||
TABLE_SIZE = 18041, // strange number
|
||||
START_BITS = 9
|
||||
};
|
||||
|
||||
int32 BITS, MAX_VALUE, MAX_CODE;
|
||||
uint32 *prefixCode;
|
||||
uint8 *appendCharacter;
|
||||
uint8 *decodeStack;
|
||||
int32 inputBitCount; // Number of bits in input bit buffer
|
||||
uint32 inputBitBuffer;
|
||||
|
||||
public:
|
||||
LZWDecoder();
|
||||
~LZWDecoder();
|
||||
|
||||
void lzwExpand(uint8 *in, uint8 *out, int32 len);
|
||||
|
||||
private:
|
||||
int setBits(int32 value);
|
||||
uint8 *decodeString(uint8 *buffer, uint32 code);
|
||||
uint32 inputCode(uint8 **input);
|
||||
};
|
||||
|
||||
LZWDecoder::LZWDecoder() {
|
||||
decodeStack = (uint8 *)calloc(1, 8192);
|
||||
prefixCode = (uint32 *)malloc(TABLE_SIZE * sizeof(uint32));
|
||||
appendCharacter = (uint8 *)malloc(TABLE_SIZE * sizeof(uint8));
|
||||
inputBitCount = 0; // Number of bits in input bit buffer
|
||||
inputBitBuffer = 0L;
|
||||
BITS = MAX_VALUE = MAX_CODE = 0;
|
||||
}
|
||||
|
||||
LZWDecoder::~LZWDecoder() {
|
||||
free(decodeStack);
|
||||
free(prefixCode);
|
||||
free(appendCharacter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the number of bits used to store codes to the value passed in.
|
||||
*/
|
||||
int LZWDecoder::setBits(int32 value) {
|
||||
if (value == MAXBITS)
|
||||
return true;
|
||||
|
||||
BITS = value;
|
||||
MAX_VALUE = (1 << BITS) - 1;
|
||||
MAX_CODE = MAX_VALUE - 1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the string that the code taken from the input buffer
|
||||
* represents. The string is returned as a stack, i.e. the characters are
|
||||
* in reverse order.
|
||||
*/
|
||||
uint8 *LZWDecoder::decodeString(uint8 *buffer, uint32 code) {
|
||||
uint32 i;
|
||||
|
||||
for (i = 0; code > 255;) {
|
||||
*buffer++ = appendCharacter[code];
|
||||
code = prefixCode[code];
|
||||
if (i++ >= 4000) {
|
||||
error("lzw: error in code expansion");
|
||||
}
|
||||
}
|
||||
*buffer = code;
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next code from the input buffer.
|
||||
*/
|
||||
uint32 LZWDecoder::inputCode(uint8 **input) {
|
||||
uint32 r;
|
||||
|
||||
while (inputBitCount <= 24) {
|
||||
inputBitBuffer |= (uint32) * (*input)++ << inputBitCount;
|
||||
inputBitCount += 8;
|
||||
}
|
||||
r = (inputBitBuffer & 0x7FFF) % (1 << BITS);
|
||||
inputBitBuffer >>= BITS;
|
||||
inputBitCount -= BITS;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uncompress the data contained in the input buffer and store
|
||||
* the result in the output buffer. The fileLength parameter says how
|
||||
* many bytes to uncompress. The compression itself is a form of LZW that
|
||||
* adjusts the number of bits that it represents its codes in as it fills
|
||||
* up the available codes. Two codes have special meaning:
|
||||
*
|
||||
* code 256 = start over
|
||||
* code 257 = end of data
|
||||
*/
|
||||
void LZWDecoder::lzwExpand(uint8 *in, uint8 *out, int32 len) {
|
||||
int32 c, lzwnext, lzwnew, lzwold;
|
||||
uint8 *s, *end;
|
||||
|
||||
LZWDecoder d;
|
||||
|
||||
setBits(START_BITS); // Starts at 9-bits
|
||||
lzwnext = 257; // Next available code to define
|
||||
|
||||
end = (uint8 *)(out + (uint32)len);
|
||||
|
||||
lzwold = inputCode(&in); // Read in the first code
|
||||
c = lzwold;
|
||||
lzwnew = inputCode(&in);
|
||||
|
||||
while ((out < end) && (lzwnew != 0x101)) {
|
||||
if (lzwnew == 0x100) {
|
||||
// Code to "start over"
|
||||
lzwnext = 258;
|
||||
setBits(START_BITS);
|
||||
lzwold = inputCode(&in);
|
||||
c = lzwold;
|
||||
*out++ = (char)c;
|
||||
lzwnew = inputCode(&in);
|
||||
} else {
|
||||
if (lzwnew >= lzwnext) {
|
||||
// Handles special LZW scenario
|
||||
*decodeStack = c;
|
||||
s = decodeString(decodeStack + 1, lzwold);
|
||||
} else
|
||||
s = decodeString(decodeStack, lzwnew);
|
||||
|
||||
// Reverse order of decoded string and
|
||||
// store in out buffer
|
||||
c = *s;
|
||||
while (s >= decodeStack)
|
||||
*out++ = *s--;
|
||||
|
||||
if (lzwnext > MAX_CODE)
|
||||
setBits(BITS + 1);
|
||||
|
||||
prefixCode[lzwnext] = lzwold;
|
||||
appendCharacter[lzwnext] = c;
|
||||
lzwnext++;
|
||||
lzwold = lzwnew;
|
||||
|
||||
lzwnew = inputCode(&in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void lzwExpand(uint8 *in, uint8 *out, int32 len) {
|
||||
LZWDecoder d;
|
||||
d.lzwExpand(in, out, len);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
31
engines/agi/lzw.h
Normal file
31
engines/agi/lzw.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_LZW_H
|
||||
#define AGI_LZW_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
void lzwExpand(uint8 *in, uint8 *out, int32 len);
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_LZW_H */
|
||||
733
engines/agi/menu.cpp
Normal file
733
engines/agi/menu.cpp
Normal file
@@ -0,0 +1,733 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/config-manager.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/text.h"
|
||||
#include "agi/keyboard.h"
|
||||
#include "agi/menu.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
GfxMenu::GfxMenu(AgiEngine *vm, GfxMgr *gfx, PictureMgr *picture, TextMgr *text) {
|
||||
_vm = vm;
|
||||
_gfx = gfx;
|
||||
_picture = picture;
|
||||
_text = text;
|
||||
|
||||
_allowed = true;
|
||||
_submitted = false;
|
||||
_delayedExecuteViaKeyboard = false;
|
||||
_delayedExecuteViaMouse = false;
|
||||
|
||||
if (!_vm->isLanguageRTL())
|
||||
_setupMenuColumn = 1;
|
||||
else
|
||||
_setupMenuColumn = FONT_COLUMN_CHARACTERS - 2;
|
||||
_setupMenuItemColumn = 1;
|
||||
|
||||
_lastSelectedMenuNr = 0;
|
||||
|
||||
_mouseModeItemNr = -1;
|
||||
|
||||
_drawnMenuNr = -1;
|
||||
_drawnMenuHeight = 0;
|
||||
_drawnMenuWidth = 0;
|
||||
_drawnMenuY = 0;
|
||||
_drawnMenuX = 0;
|
||||
}
|
||||
|
||||
GfxMenu::~GfxMenu() {
|
||||
for (auto *menu : _array)
|
||||
delete menu;
|
||||
_array.clear();
|
||||
|
||||
for (auto *menuItem : _itemArray)
|
||||
delete menuItem;
|
||||
_itemArray.clear();
|
||||
}
|
||||
|
||||
void GfxMenu::addMenu(const char *menuText) {
|
||||
int16 curColumnEnd = _setupMenuColumn;
|
||||
|
||||
// already submitted? in that case no further changes possible
|
||||
if (_submitted)
|
||||
return;
|
||||
|
||||
GuiMenuEntry *menuEntry = new GuiMenuEntry();
|
||||
|
||||
menuEntry->text = menuText;
|
||||
// WORKAROUND: Apple II gs Goldrush! Speed menu exceeds screen width, because of a redundant space at 'Special' menu, remove it
|
||||
if (_vm->getPlatform() == Common::kPlatformApple2GS && ConfMan.getBool("apple2gs_speedmenu") && _vm->getGameID() == GID_GOLDRUSH)
|
||||
if (menuEntry->text == " Special ")
|
||||
menuEntry->text = "Special ";
|
||||
|
||||
menuEntry->textLen = menuEntry->text.size();
|
||||
|
||||
if (!_vm->isLanguageRTL()) {
|
||||
// Cut menu name in case menu bar is full
|
||||
// Happens in at least the fan game Get Outta Space Quest
|
||||
// Original interpreter had graphical issues in this case
|
||||
// TODO: this whole code needs to get reworked anyway to support different types of menu bars depending on platform
|
||||
curColumnEnd += menuEntry->textLen;
|
||||
while ((menuEntry->textLen) && (curColumnEnd > 40)) {
|
||||
menuEntry->text.deleteLastChar();
|
||||
menuEntry->textLen--;
|
||||
curColumnEnd--;
|
||||
}
|
||||
}
|
||||
|
||||
menuEntry->row = 0;
|
||||
menuEntry->column = _setupMenuColumn;
|
||||
if (_vm->isLanguageRTL())
|
||||
menuEntry->column -= menuEntry->textLen;
|
||||
menuEntry->itemCount = 0;
|
||||
menuEntry->firstItemNr = _itemArray.size();
|
||||
menuEntry->selectedItemNr = menuEntry->firstItemNr;
|
||||
menuEntry->maxItemTextLen = 0;
|
||||
_array.push_back(menuEntry);
|
||||
|
||||
if (!_vm->isLanguageRTL())
|
||||
_setupMenuColumn += menuEntry->textLen + 1;
|
||||
else
|
||||
_setupMenuColumn -= menuEntry->textLen + 1;
|
||||
}
|
||||
|
||||
void GfxMenu::addMenuItem(const char *menuItemText, uint16 controllerSlot) {
|
||||
int16 arrayCount = _array.size();
|
||||
|
||||
// already submitted? in that case no further changes possible
|
||||
if (_submitted)
|
||||
return;
|
||||
|
||||
if (arrayCount == 0)
|
||||
error("tried to add a menu item before adding an actual menu");
|
||||
|
||||
// go to latest menu entry
|
||||
GuiMenuEntry *curMenuEntry = _array.back();
|
||||
|
||||
GuiMenuItemEntry *menuItemEntry = new GuiMenuItemEntry();
|
||||
|
||||
menuItemEntry->enabled = true;
|
||||
menuItemEntry->text = menuItemText;
|
||||
menuItemEntry->textLen = menuItemEntry->text.size();
|
||||
menuItemEntry->controllerSlot = controllerSlot;
|
||||
|
||||
// Original interpreter on PC used the length of the first item for drawing
|
||||
// At least in KQ2 on Apple IIgs follow-up items are longer, which would result in graphic glitches.
|
||||
// That's why we remember the longest item and draw according to that
|
||||
if (curMenuEntry->maxItemTextLen < menuItemEntry->textLen) {
|
||||
curMenuEntry->maxItemTextLen = menuItemEntry->textLen;
|
||||
}
|
||||
|
||||
if (curMenuEntry->itemCount == 0) {
|
||||
if (!_vm->isLanguageRTL()) {
|
||||
// for first menu item of menu calculated column
|
||||
if (menuItemEntry->textLen + curMenuEntry->column < (FONT_COLUMN_CHARACTERS - 1)) {
|
||||
_setupMenuItemColumn = curMenuEntry->column;
|
||||
} else {
|
||||
_setupMenuItemColumn = (FONT_COLUMN_CHARACTERS - 1) - menuItemEntry->textLen;
|
||||
}
|
||||
} else {
|
||||
_setupMenuItemColumn = curMenuEntry->column + curMenuEntry->textLen - menuItemEntry->textLen;
|
||||
if (_setupMenuItemColumn < 2)
|
||||
_setupMenuItemColumn = 2;
|
||||
}
|
||||
}
|
||||
|
||||
menuItemEntry->row = 2 + curMenuEntry->itemCount;
|
||||
menuItemEntry->column = _setupMenuItemColumn;
|
||||
|
||||
_itemArray.push_back(menuItemEntry);
|
||||
|
||||
curMenuEntry->itemCount++;
|
||||
}
|
||||
|
||||
void GfxMenu::submit() {
|
||||
if ((_array.size() == 0) || (_itemArray.size() == 0))
|
||||
return;
|
||||
|
||||
// WORKAROUND: For Apple II gs we add a Speed menu
|
||||
if (_vm->getPlatform() == Common::kPlatformApple2GS && ConfMan.getBool("apple2gs_speedmenu")) {
|
||||
uint16 maxControllerSlot = 0;
|
||||
for (auto &menuItem : _itemArray)
|
||||
if (menuItem->controllerSlot > maxControllerSlot)
|
||||
maxControllerSlot = menuItem->controllerSlot;
|
||||
for (uint16 curMapping = 0; curMapping < MAX_CONTROLLER_KEYMAPPINGS; curMapping++)
|
||||
if (_vm->_game.controllerKeyMapping[curMapping].controllerSlot > maxControllerSlot)
|
||||
maxControllerSlot = _vm->_game.controllerKeyMapping[curMapping].controllerSlot;
|
||||
|
||||
if (maxControllerSlot >= 0xff - 4)
|
||||
warning("GfxMenu::submit : failed to add 'Speed' menu");
|
||||
else {
|
||||
_vm->_game.appleIIgsSpeedControllerSlot = maxControllerSlot + 1;
|
||||
addMenu("Speed");
|
||||
addMenuItem("Normal", _vm->_game.appleIIgsSpeedControllerSlot + 2);
|
||||
addMenuItem("Slow", _vm->_game.appleIIgsSpeedControllerSlot + 3);
|
||||
addMenuItem("Fast", _vm->_game.appleIIgsSpeedControllerSlot + 1);
|
||||
addMenuItem("Fastest", _vm->_game.appleIIgsSpeedControllerSlot + 0);
|
||||
}
|
||||
}
|
||||
|
||||
_submitted = true;
|
||||
|
||||
// WORKAROUND: For Apple II gs we try to fix the menu text
|
||||
// On this platform it seems a system font was used and the menu was drawn differently (probably system menu?)
|
||||
// Still the text was misaligned anyway, but it looks worse in our (the original PC) implementation
|
||||
// Atari ST SQ1 had one bad menu entry as well, we fix that too.
|
||||
switch (_vm->getPlatform()) {
|
||||
case Common::kPlatformApple2GS:
|
||||
case Common::kPlatformAtariST: {
|
||||
// Go through all menus
|
||||
int16 menuCount = _array.size();
|
||||
for (int16 menuNr = 0; menuNr < menuCount; menuNr++) {
|
||||
GuiMenuEntry *menuEntry = _array[menuNr];
|
||||
int16 menuItemLastNr = menuEntry->firstItemNr + menuEntry->itemCount;
|
||||
|
||||
// Go through all items of current menu
|
||||
for (int16 menuItemNr = menuEntry->firstItemNr; menuItemNr < menuItemLastNr; menuItemNr++) {
|
||||
GuiMenuItemEntry *menuItemEntry = _itemArray[menuItemNr];
|
||||
|
||||
if (menuItemEntry->textLen < menuEntry->maxItemTextLen) {
|
||||
// current item text is shorter than the maximum?
|
||||
int16 missingCharCount = menuEntry->maxItemTextLen - menuItemEntry->textLen;
|
||||
|
||||
if (menuItemEntry->text.contains('>')) {
|
||||
// text contains '>', we now try to find a '<'
|
||||
// and then add spaces in case this item is shorter than the first item
|
||||
int16 textPos = menuItemEntry->textLen - 1;
|
||||
|
||||
while (textPos > 0) {
|
||||
if (menuItemEntry->text[textPos] == '<')
|
||||
break;
|
||||
textPos--;
|
||||
}
|
||||
|
||||
if (textPos > 0) {
|
||||
while (missingCharCount) {
|
||||
menuItemEntry->text.insertChar(' ', textPos);
|
||||
missingCharCount--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Also check if text consists only of '-', which is the separator
|
||||
// These were sometimes also too small
|
||||
int16 separatorCount = 0;
|
||||
int16 charPos = 0;
|
||||
|
||||
while (charPos < menuItemEntry->textLen) {
|
||||
if (menuItemEntry->text[charPos] != '-')
|
||||
break;
|
||||
separatorCount++;
|
||||
charPos++;
|
||||
}
|
||||
|
||||
if (separatorCount == menuItemEntry->textLen) {
|
||||
// Separator detected
|
||||
while (missingCharCount) {
|
||||
menuItemEntry->text.insertChar('-', 0);
|
||||
missingCharCount--;
|
||||
}
|
||||
} else {
|
||||
// Append spaces to the end to fill it up
|
||||
int16 textPos = menuItemEntry->textLen;
|
||||
while (missingCharCount) {
|
||||
menuItemEntry->text.insertChar(' ', textPos);
|
||||
textPos++;
|
||||
missingCharCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menuItemEntry->textLen = menuItemEntry->text.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GfxMenu::itemEnable(uint16 controllerSlot) {
|
||||
itemEnableDisable(controllerSlot, true);
|
||||
}
|
||||
|
||||
void GfxMenu::itemDisable(uint16 controllerSlot) {
|
||||
itemEnableDisable(controllerSlot, false);
|
||||
}
|
||||
|
||||
void GfxMenu::itemEnableDisable(uint16 controllerSlot, bool enabled) {
|
||||
for (auto &menuItem : _itemArray) {
|
||||
if (menuItem->controllerSlot == controllerSlot) {
|
||||
menuItem->enabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GfxMenu::itemEnableAll() {
|
||||
for (auto &menuItem : _itemArray) {
|
||||
menuItem->enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// return true, in case a menu was actually created and submitted by the scripts
|
||||
bool GfxMenu::isAvailable() {
|
||||
return _submitted;
|
||||
}
|
||||
|
||||
void GfxMenu::accessAllow() {
|
||||
_allowed = true;
|
||||
}
|
||||
|
||||
void GfxMenu::accessDeny() {
|
||||
_allowed = false;
|
||||
}
|
||||
|
||||
void GfxMenu::delayedExecuteViaKeyboard() {
|
||||
_delayedExecuteViaKeyboard = true;
|
||||
_delayedExecuteViaMouse = false;
|
||||
}
|
||||
void GfxMenu::delayedExecuteViaMouse() {
|
||||
_delayedExecuteViaKeyboard = false;
|
||||
_delayedExecuteViaMouse = true;
|
||||
}
|
||||
|
||||
bool GfxMenu::delayedExecuteActive() {
|
||||
return _delayedExecuteViaKeyboard || _delayedExecuteViaMouse;
|
||||
}
|
||||
|
||||
void GfxMenu::execute() {
|
||||
bool viaKeyboard = _delayedExecuteViaKeyboard;
|
||||
bool viaMouse = _delayedExecuteViaMouse;
|
||||
_delayedExecuteViaKeyboard = false;
|
||||
_delayedExecuteViaMouse = false;
|
||||
|
||||
// got submitted? -> safety check
|
||||
if (!_submitted)
|
||||
return;
|
||||
|
||||
// access allowed at the moment?
|
||||
if (!_allowed)
|
||||
return;
|
||||
|
||||
_text->charPos_Push();
|
||||
_text->charAttrib_Push();
|
||||
_text->clearLine(0, _text->calculateTextBackground(15));
|
||||
|
||||
// Draw all menus
|
||||
for (uint16 menuNr = 0; menuNr < _array.size(); menuNr++) {
|
||||
drawMenuName(menuNr, false);
|
||||
}
|
||||
|
||||
// Draw last selected menu
|
||||
_drawnMenuNr = _lastSelectedMenuNr;
|
||||
|
||||
// Unless we are in "via mouse" mode. In that case check current mouse position
|
||||
if (viaMouse) {
|
||||
int16 mouseRow = _vm->_mouse.pos.y;
|
||||
int16 mouseColumn = _vm->_mouse.pos.x;
|
||||
_gfx->translateDisplayPosToFontScreen(mouseColumn, mouseRow);
|
||||
|
||||
mouseFindMenuSelection(mouseRow, mouseColumn, _drawnMenuNr, _mouseModeItemNr);
|
||||
}
|
||||
|
||||
if (_drawnMenuNr >= 0) {
|
||||
if (viaKeyboard) {
|
||||
#ifdef USE_TTS
|
||||
_vm->_queueNextText = true;
|
||||
#endif
|
||||
drawMenu(_drawnMenuNr, _array[_drawnMenuNr]->selectedItemNr);
|
||||
}
|
||||
if (viaMouse) {
|
||||
drawMenu(_drawnMenuNr, _mouseModeItemNr);
|
||||
}
|
||||
}
|
||||
|
||||
if (viaKeyboard) {
|
||||
_vm->cycleInnerLoopActive(CYCLE_INNERLOOP_MENU_VIA_KEYBOARD);
|
||||
} else if (viaMouse) {
|
||||
_vm->cycleInnerLoopActive(CYCLE_INNERLOOP_MENU_VIA_MOUSE);
|
||||
}
|
||||
|
||||
do {
|
||||
_vm->processAGIEvents();
|
||||
} while (_vm->cycleInnerLoopIsActive() && !(_vm->shouldQuit() || _vm->_restartGame));
|
||||
|
||||
if (_drawnMenuNr >= 0) {
|
||||
removeActiveMenu(_drawnMenuNr);
|
||||
}
|
||||
|
||||
if (viaKeyboard) {
|
||||
// In "via Keyboard" mode, remember last selection
|
||||
_lastSelectedMenuNr = _drawnMenuNr;
|
||||
}
|
||||
|
||||
_text->charAttrib_Pop();
|
||||
_text->charPos_Pop();
|
||||
|
||||
// Restore status line
|
||||
if (_text->statusEnabled()) {
|
||||
_text->statusDraw();
|
||||
} else {
|
||||
if (_text->getWindowRowMin() == 0) {
|
||||
// WORKAROUND: Playarea starts right at the stop, so instead of clearing that part, render it from playarea
|
||||
// Required for at least Donald Duck
|
||||
// This was not done by original AGI, which means the upper pixel line were cleared in this case.
|
||||
_gfx->render_Block(0, 0, SCRIPT_WIDTH, FONT_VISUAL_HEIGHT);
|
||||
} else {
|
||||
_text->clearLine(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GfxMenu::drawMenuName(int16 menuNr, bool inverted) {
|
||||
GuiMenuEntry *menuEntry = _array[menuNr];
|
||||
bool disabledLook = false;
|
||||
|
||||
// Don't draw in case there is no text
|
||||
if (!menuEntry->text.size())
|
||||
return;
|
||||
|
||||
if (!inverted) {
|
||||
_text->charAttrib_Set(0, _text->calculateTextBackground(15));
|
||||
} else {
|
||||
#ifdef USE_TTS
|
||||
_vm->sayText(menuEntry->text.c_str(), Common::TextToSpeechManager::INTERRUPT, false);
|
||||
#endif
|
||||
_text->charAttrib_Set(15, _text->calculateTextBackground(0));
|
||||
}
|
||||
|
||||
_text->charPos_Set(menuEntry->row, menuEntry->column);
|
||||
|
||||
if (menuEntry->itemCount == 0)
|
||||
disabledLook = true;
|
||||
|
||||
_text->displayText(menuEntry->text.c_str(), disabledLook);
|
||||
}
|
||||
|
||||
void GfxMenu::drawItemName(int16 itemNr, bool inverted) {
|
||||
GuiMenuItemEntry *itemEntry = _itemArray[itemNr];
|
||||
bool disabledLook = false;
|
||||
|
||||
if (!inverted) {
|
||||
_text->charAttrib_Set(0, _text->calculateTextBackground(15));
|
||||
} else {
|
||||
#ifdef USE_TTS
|
||||
if (_vm->_queueNextText) {
|
||||
_vm->sayText(itemEntry->text.c_str(), Common::TextToSpeechManager::QUEUE, false);
|
||||
_vm->_queueNextText = false;
|
||||
} else {
|
||||
_vm->sayText(itemEntry->text.c_str(), Common::TextToSpeechManager::INTERRUPT, false);
|
||||
}
|
||||
#endif
|
||||
|
||||
_text->charAttrib_Set(15, _text->calculateTextBackground(0));
|
||||
}
|
||||
|
||||
_text->charPos_Set(itemEntry->row, itemEntry->column);
|
||||
|
||||
if (itemEntry->enabled == false)
|
||||
disabledLook = true;
|
||||
|
||||
_text->displayText(itemEntry->text.c_str(), disabledLook);
|
||||
}
|
||||
|
||||
void GfxMenu::drawMenu(int16 selectedMenuNr, int16 selectedMenuItemNr) {
|
||||
GuiMenuEntry *menuEntry = _array[selectedMenuNr];
|
||||
GuiMenuItemEntry *itemEntry = _itemArray[menuEntry->firstItemNr];
|
||||
int16 itemNr = menuEntry->firstItemNr;
|
||||
int16 itemCount = menuEntry->itemCount;
|
||||
|
||||
// draw menu name as inverted
|
||||
drawMenuName(selectedMenuNr, true);
|
||||
|
||||
// calculate active menu dimensions
|
||||
_drawnMenuHeight = (menuEntry->itemCount + 2) * FONT_VISUAL_HEIGHT;
|
||||
_drawnMenuWidth = (menuEntry->maxItemTextLen * FONT_VISUAL_WIDTH) + 8;
|
||||
_drawnMenuY = (1 - _text->getWindowRowMin()) * FONT_VISUAL_HEIGHT;
|
||||
//(menuEntry->itemCount + 3 - _text->getWindowRowMin()) * FONT_VISUAL_HEIGHT - 1;
|
||||
_drawnMenuX = (itemEntry->column - 1) * FONT_VISUAL_WIDTH;
|
||||
|
||||
_gfx->drawBox(_drawnMenuX, _drawnMenuY, _drawnMenuWidth, _drawnMenuHeight, 15, 0);
|
||||
|
||||
while (itemCount) {
|
||||
if (itemNr == selectedMenuItemNr) {
|
||||
drawItemName(itemNr, true);
|
||||
} else {
|
||||
drawItemName(itemNr, false);
|
||||
}
|
||||
itemNr++;
|
||||
itemCount--;
|
||||
}
|
||||
}
|
||||
|
||||
void GfxMenu::removeActiveMenu(int16 selectedMenuNr) {
|
||||
// draw menu name normally again
|
||||
drawMenuName(selectedMenuNr, false);
|
||||
|
||||
// overwrite actual menu items by rendering play screen
|
||||
_gfx->render_Block(_drawnMenuX, _drawnMenuY, _drawnMenuWidth, _drawnMenuHeight);
|
||||
}
|
||||
|
||||
void GfxMenu::keyPress(uint16 newKey) {
|
||||
GuiMenuEntry *menuEntry = _array[_drawnMenuNr];
|
||||
GuiMenuItemEntry *itemEntry = _itemArray[menuEntry->selectedItemNr];
|
||||
int16 newMenuNr = _drawnMenuNr;
|
||||
int16 newItemNr = menuEntry->selectedItemNr;
|
||||
|
||||
switch (newKey) {
|
||||
case AGI_KEY_ENTER:
|
||||
// check, if current item is actually enabled
|
||||
if (!itemEntry->enabled)
|
||||
return;
|
||||
|
||||
// Trigger controller
|
||||
_vm->_game.controllerOccurred[itemEntry->controllerSlot] = true;
|
||||
|
||||
_vm->cycleInnerLoopInactive(); // exit execute-loop
|
||||
break;
|
||||
case AGI_KEY_ESCAPE:
|
||||
_vm->cycleInnerLoopInactive(); // exit execute-loop
|
||||
break;
|
||||
|
||||
// these here change menu item
|
||||
case AGI_KEY_UP:
|
||||
newItemNr--;
|
||||
break;
|
||||
case AGI_KEY_DOWN:
|
||||
newItemNr++;
|
||||
break;
|
||||
case AGI_KEY_PAGE_UP:
|
||||
// select first item of current menu
|
||||
newItemNr = menuEntry->firstItemNr;
|
||||
break;
|
||||
case AGI_KEY_PAGE_DOWN:
|
||||
// select last item of current menu
|
||||
newItemNr = menuEntry->firstItemNr + menuEntry->itemCount - 1;
|
||||
break;
|
||||
|
||||
case AGI_KEY_LEFT:
|
||||
if (!_vm->isLanguageRTL())
|
||||
newMenuNr--;
|
||||
else
|
||||
newMenuNr++;
|
||||
break;
|
||||
case AGI_KEY_RIGHT:
|
||||
if (!_vm->isLanguageRTL())
|
||||
newMenuNr++;
|
||||
else
|
||||
newMenuNr--;
|
||||
break;
|
||||
case AGI_KEY_HOME:
|
||||
// select first menu
|
||||
newMenuNr = 0;
|
||||
break;
|
||||
case AGI_KEY_END:
|
||||
// select last menu
|
||||
newMenuNr = _array.size() - 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (newMenuNr != _drawnMenuNr) {
|
||||
// selected menu was changed
|
||||
int16 lastMenuNr = _array.size() - 1;
|
||||
|
||||
if (newMenuNr < 0) {
|
||||
newMenuNr = lastMenuNr;
|
||||
} else if (newMenuNr > lastMenuNr) {
|
||||
newMenuNr = 0;
|
||||
}
|
||||
|
||||
if (newMenuNr != _drawnMenuNr) {
|
||||
#ifdef USE_TTS
|
||||
_vm->_queueNextText = true;
|
||||
#endif
|
||||
removeActiveMenu(_drawnMenuNr);
|
||||
_drawnMenuNr = newMenuNr;
|
||||
drawMenu(_drawnMenuNr, _array[_drawnMenuNr]->selectedItemNr);
|
||||
}
|
||||
}
|
||||
|
||||
if (newItemNr != menuEntry->selectedItemNr) {
|
||||
// selected item was changed
|
||||
int16 lastItemNr = menuEntry->firstItemNr + menuEntry->itemCount - 1;
|
||||
|
||||
if (newItemNr < menuEntry->firstItemNr) {
|
||||
newItemNr = lastItemNr;
|
||||
} else if (newItemNr > lastItemNr) {
|
||||
newItemNr = menuEntry->firstItemNr;
|
||||
}
|
||||
|
||||
if (newItemNr != menuEntry->selectedItemNr) {
|
||||
// still changed after clip -> draw changes
|
||||
drawItemName(menuEntry->selectedItemNr, false);
|
||||
drawItemName(newItemNr, true);
|
||||
menuEntry->selectedItemNr = newItemNr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This gets called:
|
||||
// During "via keyboard" mode in case user actively clicks on something
|
||||
// During "via mouse" mode all the time, so that current mouse cursor position modifies active selection
|
||||
// In "via mouse" mode, we check if user let go of the left mouse button and then select the item that way
|
||||
void GfxMenu::mouseEvent(uint16 newKey) {
|
||||
// Find out, where current mouse cursor actually is
|
||||
int16 mouseRow = _vm->_mouse.pos.y;
|
||||
int16 mouseColumn = _vm->_mouse.pos.x;
|
||||
|
||||
_gfx->translateDisplayPosToFontScreen(mouseColumn, mouseRow);
|
||||
|
||||
int16 activeMenuNr, activeItemNr;
|
||||
mouseFindMenuSelection(mouseRow, mouseColumn, activeMenuNr, activeItemNr);
|
||||
|
||||
switch (newKey) {
|
||||
case AGI_MOUSE_BUTTON_LEFT:
|
||||
// User clicked somewhere, in this case check if user clicked on status bar or on one of the currently shown menu items
|
||||
// Happens in "via keyboard" mode only
|
||||
// We do not close menu in case user clicked on something invalid
|
||||
|
||||
if (activeItemNr >= 0) {
|
||||
GuiMenuItemEntry *itemEntry = _itemArray[activeItemNr];
|
||||
if (!itemEntry->enabled)
|
||||
return;
|
||||
|
||||
// Trigger controller
|
||||
_vm->_game.controllerOccurred[itemEntry->controllerSlot] = true;
|
||||
|
||||
_vm->cycleInnerLoopInactive(); // exit execute-loop
|
||||
return;
|
||||
}
|
||||
if (activeMenuNr >= 0) {
|
||||
// User clicked on a menu, check if that menu is already active
|
||||
if (activeMenuNr != _drawnMenuNr) {
|
||||
removeActiveMenu(_drawnMenuNr);
|
||||
_drawnMenuNr = activeMenuNr;
|
||||
drawMenu(_drawnMenuNr, _array[_drawnMenuNr]->selectedItemNr);
|
||||
}
|
||||
}
|
||||
return; // exit all the time, we do not want to change the user selection while in "via keyboard" mode
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// If mouse is not selecting any menu, just use the last menu instead
|
||||
if (activeMenuNr < 0) {
|
||||
activeMenuNr = _drawnMenuNr;
|
||||
}
|
||||
|
||||
if (activeMenuNr != _drawnMenuNr) {
|
||||
if (_drawnMenuNr >= 0) {
|
||||
removeActiveMenu(_drawnMenuNr);
|
||||
}
|
||||
|
||||
_drawnMenuNr = activeMenuNr;
|
||||
|
||||
if (_drawnMenuNr >= 0) {
|
||||
drawMenu(_drawnMenuNr, activeItemNr);
|
||||
}
|
||||
_mouseModeItemNr = activeItemNr;
|
||||
}
|
||||
|
||||
if (activeItemNr != _mouseModeItemNr) {
|
||||
if (_mouseModeItemNr >= 0) {
|
||||
drawItemName(_mouseModeItemNr, false);
|
||||
}
|
||||
if (activeItemNr >= 0) {
|
||||
drawItemName(activeItemNr, true);
|
||||
}
|
||||
_mouseModeItemNr = activeItemNr;
|
||||
}
|
||||
|
||||
if (_vm->_mouse.button == kAgiMouseButtonUp) {
|
||||
// User has stopped pressing the mouse button, if any item number is selected -> execute it
|
||||
if (activeItemNr >= 0) {
|
||||
GuiMenuItemEntry *itemEntry = _itemArray[activeItemNr];
|
||||
if (itemEntry->enabled) {
|
||||
// Trigger controller
|
||||
_vm->_game.controllerOccurred[itemEntry->controllerSlot] = true;
|
||||
}
|
||||
}
|
||||
|
||||
_vm->cycleInnerLoopInactive(); // exit execute-loop
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GfxMenu::mouseFindMenuSelection(int16 mouseRow, int16 mouseColumn, int16 &activeMenuNr, int16 &activeMenuItemNr) {
|
||||
GuiMenuEntry *menuEntry = nullptr;
|
||||
int16 menuCount = _array.size();
|
||||
|
||||
for (int16 menuNr = 0; menuNr < menuCount; menuNr++) {
|
||||
menuEntry = _array[menuNr];
|
||||
|
||||
if (mouseRow == menuEntry->row) {
|
||||
// line match
|
||||
if ((mouseColumn >= menuEntry->column) && (mouseColumn < (menuEntry->column + menuEntry->textLen))) {
|
||||
// full match
|
||||
activeMenuNr = menuNr;
|
||||
activeMenuItemNr = -1; // no item selected
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now also check current menu
|
||||
if (_drawnMenuNr >= 0) {
|
||||
// A menu is currently shown
|
||||
menuEntry = _array[_drawnMenuNr];
|
||||
|
||||
int16 itemNr = menuEntry->firstItemNr;
|
||||
int16 itemCount = menuEntry->itemCount;
|
||||
|
||||
while (itemCount) {
|
||||
GuiMenuItemEntry *itemEntry = _itemArray[itemNr];
|
||||
|
||||
if (mouseRow == itemEntry->row) {
|
||||
// line match
|
||||
if ((mouseColumn >= itemEntry->column) && (mouseColumn < (itemEntry->column + itemEntry->textLen))) {
|
||||
// full match
|
||||
if (itemEntry->enabled) {
|
||||
// Only see it, when it's currently enabled
|
||||
activeMenuNr = _drawnMenuNr;
|
||||
activeMenuItemNr = itemNr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
itemNr++;
|
||||
itemCount--;
|
||||
}
|
||||
}
|
||||
activeMenuNr = -1;
|
||||
activeMenuItemNr = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
122
engines/agi/menu.h
Normal file
122
engines/agi/menu.h
Normal file
@@ -0,0 +1,122 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_MENU_H
|
||||
#define AGI_MENU_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
struct GuiMenuEntry {
|
||||
Common::String text;
|
||||
int16 textLen;
|
||||
|
||||
int16 row;
|
||||
int16 column;
|
||||
|
||||
int16 itemCount; // total number of menu items
|
||||
int16 firstItemNr; // first menu item number, points into _itemArray[]
|
||||
|
||||
int16 selectedItemNr; // currently selected menu item
|
||||
|
||||
int16 maxItemTextLen; // maximum text length of all menu items
|
||||
};
|
||||
typedef Common::Array<GuiMenuEntry *> GuiMenuArray;
|
||||
|
||||
struct GuiMenuItemEntry {
|
||||
Common::String text;
|
||||
int16 textLen;
|
||||
|
||||
int16 row;
|
||||
int16 column;
|
||||
|
||||
bool enabled; // enabled-state, set by scripts
|
||||
uint16 controllerSlot; // controller to trigger, when item is executed
|
||||
};
|
||||
typedef Common::Array<GuiMenuItemEntry *> GuiMenuItemArray;
|
||||
|
||||
class GfxMenu {
|
||||
public:
|
||||
GfxMenu(AgiEngine *vm, GfxMgr *gfx, PictureMgr *picture, TextMgr *text);
|
||||
~GfxMenu();
|
||||
|
||||
void addMenu(const char *menuText);
|
||||
void addMenuItem(const char *menuText, uint16 controlCode);
|
||||
void submit();
|
||||
void itemEnable(uint16 controllerSlot);
|
||||
void itemDisable(uint16 controllerSlot);
|
||||
void itemEnableAll();
|
||||
|
||||
void keyPress(uint16 newKey);
|
||||
void mouseEvent(uint16 newKey);
|
||||
|
||||
bool isAvailable();
|
||||
|
||||
void accessAllow();
|
||||
void accessDeny();
|
||||
|
||||
void delayedExecuteViaKeyboard();
|
||||
void delayedExecuteViaMouse();
|
||||
bool delayedExecuteActive();
|
||||
void execute();
|
||||
|
||||
private:
|
||||
void itemEnableDisable(uint16 controllerSlot, bool enabled);
|
||||
|
||||
void drawMenuName(int16 menuNr, bool inverted);
|
||||
void drawItemName(int16 itemNr, bool inverted);
|
||||
void drawMenu(int16 selectedMenuNr, int16 selectedMenuItemNr);
|
||||
void removeActiveMenu(int16 selectedMenuNr);
|
||||
|
||||
void mouseFindMenuSelection(int16 mouseRow, int16 mouseColumn, int16 &activeMenuNr, int16 &activeMenuItemNr);
|
||||
|
||||
AgiEngine *_vm;
|
||||
GfxMgr *_gfx;
|
||||
PictureMgr *_picture;
|
||||
TextMgr *_text;
|
||||
|
||||
bool _allowed;
|
||||
bool _submitted;
|
||||
bool _delayedExecuteViaKeyboard;
|
||||
bool _delayedExecuteViaMouse;
|
||||
|
||||
// for initial setup of the menu
|
||||
int16 _setupMenuColumn;
|
||||
int16 _setupMenuItemColumn;
|
||||
|
||||
GuiMenuArray _array;
|
||||
GuiMenuItemArray _itemArray;
|
||||
|
||||
int16 _lastSelectedMenuNr; // only used for "via keyboard" mode
|
||||
|
||||
int16 _drawnMenuNr;
|
||||
|
||||
uint16 _drawnMenuHeight;
|
||||
uint16 _drawnMenuWidth;
|
||||
int16 _drawnMenuY;
|
||||
int16 _drawnMenuX;
|
||||
|
||||
// Following variables are used in "via mouse" mode
|
||||
int16 _mouseModeItemNr;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_MENU_H */
|
||||
490
engines/agi/metaengine.cpp
Normal file
490
engines/agi/metaengine.cpp
Normal file
@@ -0,0 +1,490 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/config-manager.h"
|
||||
#include "common/file.h"
|
||||
#include "common/md5.h"
|
||||
#include "common/savefile.h"
|
||||
#include "common/textconsole.h"
|
||||
#include "common/translation.h"
|
||||
|
||||
#include "graphics/thumbnail.h"
|
||||
#include "graphics/surface.h"
|
||||
|
||||
#include "base/plugins.h"
|
||||
#include "engines/advancedDetector.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/preagi/preagi.h"
|
||||
#include "agi/preagi/mickey.h"
|
||||
#include "agi/preagi/troll.h"
|
||||
#include "agi/preagi/winnie.h"
|
||||
|
||||
#include "agi/detection.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
uint32 AgiBase::getGameID() const {
|
||||
return _gameDescription->gameID;
|
||||
}
|
||||
|
||||
uint32 AgiBase::getFeatures() const {
|
||||
return _gameFeatures;
|
||||
}
|
||||
|
||||
Common::Platform AgiBase::getPlatform() const {
|
||||
return _gameDescription->desc.platform;
|
||||
}
|
||||
|
||||
Common::Language AgiBase::getLanguage() const {
|
||||
if (_gameDescription->desc.language != Common::UNK_LANG)
|
||||
return _gameDescription->desc.language;
|
||||
else if (ConfMan.hasKey("language"))
|
||||
return Common::parseLanguage(ConfMan.get("language"));
|
||||
else
|
||||
return Common::UNK_LANG;
|
||||
}
|
||||
|
||||
bool AgiBase::isLanguageRTL() const {
|
||||
return getLanguage() == Common::HE_ISR;
|
||||
}
|
||||
|
||||
uint16 AgiBase::getVersion() const {
|
||||
return _gameVersion;
|
||||
}
|
||||
|
||||
uint16 AgiBase::getGameType() const {
|
||||
return _gameDescription->gameType;
|
||||
}
|
||||
|
||||
const char *AgiBase::getGameMD5() const {
|
||||
return _gameDescription->desc.filesDescriptions[0].md5;
|
||||
}
|
||||
|
||||
void AgiBase::initFeatures() {
|
||||
_gameFeatures = _gameDescription->features;
|
||||
}
|
||||
|
||||
void AgiBase::initVersion() {
|
||||
_gameVersion = _gameDescription->version;
|
||||
}
|
||||
|
||||
const char *AgiBase::getDiskName(uint16 id) {
|
||||
for (int i = 0; _gameDescription->desc.filesDescriptions[i].fileName != nullptr; i++)
|
||||
if (_gameDescription->desc.filesDescriptions[i].fileType == id)
|
||||
return _gameDescription->desc.filesDescriptions[i].fileName;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool AgiBase::hasFeature(EngineFeature f) const {
|
||||
return
|
||||
(f == kSupportsReturnToLauncher) ||
|
||||
(f == kSupportsLoadingDuringRuntime) ||
|
||||
(f == kSupportsSavingDuringRuntime);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
static const ADExtraGuiOptionsMap optionsList[] = {
|
||||
{
|
||||
GAMEOPTION_ORIGINAL_SAVELOAD,
|
||||
{
|
||||
_s("Use original save/load screens"),
|
||||
_s("Use the original save/load screens instead of the ScummVM ones"),
|
||||
"originalsaveload",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_AMIGA_ALTERNATIVE_PALETTE,
|
||||
{
|
||||
_s("Use an alternative palette"),
|
||||
_s("Use an alternative palette, common for all Amiga games. This was the old behavior"),
|
||||
"altamigapalette",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_ENABLE_MOUSE,
|
||||
{
|
||||
_s("Mouse support"),
|
||||
_s("Enables mouse support. Allows to use mouse for movement and in game menus."),
|
||||
"mousesupport",
|
||||
true,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_ENABLE_PREDICTIVE_FOR_MOUSE,
|
||||
{
|
||||
_s("Predictive Input Dialog on mouse click"),
|
||||
_s("Enables the assistive Predictive Input Dialog specifically for when clicking the left mouse button within text input fields.\nThe Predictive Input Dialog can still be activated on demand if there's a specified key mapping for it"),
|
||||
"predictivedlgonmouseclick",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_USE_HERCULES_FONT,
|
||||
{
|
||||
_s("Use Hercules hires font"),
|
||||
_s("Uses Hercules hires font, when font file is available."),
|
||||
"herculesfont",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_COMMAND_PROMPT_WINDOW,
|
||||
{
|
||||
_s("Pause when entering commands"),
|
||||
_s("Shows a command prompt window and pauses the game (like in SCI) instead of a real-time prompt."),
|
||||
"commandpromptwindow",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_APPLE2GS_ADD_SPEED_MENU,
|
||||
{
|
||||
_s("Add speed menu"),
|
||||
_s("Add game speed menu (similar to PC version)"),
|
||||
"apple2gs_speedmenu",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_COPY_PROTECTION,
|
||||
{
|
||||
_s("Enable copy protection"),
|
||||
_s("Enable any copy protection that would otherwise be bypassed by default."),
|
||||
"copy_protection",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
GAMEOPTION_PCJR_SN76496_16BIT,
|
||||
{
|
||||
_s("Use PCjr's sound chip in 16-bit shift mode"),
|
||||
_s("In PCjr sound mode, emulate a non-standard SN76496 sound chip, similar to chips found in SEGA Master System consoles. Allows certain music effects, especially in fanmade games, but original Sierra music designed strictly for PCjr may sound wrong."),
|
||||
"pcjr_16bitnoise",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
|
||||
#ifdef USE_TTS
|
||||
{
|
||||
GAMEOPTION_TTS,
|
||||
{
|
||||
_s("Enable Text to Speech"),
|
||||
_s("Use TTS to read text in the game (if TTS is available)"),
|
||||
"tts_enabled",
|
||||
false,
|
||||
0,
|
||||
0
|
||||
}
|
||||
},
|
||||
#endif
|
||||
|
||||
AD_EXTRA_GUI_OPTIONS_TERMINATOR
|
||||
};
|
||||
|
||||
using namespace Agi;
|
||||
|
||||
class AgiMetaEngine : public AdvancedMetaEngine<Agi::AGIGameDescription> {
|
||||
public:
|
||||
const char *getName() const override {
|
||||
return "agi";
|
||||
}
|
||||
|
||||
const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
|
||||
return optionsList;
|
||||
}
|
||||
|
||||
Common::Error createInstance(OSystem *syst, Engine **engine, const Agi::AGIGameDescription *gd) const override;
|
||||
|
||||
SaveStateList listSaves(const char *target) const override;
|
||||
int getMaximumSaveSlot() const override;
|
||||
bool removeSaveState(const char *target, int slot) const override;
|
||||
SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const override;
|
||||
|
||||
bool hasFeature(MetaEngineFeature f) const override;
|
||||
};
|
||||
|
||||
bool AgiMetaEngine::hasFeature(MetaEngineFeature f) const {
|
||||
return
|
||||
(f == kSupportsListSaves) ||
|
||||
(f == kSupportsLoadingDuringStartup) ||
|
||||
(f == kSupportsDeleteSave) ||
|
||||
(f == kSavesSupportMetaInfo) ||
|
||||
(f == kSavesSupportThumbnail) ||
|
||||
(f == kSavesSupportCreationDate) ||
|
||||
(f == kSavesSupportPlayTime) ||
|
||||
(f == kSimpleSavesNames);
|
||||
}
|
||||
|
||||
Common::Error AgiMetaEngine::createInstance(OSystem *syst, Engine **engine, const Agi::AGIGameDescription *gd) const {
|
||||
switch (gd->gameType) {
|
||||
case Agi::GType_PreAGI:
|
||||
switch (gd->gameID) {
|
||||
case GID_MICKEY:
|
||||
*engine = new Agi::MickeyEngine(syst, gd);
|
||||
break;
|
||||
case GID_TROLL:
|
||||
*engine = new Agi::TrollEngine(syst, gd);
|
||||
break;
|
||||
case GID_WINNIE:
|
||||
*engine = new Agi::WinnieEngine(syst, gd);
|
||||
break;
|
||||
default:
|
||||
return Common::kUnsupportedGameidError;
|
||||
}
|
||||
break;
|
||||
case Agi::GType_V1:
|
||||
case Agi::GType_V2:
|
||||
case Agi::GType_V3:
|
||||
case Agi::GType_A2:
|
||||
case Agi::GType_GAL:
|
||||
*engine = new Agi::AgiEngine(syst, gd);
|
||||
break;
|
||||
default:
|
||||
return Common::kUnsupportedGameidError;
|
||||
}
|
||||
|
||||
return Common::kNoError;
|
||||
}
|
||||
|
||||
SaveStateList AgiMetaEngine::listSaves(const char *target) const {
|
||||
const uint32 AGIflag = MKTAG('A', 'G', 'I', ':');
|
||||
Common::SaveFileManager *saveFileMan = g_system->getSavefileManager();
|
||||
Common::StringArray filenames;
|
||||
Common::String pattern = target;
|
||||
pattern += ".###";
|
||||
|
||||
filenames = saveFileMan->listSavefiles(pattern);
|
||||
|
||||
SaveStateList saveList;
|
||||
for (const auto &file : filenames) {
|
||||
// Obtain the last 3 digits of the filename, since they correspond to the save slot
|
||||
int slotNr = atoi(file.c_str() + file.size() - 3);
|
||||
|
||||
if (slotNr >= 0 && slotNr <= 999) {
|
||||
Common::InSaveFile *in = saveFileMan->openForLoading(file);
|
||||
if (in) {
|
||||
uint32 type = in->readUint32BE();
|
||||
char description[31];
|
||||
|
||||
if (type == AGIflag) {
|
||||
uint16 descriptionPos = 0;
|
||||
|
||||
in->read(description, 31);
|
||||
|
||||
// Security-check, if saveDescription has a terminating NUL
|
||||
while (description[descriptionPos]) {
|
||||
descriptionPos++;
|
||||
if (descriptionPos >= sizeof(description))
|
||||
break;
|
||||
}
|
||||
if (descriptionPos >= sizeof(description)) {
|
||||
Common::strcpy_s(description, "[broken saved game]");
|
||||
}
|
||||
} else {
|
||||
Common::strcpy_s(description, "[not an AGI saved game]");
|
||||
}
|
||||
|
||||
delete in;
|
||||
|
||||
saveList.push_back(SaveStateDescriptor(this, slotNr, description));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort saves based on slot number.
|
||||
Common::sort(saveList.begin(), saveList.end(), SaveStateDescriptorSlotComparator());
|
||||
return saveList;
|
||||
}
|
||||
|
||||
bool AgiMetaEngine::removeSaveState(const char *target, int slot) const {
|
||||
Common::String fileName = Common::String::format("%s.%03d", target, slot);
|
||||
return g_system->getSavefileManager()->removeSavefile(fileName);
|
||||
}
|
||||
|
||||
int AgiMetaEngine::getMaximumSaveSlot() const { return 999; }
|
||||
|
||||
SaveStateDescriptor AgiMetaEngine::querySaveMetaInfos(const char *target, int slotNr) const {
|
||||
const uint32 AGIflag = MKTAG('A', 'G', 'I', ':');
|
||||
Common::String fileName = Common::String::format("%s.%03d", target, slotNr);
|
||||
|
||||
Common::InSaveFile *in = g_system->getSavefileManager()->openForLoading(fileName);
|
||||
|
||||
if (in) {
|
||||
if (in->readUint32BE() != AGIflag) {
|
||||
delete in;
|
||||
return SaveStateDescriptor();
|
||||
}
|
||||
|
||||
char description[31];
|
||||
uint16 descriptionPos = 0;
|
||||
|
||||
in->read(description, 31);
|
||||
|
||||
while (description[descriptionPos]) {
|
||||
descriptionPos++;
|
||||
if (descriptionPos >= sizeof(description))
|
||||
break;
|
||||
}
|
||||
if (descriptionPos >= sizeof(description)) {
|
||||
// broken description, ignore it
|
||||
delete in;
|
||||
|
||||
SaveStateDescriptor descriptor(this, slotNr, "[broken saved game]");
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
SaveStateDescriptor descriptor(this, slotNr, description);
|
||||
|
||||
char saveVersion = in->readByte();
|
||||
if (saveVersion >= 4) {
|
||||
Graphics::Surface *thumbnail;
|
||||
if (!Graphics::loadThumbnail(*in, thumbnail)) {
|
||||
delete in;
|
||||
return SaveStateDescriptor();
|
||||
}
|
||||
|
||||
descriptor.setThumbnail(thumbnail);
|
||||
|
||||
uint32 saveDate = in->readUint32BE();
|
||||
uint16 saveTime = in->readUint16BE();
|
||||
if (saveVersion >= 9) {
|
||||
in->readByte(); // skip over seconds of saveTime (not needed here)
|
||||
}
|
||||
if (saveVersion >= 6) {
|
||||
uint32 playTime = in->readUint32BE();
|
||||
descriptor.setPlayTime(playTime * 1000);
|
||||
}
|
||||
|
||||
int day = (saveDate >> 24) & 0xFF;
|
||||
int month = (saveDate >> 16) & 0xFF;
|
||||
int year = saveDate & 0xFFFF;
|
||||
|
||||
descriptor.setSaveDate(year, month, day);
|
||||
|
||||
int hour = (saveTime >> 8) & 0xFF;
|
||||
int minutes = saveTime & 0xFF;
|
||||
|
||||
descriptor.setSaveTime(hour, minutes);
|
||||
}
|
||||
|
||||
delete in;
|
||||
|
||||
return descriptor;
|
||||
|
||||
} else {
|
||||
SaveStateDescriptor emptySave;
|
||||
// Do not allow save slot 0 (used for auto-saving) to be overwritten.
|
||||
if (slotNr == 0) {
|
||||
emptySave.setAutosave(true);
|
||||
emptySave.setWriteProtectedFlag(true);
|
||||
} else {
|
||||
emptySave.setWriteProtectedFlag(false);
|
||||
}
|
||||
return emptySave;
|
||||
}
|
||||
}
|
||||
|
||||
#if PLUGIN_ENABLED_DYNAMIC(AGI)
|
||||
REGISTER_PLUGIN_DYNAMIC(AGI, PLUGIN_TYPE_ENGINE, AgiMetaEngine);
|
||||
#else
|
||||
REGISTER_PLUGIN_STATIC(AGI, PLUGIN_TYPE_ENGINE, AgiMetaEngine);
|
||||
#endif
|
||||
|
||||
namespace Agi {
|
||||
|
||||
bool AgiBase::canLoadGameStateCurrently(Common::U32String *msg) {
|
||||
if (getGameType() == GType_PreAGI) {
|
||||
if (msg)
|
||||
*msg = _("This game does not support loading");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getFlag(VM_FLAG_MENUS_ACCESSIBLE)) {
|
||||
if (!_noSaveLoadAllowed) {
|
||||
if (!cycleInnerLoopIsActive()) {
|
||||
// We can't allow to restore a game, while inner loop is active
|
||||
// For example Mixed Up Mother Goose has an endless loop for user name input
|
||||
// Which means even if we abort the inner loop, the game would keep on calling
|
||||
// GetString() until something is entered. And this would of course also happen
|
||||
// right after restoring a saved game.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AgiBase::canSaveGameStateCurrently(Common::U32String *msg) {
|
||||
if (getGameType() == GType_PreAGI) {
|
||||
if (msg)
|
||||
*msg = _("This game does not support saving");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getGameID() == GID_BC) // Technically in Black Cauldron we may save anytime
|
||||
return true;
|
||||
|
||||
if (getFlag(VM_FLAG_MENUS_ACCESSIBLE)) {
|
||||
if (!_noSaveLoadAllowed) {
|
||||
if (!cycleInnerLoopIsActive()) {
|
||||
if (promptIsEnabled()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
74
engines/agi/module.mk
Normal file
74
engines/agi/module.mk
Normal file
@@ -0,0 +1,74 @@
|
||||
MODULE := engines/agi
|
||||
|
||||
MODULE_OBJS := \
|
||||
agi.o \
|
||||
checks.o \
|
||||
console.o \
|
||||
cycle.o \
|
||||
disk_image.o \
|
||||
font.o \
|
||||
global.o \
|
||||
graphics.o \
|
||||
inv.o \
|
||||
keyboard.o \
|
||||
loader.o \
|
||||
loader_a2.o \
|
||||
loader_gal.o \
|
||||
loader_gal_a2.o \
|
||||
loader_v1.o \
|
||||
loader_v2.o \
|
||||
loader_v3.o \
|
||||
logic.o \
|
||||
lzw.o \
|
||||
menu.o \
|
||||
metaengine.o \
|
||||
motion.o \
|
||||
objects.o \
|
||||
opcodes.o \
|
||||
op_cmd.o \
|
||||
op_dbg.o \
|
||||
op_test.o \
|
||||
picture.o \
|
||||
picture_gal.o \
|
||||
saveload.o \
|
||||
sound.o \
|
||||
sound_2gs.o \
|
||||
sound_a2.o \
|
||||
sound_coco3.o \
|
||||
sound_midi.o \
|
||||
sound_pcjr.o \
|
||||
sound_sarien.o \
|
||||
sprite.o \
|
||||
systemui.o \
|
||||
text.o \
|
||||
view.o \
|
||||
words.o \
|
||||
preagi/preagi.o \
|
||||
preagi/mickey.o \
|
||||
preagi/picture_mickey_winnie.o \
|
||||
preagi/picture_troll.o \
|
||||
preagi/troll.o \
|
||||
preagi/winnie.o
|
||||
|
||||
# This module can be built as a plugin
|
||||
ifeq ($(ENABLE_AGI), DYNAMIC_PLUGIN)
|
||||
PLUGIN := 1
|
||||
endif
|
||||
|
||||
# Include common rules
|
||||
include $(srcdir)/rules.mk
|
||||
|
||||
# Detection objects
|
||||
DETECT_OBJS += $(MODULE)/detection.o
|
||||
|
||||
# External dependencies of detection.
|
||||
# This is unneeded by the engine module itself,
|
||||
# so separate it completely.
|
||||
DETECT_OBJS += $(MODULE)/wagparser.o
|
||||
|
||||
# Skip building the following objects if a static
|
||||
# module is enabled, because it already has the contents.
|
||||
ifneq ($(ENABLE_AGI), STATIC_PLUGIN)
|
||||
# External dependencies for detection.
|
||||
DETECT_OBJS += $(MODULE)/disk_image.o
|
||||
endif
|
||||
320
engines/agi/motion.cpp
Normal file
320
engines/agi/motion.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "common/random.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
int AgiEngine::checkStep(int delta, int step) {
|
||||
return (-step >= delta) ? 0 : (step <= delta) ? 2 : 1;
|
||||
}
|
||||
|
||||
bool AgiEngine::checkBlock(int16 x, int16 y) {
|
||||
if (x <= _game.block.x1 || x >= _game.block.x2)
|
||||
return false;
|
||||
|
||||
if (y <= _game.block.y1 || y >= _game.block.y2)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AgiEngine::changePos(ScreenObjEntry *screenObj) {
|
||||
const int dx[9] = { 0, 0, 1, 1, 1, 0, -1, -1, -1 };
|
||||
const int dy[9] = { 0, -1, -1, 0, 1, 1, 1, 0, -1 };
|
||||
|
||||
int16 x = screenObj->xPos;
|
||||
int16 y = screenObj->yPos;
|
||||
bool insideBlock = checkBlock(x, y);
|
||||
|
||||
x += screenObj->stepSize * dx[screenObj->direction];
|
||||
y += screenObj->stepSize * dy[screenObj->direction];
|
||||
|
||||
if (checkBlock(x, y) == insideBlock) {
|
||||
screenObj->flags &= ~fMotion;
|
||||
} else {
|
||||
screenObj->flags |= fMotion;
|
||||
screenObj->direction = 0;
|
||||
if (isEgoView(screenObj))
|
||||
setVar(VM_VAR_EGO_DIRECTION, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// WORKAROUND:
|
||||
// Overwrite cycler state with motion data as original AGI did, almost.
|
||||
//
|
||||
// The original AGI interpreter stored motion and cycler data in the same four
|
||||
// bytes of the screen object struct. If a script activated a motion while a
|
||||
// cycler is active, or the opposite, then the previous action's state was
|
||||
// overwritten. Some game scripts rely on this behavior, although probably
|
||||
// unintentionally. We store motion and cycler data in separate fields, so
|
||||
// in order to produce the original behavior that games depend on, we implement
|
||||
// the overwrite behavior.
|
||||
//
|
||||
// However, we make an exception: when cycler data is overwritten, the original
|
||||
// would set an unintended game flag when it completed. It doesn't seem like
|
||||
// anything good can come from setting an unintended flag, so we do not set any
|
||||
// flag when an overwritten cycler completes.
|
||||
//
|
||||
// This affects at least:
|
||||
// - KQ1: when the eagle grabs ego (room 22), Bug #7046
|
||||
// - BC: when the witches disappear at the end of the game (room 12, screen object 13)
|
||||
// - DDP: introduction when the ducks jump, Bug #14170
|
||||
// - KQ2: happened somewhere in the game, LordHoto couldn't remember exactly where
|
||||
void AgiEngine::motionActivated(ScreenObjEntry *screenObj) {
|
||||
if (screenObj->flags & fCycling) { // Is a cycler active?
|
||||
switch (screenObj->cycle) {
|
||||
case kCycleEndOfLoop: // "end.of.loop"
|
||||
case kCycleRevLoop: // "reverse.loop"
|
||||
// This would have overwritten the cycler's flag with wander_count
|
||||
// or follow_stepSize or move_x, and that would cause the cycler
|
||||
// to set an unintended flag if it completes.
|
||||
// We skip setting the unintended flag with ignoreLoopFlag.
|
||||
// Jumping at the eagle in KQ1 room 22 depends on the overwritten
|
||||
// flag not being set. Bug #7046
|
||||
screenObj->ignoreLoopFlag = true;
|
||||
warning("Motion activated for screen object %d while cycler is active", screenObj->objectNr);
|
||||
warning("Original AGI would overwrite flag %d, we skip setting it", screenObj->loop_flag);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WORKAROUND:
|
||||
// Overwrite motion state with cycler data as original AGI did.
|
||||
// This is necessary because we use a different data structure than the
|
||||
// original, but games relied on undefined behavior when activating a
|
||||
// cycler while a motion was in progress.
|
||||
// See comment for motionActivated()
|
||||
void AgiEngine::cyclerActivated(ScreenObjEntry *screenObj) {
|
||||
const char *fieldName;
|
||||
uint8 previousValue;
|
||||
switch (screenObj->motionType) {
|
||||
case kMotionWander:
|
||||
// Overwrite wander count with the cycler's flag.
|
||||
fieldName = "wander_count";
|
||||
previousValue = screenObj->wander_count;
|
||||
screenObj->wander_count = screenObj->loop_flag;
|
||||
break;
|
||||
case kMotionFollowEgo:
|
||||
// Overwrite follow step size with the cycler's flag.
|
||||
fieldName = "follow_stepSize";
|
||||
previousValue = screenObj->follow_stepSize;
|
||||
screenObj->follow_stepSize = screenObj->loop_flag;
|
||||
break;
|
||||
case kMotionMoveObj:
|
||||
// Overwrite move_x with the cycler's flag.
|
||||
// Required for witches to disappear at the end of Black Cauldron, room 12.
|
||||
fieldName = "move_x";
|
||||
previousValue = screenObj->move_x;
|
||||
screenObj->move_x = screenObj->loop_flag;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
warning("Cycler activated for screen object %d while motion is active", screenObj->objectNr);
|
||||
warning("Overwriting %s: %d with flag number %d, as original AGI did", fieldName, previousValue, screenObj->loop_flag);
|
||||
}
|
||||
|
||||
void AgiEngine::motionWander(ScreenObjEntry *screenObj) {
|
||||
uint8 originalWanderCount = screenObj->wander_count;
|
||||
|
||||
screenObj->wander_count--;
|
||||
if ((originalWanderCount == 0) || (screenObj->flags & fDidntMove)) {
|
||||
screenObj->direction = _rnd->getRandomNumber(8);
|
||||
|
||||
if (isEgoView(screenObj)) {
|
||||
setVar(VM_VAR_EGO_DIRECTION, screenObj->direction);
|
||||
}
|
||||
|
||||
while (screenObj->wander_count < 6) {
|
||||
screenObj->wander_count = _rnd->getRandomNumber(50); // huh?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::motionFollowEgo(ScreenObjEntry *screenObj) {
|
||||
ScreenObjEntry *screenObjEgo = &_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY];
|
||||
|
||||
int egoX = screenObjEgo->xPos + screenObjEgo->xSize / 2;
|
||||
int egoY = screenObjEgo->yPos;
|
||||
|
||||
int objX = screenObj->xPos + screenObj->xSize / 2;
|
||||
int objY = screenObj->yPos;
|
||||
|
||||
// Get direction to reach ego
|
||||
int dir = getDirection(objX, objY, egoX, egoY, screenObj->follow_stepSize);
|
||||
|
||||
// Already at ego coordinates
|
||||
if (dir == 0) {
|
||||
screenObj->direction = 0;
|
||||
screenObj->motionType = kMotionNormal;
|
||||
setFlag(screenObj->follow_flag, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenObj->follow_count == 0xff) {
|
||||
screenObj->follow_count = 0;
|
||||
} else if (screenObj->flags & fDidntMove) {
|
||||
int d;
|
||||
|
||||
while ((screenObj->direction = _rnd->getRandomNumber(8)) == 0) {
|
||||
}
|
||||
|
||||
d = (ABS(egoY - objY) + ABS(egoX - objX)) / 2;
|
||||
|
||||
if (d < screenObj->stepSize) {
|
||||
screenObj->follow_count = screenObj->stepSize;
|
||||
return;
|
||||
}
|
||||
|
||||
while ((screenObj->follow_count = _rnd->getRandomNumber(d)) < screenObj->stepSize) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (screenObj->follow_count != 0) {
|
||||
// DF: this is ugly and I dont know why this works, but
|
||||
// other line does not! (watcom complained about lvalue)
|
||||
//
|
||||
// if (((int8)v->parm3 -= v->step_size) < 0)
|
||||
// v->parm3 = 0;
|
||||
|
||||
int k = screenObj->follow_count;
|
||||
k -= screenObj->stepSize;
|
||||
screenObj->follow_count = k;
|
||||
|
||||
if ((int8) screenObj->follow_count < 0)
|
||||
screenObj->follow_count = 0;
|
||||
} else {
|
||||
screenObj->direction = dir;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::motionMoveObj(ScreenObjEntry *screenObj) {
|
||||
screenObj->direction = getDirection(screenObj->xPos, screenObj->yPos, screenObj->move_x, screenObj->move_y, screenObj->stepSize);
|
||||
|
||||
// Update V6 if ego
|
||||
if (isEgoView(screenObj))
|
||||
setVar(VM_VAR_EGO_DIRECTION, screenObj->direction);
|
||||
|
||||
if (screenObj->direction == 0)
|
||||
motionMoveObjStop(screenObj);
|
||||
}
|
||||
|
||||
void AgiEngine::checkMotion(ScreenObjEntry *screenObj) {
|
||||
switch (screenObj->motionType) {
|
||||
case kMotionNormal:
|
||||
break;
|
||||
case kMotionWander:
|
||||
motionWander(screenObj);
|
||||
break;
|
||||
case kMotionFollowEgo:
|
||||
motionFollowEgo(screenObj);
|
||||
break;
|
||||
case kMotionEgo:
|
||||
case kMotionMoveObj:
|
||||
motionMoveObj(screenObj);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if ((_game.block.active && (~screenObj->flags & fIgnoreBlocks)) && screenObj->direction)
|
||||
changePos(screenObj);
|
||||
}
|
||||
|
||||
/*
|
||||
* Public functions
|
||||
*/
|
||||
|
||||
void AgiEngine::checkAllMotions() {
|
||||
ScreenObjEntry *screenObj;
|
||||
|
||||
for (screenObj = _game.screenObjTable; screenObj < &_game.screenObjTable[SCREENOBJECTS_MAX]; screenObj++) {
|
||||
if ((screenObj->flags & (fAnimated | fUpdate | fDrawn)) == (fAnimated | fUpdate | fDrawn)
|
||||
&& screenObj->stepTimeCount == 1) {
|
||||
checkMotion(screenObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given entry is at destination point.
|
||||
* This function is used to updated the flags of an object with move.obj
|
||||
* type motion that * has reached its final destination coordinates.
|
||||
* @param v Pointer to view table entry
|
||||
*/
|
||||
void AgiEngine::inDestination(ScreenObjEntry *screenObj) {
|
||||
if (screenObj->motionType == kMotionMoveObj) {
|
||||
screenObj->stepSize = screenObj->move_stepSize;
|
||||
setFlag(screenObj->move_flag, true);
|
||||
}
|
||||
screenObj->motionType = kMotionNormal;
|
||||
if (isEgoView(screenObj))
|
||||
_game.playerControl = true;
|
||||
}
|
||||
|
||||
void AgiEngine::motionMoveObjStop(ScreenObjEntry *screenObj) {
|
||||
screenObj->stepSize = screenObj->move_stepSize;
|
||||
|
||||
// This check for motionType was only done in AGI3.
|
||||
// But we use this motion type for mouse movement, so we need to check in any case, otherwise it will cause glitches.
|
||||
if (screenObj->motionType != kMotionEgo) {
|
||||
setFlag(screenObj->move_flag, true);
|
||||
}
|
||||
|
||||
screenObj->motionType = kMotionNormal;
|
||||
if (isEgoView(screenObj)) {
|
||||
_game.playerControl = true;
|
||||
setVar(VM_VAR_EGO_DIRECTION, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for static function motion_moveobj().
|
||||
* This function is used by cmd_move_object() in the first motion cycle
|
||||
* after setting the motion mode to kMotionMoveObj.
|
||||
* @param v Pointer to view table entry
|
||||
*/
|
||||
void AgiEngine::moveObj(ScreenObjEntry *screenObj) {
|
||||
motionMoveObj(screenObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get direction from motion coordinates
|
||||
* This function gets the motion direction from the current and previous
|
||||
* object coordinates and the step size.
|
||||
* @param x0 Original x coordinate of the object
|
||||
* @param y0 Original y coordinate of the object
|
||||
* @param x x coordinate of the object
|
||||
* @param y y coordinate of the object
|
||||
* @param s step size
|
||||
*/
|
||||
int AgiEngine::getDirection(int16 objX, int16 objY, int16 destX, int16 destY, int16 stepSize) {
|
||||
const int dirTable[9] = { 8, 1, 2, 7, 0, 3, 6, 5, 4 };
|
||||
return dirTable[checkStep(destX - objX, stepSize) + 3 * checkStep(destY - objY, stepSize)];
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
205
engines/agi/mouse_cursor.h
Normal file
205
engines/agi/mouse_cursor.h
Normal file
@@ -0,0 +1,205 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_MOUSE_CURSOR_H
|
||||
#define AGI_MOUSE_CURSOR_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* RGB-palette for the Amiga-style arrow cursor
|
||||
* and the Amiga-style busy cursor.
|
||||
*/
|
||||
static const byte MOUSECURSOR_PALETTE[] = {
|
||||
0x00, 0x00, 0x00, // Black
|
||||
0xFF, 0xFF, 0xFF, // White
|
||||
0xDE, 0x20, 0x21, // Red
|
||||
0xFF, 0xCF, 0xAD // Light red
|
||||
};
|
||||
|
||||
/**
|
||||
* A black and white SCI-style arrow cursor (11x16).
|
||||
* 0 = Transparent.
|
||||
* 1 = Black (#000000 in 24-bit RGB).
|
||||
* 2 = White (#FFFFFF in 24-bit RGB).
|
||||
*/
|
||||
static const byte MOUSECURSOR_SCI[] = {
|
||||
1,1,0,0,0,0,0,0,0,0,0,
|
||||
1,2,1,0,0,0,0,0,0,0,0,
|
||||
1,2,2,1,0,0,0,0,0,0,0,
|
||||
1,2,2,2,1,0,0,0,0,0,0,
|
||||
1,2,2,2,2,1,0,0,0,0,0,
|
||||
1,2,2,2,2,2,1,0,0,0,0,
|
||||
1,2,2,2,2,2,2,1,0,0,0,
|
||||
1,2,2,2,2,2,2,2,1,0,0,
|
||||
1,2,2,2,2,2,2,2,2,1,0,
|
||||
1,2,2,2,2,2,2,2,2,2,1,
|
||||
1,2,2,2,2,2,1,0,0,0,0,
|
||||
1,2,1,0,1,2,2,1,0,0,0,
|
||||
1,1,0,0,1,2,2,1,0,0,0,
|
||||
0,0,0,0,0,1,2,2,1,0,0,
|
||||
0,0,0,0,0,1,2,2,1,0,0,
|
||||
0,0,0,0,0,0,1,2,2,1,0
|
||||
};
|
||||
|
||||
/**
|
||||
* A black and white SCI-style busy cursor (15x16).
|
||||
* 0 = Transparent.
|
||||
* 1 = Black (#000000 in 24-bit RGB).
|
||||
* 2 = White (#FFFFFF in 24-bit RGB).
|
||||
*/
|
||||
static const byte MOUSECURSOR_SCI_BUSY[] = {
|
||||
0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,
|
||||
0,0,0,0,1,1,1,2,2,1,1,1,0,0,0,
|
||||
0,0,0,1,2,2,1,2,2,1,2,2,1,0,0,
|
||||
0,1,1,1,2,2,1,2,2,1,2,2,1,0,0,
|
||||
1,2,2,1,2,2,1,2,2,1,2,2,1,0,0,
|
||||
1,2,2,1,2,2,1,2,2,1,2,2,1,0,0,
|
||||
1,2,2,1,2,2,1,2,2,1,2,1,2,1,0,
|
||||
1,2,2,1,2,2,1,2,2,1,1,2,2,1,1,
|
||||
1,2,2,1,2,2,1,2,2,1,1,2,2,2,1,
|
||||
1,2,2,2,2,2,2,2,2,1,1,2,2,2,1,
|
||||
1,2,2,2,2,2,2,2,1,2,2,2,2,1,0,
|
||||
1,2,2,2,2,2,2,1,2,2,2,2,2,1,0,
|
||||
0,1,2,2,2,2,2,1,2,2,2,2,1,0,0,
|
||||
0,1,2,2,2,2,2,2,2,2,2,2,1,0,0,
|
||||
0,0,1,2,2,2,2,2,2,2,2,1,0,0,0,
|
||||
0,0,0,1,1,1,1,1,1,1,1,0,0,0,0
|
||||
};
|
||||
|
||||
/**
|
||||
* A black and white Atari ST style arrow cursor (11x16).
|
||||
* 0 = Transparent.
|
||||
* 1 = Black (#000000 in 24-bit RGB).
|
||||
* 2 = White (#FFFFFF in 24-bit RGB).
|
||||
*/
|
||||
static const byte MOUSECURSOR_ATARI_ST[] = {
|
||||
2,2,0,0,0,0,0,0,0,0,0,
|
||||
2,1,2,0,0,0,0,0,0,0,0,
|
||||
2,1,1,2,0,0,0,0,0,0,0,
|
||||
2,1,1,1,2,0,0,0,0,0,0,
|
||||
2,1,1,1,1,2,0,0,0,0,0,
|
||||
2,1,1,1,1,1,2,0,0,0,0,
|
||||
2,1,1,1,1,1,1,2,0,0,0,
|
||||
2,1,1,1,1,1,1,1,2,0,0,
|
||||
2,1,1,1,1,1,1,1,1,2,0,
|
||||
2,1,1,1,1,1,2,2,2,2,2,
|
||||
2,1,1,2,1,1,2,0,0,0,0,
|
||||
2,1,2,0,2,1,1,2,0,0,0,
|
||||
2,2,0,0,2,1,1,2,0,0,0,
|
||||
2,0,0,0,0,2,1,1,2,0,0,
|
||||
0,0,0,0,0,2,1,1,2,0,0,
|
||||
0,0,0,0,0,0,2,2,2,0,0
|
||||
};
|
||||
|
||||
/**
|
||||
* A black and white Apple IIGS style arrow cursor (9x11).
|
||||
* 0 = Transparent.
|
||||
* 1 = Black (#000000 in 24-bit RGB).
|
||||
* 2 = White (#FFFFFF in 24-bit RGB).
|
||||
*/
|
||||
static const byte MOUSECURSOR_APPLE_II_GS[] = {
|
||||
2,2,0,0,0,0,0,0,0,
|
||||
2,1,2,0,0,0,0,0,0,
|
||||
2,1,1,2,0,0,0,0,0,
|
||||
2,1,1,1,2,0,0,0,0,
|
||||
2,1,1,1,1,2,0,0,0,
|
||||
2,1,1,1,1,1,2,0,0,
|
||||
2,1,1,1,1,1,1,2,0,
|
||||
2,1,1,1,1,1,1,1,2,
|
||||
2,1,1,2,1,1,2,2,0,
|
||||
2,2,2,0,2,1,1,2,0,
|
||||
0,0,0,0,0,2,2,2,0
|
||||
};
|
||||
|
||||
/**
|
||||
* An Amiga-style arrow cursor (8x11).
|
||||
* 0 = Transparent.
|
||||
* 1 = Black (#000000 in 24-bit RGB).
|
||||
* 3 = Red (#DE2021 in 24-bit RGB).
|
||||
* 4 = Light red (#FFCFAD in 24-bit RGB).
|
||||
*/
|
||||
static const byte MOUSECURSOR_AMIGA[] = {
|
||||
3,4,1,0,0,0,0,0,
|
||||
3,3,4,1,0,0,0,0,
|
||||
3,3,3,4,1,0,0,0,
|
||||
3,3,3,3,4,1,0,0,
|
||||
3,3,3,3,3,4,1,0,
|
||||
3,3,3,3,3,3,4,1,
|
||||
3,0,3,3,4,1,0,0,
|
||||
0,0,0,3,4,1,0,0,
|
||||
0,0,0,3,3,4,1,0,
|
||||
0,0,0,0,3,4,1,0,
|
||||
0,0,0,0,3,3,4,1
|
||||
};
|
||||
|
||||
/**
|
||||
* An Amiga-style busy cursor showing an hourglass (13x16).
|
||||
* 0 = Transparent.
|
||||
* 1 = Black (#000000 in 24-bit RGB).
|
||||
* 3 = Red (#DE2021 in 24-bit RGB).
|
||||
* 4 = Light red (#FFCFAD in 24-bit RGB).
|
||||
*/
|
||||
static const byte MOUSECURSOR_AMIGA_BUSY[] = {
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,3,3,3,3,3,3,3,3,3,3,3,1,
|
||||
1,3,3,3,3,3,3,3,3,3,3,3,1,
|
||||
0,1,4,4,4,4,4,4,4,4,4,1,0,
|
||||
0,0,1,4,4,4,4,4,4,4,1,0,0,
|
||||
0,0,0,1,4,4,4,4,4,1,0,0,0,
|
||||
0,0,0,0,1,4,4,4,1,0,0,0,0,
|
||||
0,0,0,0,0,1,4,1,0,0,0,0,0,
|
||||
0,0,0,0,0,1,4,1,0,0,0,0,0,
|
||||
0,0,0,0,1,3,4,3,1,0,0,0,0,
|
||||
0,0,0,1,3,3,4,3,3,1,0,0,0,
|
||||
0,0,1,3,3,3,4,3,3,3,1,0,0,
|
||||
0,1,3,3,3,4,4,4,3,3,3,1,0,
|
||||
1,4,4,4,4,4,4,4,4,4,4,4,1,
|
||||
1,4,4,4,4,4,4,4,4,4,4,4,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1
|
||||
};
|
||||
|
||||
/**
|
||||
* A Macintosh-style busy cursor showing an hourglass (10x14).
|
||||
* 0 = Transparent.
|
||||
* 1 = Black (#000000 in 24-bit RGB).
|
||||
* 2 = White (#FFFFFF in 24-bit RGB).
|
||||
*/
|
||||
static const byte MOUSECURSOR_MACINTOSH_BUSY[] = {
|
||||
0,0,1,1,1,1,1,1,0,0,
|
||||
0,0,1,1,1,1,1,1,0,0,
|
||||
0,0,1,1,1,1,1,1,0,0,
|
||||
0,1,2,2,2,2,2,2,1,0,
|
||||
1,2,2,2,2,1,2,2,2,1,
|
||||
1,2,2,2,2,1,2,2,2,1,
|
||||
1,2,2,2,2,1,2,2,2,1,
|
||||
1,2,2,1,1,1,2,2,2,1,
|
||||
1,2,2,2,2,2,2,2,2,1,
|
||||
1,2,2,2,2,2,2,2,2,1,
|
||||
0,1,2,2,2,2,2,2,1,0,
|
||||
0,0,1,1,1,1,1,1,0,0,
|
||||
0,0,1,1,1,1,1,1,0,0,
|
||||
0,0,1,1,1,1,1,1,0,0
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_MOUSE_CURSOR_H */
|
||||
136
engines/agi/objects.cpp
Normal file
136
engines/agi/objects.cpp
Normal file
@@ -0,0 +1,136 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
|
||||
#include "common/textconsole.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
int AgiEngine::decodeObjects(uint8 *mem, uint32 flen) {
|
||||
unsigned int i, so, padsize, spos;
|
||||
|
||||
padsize = getPlatform() == Common::kPlatformAmiga ? 4 : 3;
|
||||
|
||||
_game.numObjects = 0;
|
||||
|
||||
// check if first pointer exceeds file size
|
||||
// if so, its encrypted, else it is not
|
||||
|
||||
if (READ_LE_UINT16(mem) > flen) {
|
||||
debugN(0, "Decrypting objects... ");
|
||||
decrypt(mem, flen);
|
||||
debug(0, "done.");
|
||||
}
|
||||
|
||||
// alloc memory for object list
|
||||
// byte 3 = number of animated objects. this is ignored.. ??
|
||||
if (READ_LE_UINT16(mem) / padsize > 256) {
|
||||
// die with no error! AGDS game needs not to die to work!! :(
|
||||
return errOK;
|
||||
}
|
||||
|
||||
_game.numObjects = READ_LE_UINT16(mem) / padsize;
|
||||
debugC(5, kDebugLevelResources, "num_objects = %d (padsize = %d)", _game.numObjects, padsize);
|
||||
|
||||
_objects.resize(_game.numObjects);
|
||||
|
||||
// build the object list
|
||||
spos = getVersion() >= 0x2000 ? padsize : 0;
|
||||
for (i = 0, so = spos; i < _game.numObjects; i++, so += padsize) {
|
||||
int offset;
|
||||
|
||||
_objects[i].location = *(mem + so + 2);
|
||||
offset = READ_LE_UINT16(mem + so) + spos;
|
||||
|
||||
if ((uint) offset < flen) {
|
||||
_objects[i].name = (const char *)mem + offset;
|
||||
} else {
|
||||
warning("object %i name beyond object filesize (%04x > %04x)", i, offset, flen);
|
||||
_objects[i].name.clear();
|
||||
}
|
||||
|
||||
// Don't show the invalid "?" object in ego's inventory in the fanmade
|
||||
// game Beyond the Titanic 2 (bug #5523).
|
||||
if (_objects[i].name == "?" && _objects[i].location == EGO_OWNED)
|
||||
_objects[i].location = 0;
|
||||
}
|
||||
debug(0, "Reading objects: %d objects read.", _game.numObjects);
|
||||
|
||||
return errOK;
|
||||
}
|
||||
|
||||
int AgiEngine::loadObjects(const char *fname) {
|
||||
Common::File fp;
|
||||
|
||||
debugC(5, kDebugLevelResources, "(Loading objects '%s')", fname);
|
||||
|
||||
if (!fp.open(fname))
|
||||
return errBadFileOpen;
|
||||
|
||||
return loadObjects(fp, fp.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decode objects, and store them in the internal structure.
|
||||
*
|
||||
* @param fp File pointer
|
||||
* @param flen File length
|
||||
*/
|
||||
int AgiEngine::loadObjects(Common::SeekableReadStream &fp, int flen) {
|
||||
uint8 *mem;
|
||||
|
||||
if ((mem = (uint8 *)calloc(1, flen + 32)) == nullptr) {
|
||||
return errNotEnoughMemory;
|
||||
}
|
||||
|
||||
fp.read(mem, flen);
|
||||
|
||||
decodeObjects(mem, flen);
|
||||
free(mem);
|
||||
return errOK;
|
||||
}
|
||||
|
||||
void AgiEngine::objectSetLocation(uint16 objectNr, int location) {
|
||||
if (objectNr >= _game.numObjects) {
|
||||
warning("AgiEngine::objectSetLocation: Can't access object %d", objectNr);
|
||||
return;
|
||||
}
|
||||
_objects[objectNr].location = location;
|
||||
}
|
||||
|
||||
int AgiEngine::objectGetLocation(uint16 objectNr) {
|
||||
if (objectNr >= _game.numObjects) {
|
||||
warning("AgiEngine::objectGetLocation: Can't access object %d", objectNr);
|
||||
return 0;
|
||||
}
|
||||
return _objects[objectNr].location;
|
||||
}
|
||||
|
||||
const char *AgiEngine::objectName(uint16 objectNr) {
|
||||
if (objectNr >= _game.numObjects) {
|
||||
warning("AgiEngine::objectName: Can't access object %d", objectNr);
|
||||
return "";
|
||||
}
|
||||
return _objects[objectNr].name.c_str();
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
2538
engines/agi/op_cmd.cpp
Normal file
2538
engines/agi/op_cmd.cpp
Normal file
File diff suppressed because it is too large
Load Diff
105
engines/agi/op_dbg.cpp
Normal file
105
engines/agi/op_dbg.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/opcodes.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define ip (_game.logics[lognum].cIP)
|
||||
#define code (_game.logics[lognum].data)
|
||||
|
||||
const char *logicNamesIf[] = {
|
||||
"OR", "NOT", "ELSE", "IF"
|
||||
};
|
||||
|
||||
void AgiEngine::debugConsole(int lognum, int mode, const char *str) {
|
||||
AgiOpCodeEntry *curOpCodeTable;
|
||||
uint8 parametersLeft, z;
|
||||
uint8 logicNameIdx;
|
||||
const char *c;
|
||||
|
||||
if (str) {
|
||||
debug(0, " %s", str);
|
||||
return;
|
||||
}
|
||||
|
||||
debugN(0, "%03d:%04x ", lognum, ip);
|
||||
|
||||
switch (*(code + ip)) {
|
||||
case 0xFC:
|
||||
case 0xFD:
|
||||
case 0xFE:
|
||||
case 0xFF:
|
||||
if (_debug.opcodes) {
|
||||
debugN(0, "%02X %02X %02X %02X %02X %02X %02X %02X %02X\n"
|
||||
" ",
|
||||
(uint8) * (code + (0 + ip)) & 0xFF,
|
||||
(uint8) * (code + (1 + ip)) & 0xFF,
|
||||
(uint8) * (code + (2 + ip)) & 0xFF,
|
||||
(uint8) * (code + (3 + ip)) & 0xFF,
|
||||
(uint8) * (code + (4 + ip)) & 0xFF,
|
||||
(uint8) * (code + (5 + ip)) & 0xFF,
|
||||
(uint8) * (code + (6 + ip)) & 0xFF,
|
||||
(uint8) * (code + (7 + ip)) & 0xFF,
|
||||
(uint8) * (code + (8 + ip)) & 0xFF);
|
||||
}
|
||||
logicNameIdx = (*(code + ip)) - 0xFC;
|
||||
debugN(0, "%s ", logicNamesIf[logicNameIdx]);
|
||||
break;
|
||||
default:
|
||||
curOpCodeTable = mode == lCOMMAND_MODE ? _opCodes : _opCodesCond;
|
||||
parametersLeft = curOpCodeTable[*(code + ip)].parameterSize;
|
||||
c = curOpCodeTable[*(code + ip)].parameters;
|
||||
|
||||
if (_debug.opcodes) {
|
||||
debugN(0, "%02X %02X %02X %02X %02X %02X %02X %02X %02X\n"
|
||||
" ",
|
||||
(uint8) * (code + (0 + ip)) & 0xFF,
|
||||
(uint8) * (code + (1 + ip)) & 0xFF,
|
||||
(uint8) * (code + (2 + ip)) & 0xFF,
|
||||
(uint8) * (code + (3 + ip)) & 0xFF,
|
||||
(uint8) * (code + (4 + ip)) & 0xFF,
|
||||
(uint8) * (code + (5 + ip)) & 0xFF,
|
||||
(uint8) * (code + (6 + ip)) & 0xFF,
|
||||
(uint8) * (code + (7 + ip)) & 0xFF,
|
||||
(uint8) * (code + (8 + ip)) & 0xFF);
|
||||
}
|
||||
debugN(0, "%s ", (curOpCodeTable + * (code + ip))->name);
|
||||
|
||||
for (z = 1; parametersLeft > 0;) {
|
||||
if (*c == 'n') {
|
||||
debugN(0, "%d", *(code + (ip + z)));
|
||||
} else {
|
||||
debugN(0, "v%d[%d]", *(code + (ip + z)), getVar(*(code + (ip + z))));
|
||||
}
|
||||
c++;
|
||||
z++;
|
||||
if (--parametersLeft > 0)
|
||||
debugN(0, ",");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
debugN(0, "\n");
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
510
engines/agi/op_test.cpp
Normal file
510
engines/agi/op_test.cpp
Normal file
@@ -0,0 +1,510 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/opcodes.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "common/endian.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define ip (state->_curLogic->cIP)
|
||||
#define code (state->_curLogic->data)
|
||||
|
||||
void condEqual(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr1 = p[0];
|
||||
uint16 varVal1 = vm->getVar(varNr1);
|
||||
uint16 value2 = p[1];
|
||||
state->testResult = (varVal1 == value2);
|
||||
}
|
||||
|
||||
void condEqualV(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr1 = p[0];
|
||||
uint16 varNr2 = p[1];
|
||||
uint16 varVal1 = vm->getVar(varNr1);
|
||||
uint16 varVal2 = vm->getVar(varNr2);
|
||||
state->testResult = (varVal1 == varVal2);
|
||||
}
|
||||
|
||||
void condLess(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr1 = p[0];
|
||||
uint16 varVal1 = vm->getVar(varNr1);
|
||||
uint16 value2 = p[1];
|
||||
state->testResult = (varVal1 < value2);
|
||||
}
|
||||
|
||||
void condLessV(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr1 = p[0];
|
||||
uint16 varNr2 = p[1];
|
||||
uint16 varVal1 = vm->getVar(varNr1);
|
||||
uint16 varVal2 = vm->getVar(varNr2);
|
||||
state->testResult = (varVal1 < varVal2);
|
||||
}
|
||||
|
||||
void condGreater(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr1 = p[0];
|
||||
uint16 varVal1 = vm->getVar(varNr1);
|
||||
uint16 value2 = p[1];
|
||||
state->testResult = (varVal1 > value2);
|
||||
}
|
||||
|
||||
void condGreaterV(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr1 = p[0];
|
||||
uint16 varNr2 = p[1];
|
||||
uint16 varVal1 = vm->getVar(varNr1);
|
||||
uint16 varVal2 = vm->getVar(varNr2);
|
||||
state->testResult = (varVal1 > varVal2);
|
||||
}
|
||||
|
||||
void condIsSet(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = vm->getFlag(p[0]);
|
||||
}
|
||||
|
||||
void condIsSetV(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr = p[0];
|
||||
uint16 varVal = vm->getVar(varNr);
|
||||
state->testResult = vm->getFlag(varVal);
|
||||
}
|
||||
|
||||
void condIsSetV1(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 varNr = p[0];
|
||||
uint16 varVal = vm->getVar(varNr);
|
||||
state->testResult = (varVal > 0);
|
||||
}
|
||||
|
||||
void condHas(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 objectNr = p[0];
|
||||
state->testResult = (vm->objectGetLocation(objectNr) == EGO_OWNED);
|
||||
}
|
||||
|
||||
void condHasV1(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 objectNr = p[0];
|
||||
state->testResult = (vm->objectGetLocation(objectNr) == EGO_OWNED_V1);
|
||||
}
|
||||
|
||||
void condObjInRoom(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 objectNr = p[0];
|
||||
uint16 varNr = p[1];
|
||||
uint16 varVal = vm->getVar(varNr);
|
||||
state->testResult = (vm->objectGetLocation(objectNr) == varVal);
|
||||
}
|
||||
|
||||
void condPosn(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = vm->testPosn(p[0], p[1], p[2], p[3], p[4]);
|
||||
}
|
||||
|
||||
void condController(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = vm->testController(p[0]);
|
||||
}
|
||||
|
||||
void condHaveKey(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
// Only check for key when there is not already one set by scripts
|
||||
if (vm->getVar(VM_VAR_KEY)) {
|
||||
state->testResult = true;
|
||||
#ifdef USE_TTS
|
||||
vm->stopTextToSpeech(false);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
// we are not really an inner loop, but we stop processAGIEvents() from doing regular cycle work by setting it up
|
||||
vm->cycleInnerLoopActive(CYCLE_INNERLOOP_HAVEKEY);
|
||||
uint16 key = vm->processAGIEvents();
|
||||
vm->cycleInnerLoopInactive();
|
||||
if (key) {
|
||||
debugC(5, kDebugLevelInput, "keypress = %02x", key);
|
||||
vm->setVar(VM_VAR_KEY, key);
|
||||
state->testResult = true;
|
||||
#ifdef USE_TTS
|
||||
vm->stopTextToSpeech(false);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
state->testResult = false;
|
||||
}
|
||||
|
||||
void condSaid(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = vm->testSaid(p[0], p + 1);
|
||||
}
|
||||
|
||||
void condSaid1(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = false;
|
||||
|
||||
if (!vm->getFlag(VM_FLAG_ENTERED_CLI))
|
||||
return;
|
||||
|
||||
int id0 = READ_LE_UINT16(p);
|
||||
|
||||
if ((id0 == 1 || id0 == vm->_words->getEgoWordId(0)))
|
||||
state->testResult = true;
|
||||
}
|
||||
|
||||
void condSaid2(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = false;
|
||||
|
||||
if (!vm->getFlag(VM_FLAG_ENTERED_CLI))
|
||||
return;
|
||||
|
||||
int id0 = READ_LE_UINT16(p);
|
||||
int id1 = READ_LE_UINT16(p + 2);
|
||||
|
||||
if ((id0 == 1 || id0 == vm->_words->getEgoWordId(0)) &&
|
||||
(id1 == 1 || id1 == vm->_words->getEgoWordId(1)))
|
||||
state->testResult = true;
|
||||
}
|
||||
|
||||
void condSaid3(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = false;
|
||||
|
||||
if (!vm->getFlag(VM_FLAG_ENTERED_CLI))
|
||||
return;
|
||||
|
||||
int id0 = READ_LE_UINT16(p);
|
||||
int id1 = READ_LE_UINT16(p + 2);
|
||||
int id2 = READ_LE_UINT16(p + 4);
|
||||
|
||||
if ((id0 == 1 || id0 == vm->_words->getEgoWordId(0)) &&
|
||||
(id1 == 1 || id1 == vm->_words->getEgoWordId(1)) &&
|
||||
(id2 == 1 || id2 == vm->_words->getEgoWordId(2)))
|
||||
state->testResult = true;
|
||||
}
|
||||
|
||||
void condBit(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
uint16 value1 = p[0];
|
||||
uint16 varNr2 = p[1];
|
||||
uint16 varVal2 = vm->getVar(varNr2);
|
||||
state->testResult = (((varVal2 >> value1) & 1) == 1);
|
||||
}
|
||||
|
||||
void condCompareStrings(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
debugC(7, kDebugLevelScripts, "comparing [%s], [%s]", state->getString(p[0]), state->getString(p[1]));
|
||||
state->testResult = vm->testCompareStrings(p[0], p[1]);
|
||||
}
|
||||
|
||||
void condObjInBox(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = vm->testObjInBox(p[0], p[1], p[2], p[3], p[4]);
|
||||
}
|
||||
|
||||
void condCenterPosn(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = vm->testObjCenter(p[0], p[1], p[2], p[3], p[4]);
|
||||
}
|
||||
|
||||
void condRightPosn(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
state->testResult = vm->testObjRight(p[0], p[1], p[2], p[3], p[4]);
|
||||
}
|
||||
|
||||
void condUnknown13(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
// My current theory is that this command checks whether the ego is currently moving
|
||||
// and that that movement has been caused using the mouse and not using the keyboard.
|
||||
// I base this theory on the game's behavior on an Amiga emulator, not on disassembly.
|
||||
// This command is used at least in the Amiga version of Gold Rush! v2.05 1989-03-09
|
||||
// (AGI 2.316) in logics 1, 3, 5, 6, 137 and 192 (Logic.192 revealed this command's nature).
|
||||
// TODO: Check this command's implementation using disassembly just to be sure.
|
||||
bool r = ((state->screenObjTable[SCREENOBJECTS_EGO_ENTRY].flags & fAdjEgoXY) == fAdjEgoXY);
|
||||
debugC(7, kDebugLevelScripts, "op_test: in.motion.using.mouse = %s (Amiga-specific testcase 19)", r ? "true" : "false");
|
||||
state->testResult = r;
|
||||
}
|
||||
|
||||
void condUnknown(AgiGame *state, AgiEngine *vm, uint8 *p) {
|
||||
warning("Skipping unknown test command %2X", *(code + ip - 1));
|
||||
state->testResult = false;
|
||||
}
|
||||
|
||||
bool AgiEngine::testCompareStrings(uint8 s1, uint8 s2) {
|
||||
char ms1[MAX_STRINGLEN];
|
||||
char ms2[MAX_STRINGLEN];
|
||||
int j, k, l;
|
||||
|
||||
Common::strlcpy(ms1, _game.getString(s1), MAX_STRINGLEN);
|
||||
Common::strlcpy(ms2, _game.getString(s2), MAX_STRINGLEN);
|
||||
|
||||
l = strlen(ms1);
|
||||
for (k = 0, j = 0; k < l; k++) {
|
||||
switch (ms1[k]) {
|
||||
case 0x20:
|
||||
case 0x09:
|
||||
case '-':
|
||||
case '.':
|
||||
case ',':
|
||||
case ':':
|
||||
case ';':
|
||||
case '!':
|
||||
case '\'':
|
||||
break;
|
||||
|
||||
default:
|
||||
ms1[j++] = tolower(ms1[k]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ms1[j] = 0x0;
|
||||
|
||||
l = strlen(ms2);
|
||||
for (k = 0, j = 0; k < l; k++) {
|
||||
switch (ms2[k]) {
|
||||
case 0x20:
|
||||
case 0x09:
|
||||
case '-':
|
||||
case '.':
|
||||
case ',':
|
||||
case ':':
|
||||
case ';':
|
||||
case '!':
|
||||
case '\'':
|
||||
break;
|
||||
|
||||
default:
|
||||
ms2[j++] = tolower(ms2[k]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ms2[j] = 0x0;
|
||||
|
||||
return !strcmp(ms1, ms2);
|
||||
}
|
||||
|
||||
bool AgiEngine::testController(uint8 cont) {
|
||||
#ifdef USE_TTS
|
||||
if (_game.controllerOccurred[cont]) {
|
||||
stopTextToSpeech(false);
|
||||
}
|
||||
#endif
|
||||
return _game.controllerOccurred[cont];
|
||||
}
|
||||
|
||||
bool AgiEngine::testPosn(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2) {
|
||||
ScreenObjEntry *v = &_game.screenObjTable[n];
|
||||
|
||||
bool r = v->xPos >= x1 && v->yPos >= y1 && v->xPos <= x2 && v->yPos <= y2;
|
||||
|
||||
debugC(7, kDebugLevelScripts, "(%d,%d) in (%d,%d,%d,%d): %s", v->xPos, v->yPos, x1, y1, x2, y2, r ? "true" : "false");
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
bool AgiEngine::testObjInBox(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2) {
|
||||
ScreenObjEntry *v = &_game.screenObjTable[n];
|
||||
|
||||
return v->xPos >= x1 &&
|
||||
v->yPos >= y1 && v->xPos + v->xSize - 1 <= x2 && v->yPos <= y2;
|
||||
}
|
||||
|
||||
// if n is in center of box
|
||||
bool AgiEngine::testObjCenter(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2) {
|
||||
ScreenObjEntry *v = &_game.screenObjTable[n];
|
||||
|
||||
return v->xPos + v->xSize / 2 >= x1 &&
|
||||
v->xPos + v->xSize / 2 <= x2 && v->yPos >= y1 && v->yPos <= y2;
|
||||
}
|
||||
|
||||
// if nect N is in right corner
|
||||
bool AgiEngine::testObjRight(uint8 n, uint8 x1, uint8 y1, uint8 x2, uint8 y2) {
|
||||
ScreenObjEntry *v = &_game.screenObjTable[n];
|
||||
|
||||
return v->xPos + v->xSize - 1 >= x1 &&
|
||||
v->xPos + v->xSize - 1 <= x2 && v->yPos >= y1 && v->yPos <= y2;
|
||||
}
|
||||
|
||||
// When player has entered something, it is parsed elsewhere
|
||||
bool AgiEngine::testSaid(uint8 nwords, uint8 *cc) {
|
||||
AgiGame *state = &_game;
|
||||
AgiEngine *vm = state->_vm;
|
||||
Words *words = vm->_words;
|
||||
int n = words->getEgoWordCount();
|
||||
int z = 0;
|
||||
|
||||
if (vm->getFlag(VM_FLAG_SAID_ACCEPTED_INPUT) || !vm->getFlag(VM_FLAG_ENTERED_CLI))
|
||||
return false;
|
||||
|
||||
// FR:
|
||||
// I think the reason for the code below is to add some speed....
|
||||
//
|
||||
// if (nwords != num_ego_words)
|
||||
// return false;
|
||||
//
|
||||
// In the disco scene in Larry 1 when you type "examine blonde",
|
||||
// inside the logic is expected ( said("examine", "blonde", "rol") )
|
||||
// where word("rol") = 9999
|
||||
//
|
||||
// According to the interpreter code 9999 means that whatever the
|
||||
// user typed should be correct, but it looks like code 9999 means that
|
||||
// if the string is empty at this point, the entry is also correct...
|
||||
//
|
||||
// With the removal of this code, the behavior of the scene was
|
||||
// corrected
|
||||
|
||||
for (int c = 0; nwords && n; c++, nwords--, n--) {
|
||||
z = READ_LE_UINT16(cc);
|
||||
cc += 2;
|
||||
|
||||
switch (z) {
|
||||
case 9999: // rest of line (empty string counts to...)
|
||||
nwords = 1;
|
||||
break;
|
||||
case 1: // any word
|
||||
break;
|
||||
default:
|
||||
if (words->getEgoWordId(c) != z)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The entry string should be entirely parsed, or last word = 9999
|
||||
if (n && z != 9999)
|
||||
return false;
|
||||
|
||||
// The interpreter string shouldn't be entirely parsed, but next
|
||||
// word must be 9999.
|
||||
if (nwords != 0 && READ_LE_UINT16(cc) != 9999)
|
||||
return false;
|
||||
|
||||
setFlag(VM_FLAG_SAID_ACCEPTED_INPUT, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AgiEngine::testIfCode(int16 logicNr) {
|
||||
AgiGame *state = &_game;
|
||||
|
||||
bool notMode = false;
|
||||
bool orMode = false;
|
||||
bool endTest = false;
|
||||
bool result = true;
|
||||
|
||||
while (!(shouldQuit() || _restartGame) && !endTest) {
|
||||
if (_debug.enabled && (_debug.logic0 || logicNr))
|
||||
debugConsole(logicNr, lTEST_MODE, nullptr);
|
||||
|
||||
uint8 op = *(code + ip++);
|
||||
switch (op) {
|
||||
case 0xFC:
|
||||
if (orMode) {
|
||||
// We have reached the end of an OR expression without
|
||||
// a single test command evaluating as true. Thus the OR
|
||||
// expression evaluates as false which means the whole
|
||||
// expression evaluates as false. So skip until the
|
||||
// ending 0xFF and return.
|
||||
skipInstructionsUntil(0xFF);
|
||||
result = false;
|
||||
endTest = true;
|
||||
} else {
|
||||
orMode = true;
|
||||
}
|
||||
continue;
|
||||
case 0xFD:
|
||||
notMode = true;
|
||||
continue;
|
||||
case 0x00:
|
||||
case 0xFF:
|
||||
endTest = true;
|
||||
continue;
|
||||
|
||||
default: {
|
||||
// Evaluate the command and skip the rest of the instruction
|
||||
uint8 p[16];
|
||||
memmove(p, (code + ip), 16);
|
||||
_opCodesCond[op].functionPtr(state, this, p);
|
||||
if (state->exitAllLogics) {
|
||||
// required even here, because of at least the timer heuristic
|
||||
// which when triggered waits a bit and processes ScummVM events and user may therefore restore a saved game
|
||||
// fixes bug #9707
|
||||
// TODO: maybe delay restoring the game instead, when GMM is used?
|
||||
return true;
|
||||
}
|
||||
skipInstruction(op);
|
||||
|
||||
// NOT mode is enabled only for one instruction
|
||||
if (notMode)
|
||||
state->testResult = !state->testResult;
|
||||
notMode = false;
|
||||
|
||||
if (orMode) {
|
||||
if (state->testResult) {
|
||||
// We are in OR mode and the last test command evaluated
|
||||
// as true, thus the whole OR expression evaluates as
|
||||
// true. So skip the rest of the OR expression and
|
||||
// continue normally.
|
||||
skipInstructionsUntil(0xFC);
|
||||
orMode = false;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
result = (result && state->testResult);
|
||||
if (!result) {
|
||||
// Since we are in AND mode and the last test command
|
||||
// evaluated as false, the whole expression also evaluates
|
||||
// as false. So skip until the ending 0xFF and return.
|
||||
skipInstructionsUntil(0xFF);
|
||||
endTest = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the following IF block if the condition evaluates as false
|
||||
if (result)
|
||||
ip += 2;
|
||||
else
|
||||
ip += READ_LE_UINT16(code + ip) + 2;
|
||||
|
||||
if (_debug.enabled && (_debug.logic0 || logicNr))
|
||||
debugConsole(logicNr, 0xFF, result ? "=true" : "=false");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void AgiEngine::skipInstruction(byte op) {
|
||||
AgiGame *state = &_game;
|
||||
if (op >= 0xFC)
|
||||
return;
|
||||
if (op == 0x0E && state->_vm->getVersion() >= 0x2000) // said
|
||||
ip += *(code + ip) * 2 + 1;
|
||||
else {
|
||||
ip += _opCodesCond[op].parameterSize;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::skipInstructionsUntil(byte v) {
|
||||
AgiGame *state = &_game;
|
||||
int originalIP = state->_curLogic->cIP;
|
||||
|
||||
while (1) {
|
||||
byte op = *(code + ip++);
|
||||
if (op == v)
|
||||
return;
|
||||
|
||||
if (op < 0xFC) {
|
||||
if (!_opCodesCond[op].functionPtr) {
|
||||
// security-check
|
||||
error("illegal opcode %x during skipinstructions in script %d at %d (triggered at %d)", op, state->curLogicNr, ip, originalIP);
|
||||
}
|
||||
}
|
||||
skipInstruction(op);
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
531
engines/agi/opcodes.cpp
Normal file
531
engines/agi/opcodes.cpp
Normal file
@@ -0,0 +1,531 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/opcodes.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// FIXME: The parameter strings in the opcode table have mistakes.
|
||||
// Nothing depends on the values of the individual characters.
|
||||
// Some are out of sync with how the opcode function interprets
|
||||
// the parameter. Only the string lengths are used to indicate
|
||||
// the parameter count for parsing.
|
||||
// Consult the opcode functions for the real parameter types.
|
||||
|
||||
static const AgiOpCodeDefinitionEntry opCodesV1Cond[] = {
|
||||
{ "", "", &condUnknown }, // 00
|
||||
{ "equaln", "vn", &condEqual }, // 01
|
||||
{ "equalv", "vv", &condEqualV }, // 02
|
||||
{ "lessn", "vn", &condLess }, // 03
|
||||
{ "lessv", "vv", &condLessV }, // 04
|
||||
{ "greatern", "vn", &condGreater }, // 05
|
||||
{ "greaterv", "vv", &condGreaterV }, // 06
|
||||
{ "isset", "v", &condIsSetV1 }, // 07
|
||||
{ "has", "n", &condHasV1 }, // 08
|
||||
{ "said", "nnnn", &condSaid2 }, // 09
|
||||
{ "posn", "nnnnn", &condPosn }, // 0A
|
||||
{ "controller", "n", &condController }, // 0B
|
||||
{ "obj.in.room", "nv", &condObjInRoom }, // 0C
|
||||
{ "said", "nnnnnn", &condSaid3 }, // 0D
|
||||
{ "have.key", "", &condHaveKey }, // 0E
|
||||
{ "said", "nn", &condSaid1 }, // 0F
|
||||
{ "bit", "nv", &condBit }, // 10
|
||||
};
|
||||
|
||||
static const AgiOpCodeDefinitionEntry opCodesV1[] = {
|
||||
{ "return", "", nullptr }, // 00
|
||||
{ "increment", "v", &cmdIncrement }, // 01
|
||||
{ "decrement", "v", &cmdDecrement }, // 02
|
||||
{ "assignn", "vn", &cmdAssignN }, // 03
|
||||
{ "assignv", "vv", &cmdAssignV }, // 04
|
||||
{ "addn", "vn", &cmdAddN }, // 05
|
||||
{ "addv", "vv", &cmdAddV }, // 06
|
||||
{ "subn", "vn", &cmdSubN }, // 07
|
||||
{ "subv", "vv", &cmdSubV }, // 08
|
||||
{ "load.view", "n", &cmdLoadView }, // 09
|
||||
{ "animate.obj", "n", &cmdAnimateObj }, // 0A
|
||||
{ "new.room", "n", &cmdNewRoom }, // 0B TODO
|
||||
{ "draw.pic", "v", &cmdDrawPicV1 }, // 0C
|
||||
{ "print", "s", &cmdPrint }, // 0D TODO
|
||||
{ "status", "", &cmdStatus }, // 0E TODO
|
||||
{ "save.game", "", &cmdSaveGame }, // 0F TODO
|
||||
{ "restore.game", "", &cmdLoadGame }, // 10 TODO
|
||||
{ "init.disk", "", &cmdInitDisk }, // 11 TODO
|
||||
{ "restart.game", "", &cmdRestartGame }, // 12 TODO
|
||||
{ "random", "v", &cmdRandomV1 }, // 13
|
||||
{ "get", "n", &cmdGetV1 }, // 14
|
||||
{ "drop", "n", &cmdDrop }, // 15
|
||||
{ "draw", "n", &cmdDraw }, // 16 TODO
|
||||
{ "erase", "n", &cmdErase }, // 17 TODO
|
||||
{ "position", "nnn", &cmdPositionV1 }, // 18
|
||||
{ "position.v", "nvv", &cmdPositionFV1 }, // 19
|
||||
{ "get.posn", "nvv", &cmdGetPosn }, // 1A
|
||||
{ "set.cel", "nn", &cmdSetCel }, // 1B
|
||||
{ "set.loop", "nn", &cmdSetLoop }, // 1C
|
||||
{ "end.of.loop", "nn", &cmdEndOfLoopV1 }, // 1D
|
||||
{ "reverse.loop", "nn", &cmdReverseLoopV1 }, // 1E
|
||||
{ "move.obj", "nnnnn", &cmdMoveObj }, // 1F
|
||||
{ "set.view", "nn", &cmdSetView }, // 20
|
||||
{ "follow.ego", "nnv", &cmdFollowEgo }, // 21
|
||||
{ "block", "nnnn", &cmdBlock }, // 22
|
||||
{ "unblock", "", &cmdUnblock }, // 23
|
||||
{ "ignore.blocks", "n", &cmdIgnoreBlocks }, // 24
|
||||
{ "observe.blocks", "n", &cmdObserveBlocks }, // 25
|
||||
{ "wander", "n", &cmdWander }, // 26
|
||||
{ "reposition", "nvv", &cmdRepositionV1 }, // 27
|
||||
{ "stop.motion", "n", &cmdStopMotionV1 }, // 28
|
||||
{ "start.motion", "n", &cmdStartMotionV1 }, // 29
|
||||
{ "stop.cycling", "n", &cmdStopCycling }, // 2A
|
||||
{ "start.cycling", "n", &cmdStartCycling }, // 2B
|
||||
{ "stop.update", "n", &cmdStopUpdate }, // 2C
|
||||
{ "start.update", "n", &cmdStartUpdate }, // 2D
|
||||
{ "program.control", "", &cmdProgramControl }, // 2E
|
||||
{ "player.control", "", &cmdPlayerControl }, // 2F
|
||||
{ "set.priority", "nn", &cmdSetPriority }, // 30
|
||||
{ "release.priority", "n", &cmdReleasePriority }, // 31
|
||||
{ "add.to.pic", "nnnnnn", &cmdAddToPicV1 }, // 32
|
||||
{ "set.horizon", "n", &cmdSetHorizon }, // 33
|
||||
{ "ignore.horizon", "n", &cmdIgnoreHorizon }, // 34
|
||||
{ "observe.horizon", "n", &cmdObserveHorizon }, // 35
|
||||
{ "load.logics", "n", &cmdLoadLogic }, // 36 TODO
|
||||
{ "object.on.water", "n", &cmdObjectOnWater }, // 37
|
||||
{ "load.pic", "v", &cmdLoadPicV1 }, // 38
|
||||
{ "load.sound", "n", &cmdLoadSound }, // 39
|
||||
{ "sound", "nn", &cmdSound }, // 3A
|
||||
{ "stop.sound", "", &cmdStopSound }, // 3B
|
||||
{ "set.v", "v", &cmdSetV }, // 3C
|
||||
{ "reset.v", "v", &cmdResetV }, // 3D
|
||||
{ "toggle.v", "v", &cmdToggleV }, // 3E
|
||||
{ "new.room.v", "v", &cmdNewRoom }, // 3F
|
||||
{ "call", "n", &cmdCallV1 }, // 40 TODO
|
||||
{ "quit", "", &cmdQuitV1 }, // 41
|
||||
{ "set.speed", "v", &cmdSetSpeed }, // 42
|
||||
{ "move.obj.v", "nvvvv", &cmdMoveObjF }, // 43
|
||||
{ "get.num", "nv", &cmdUnknown }, // 44
|
||||
{ "get.v", "v", &cmdUnknown }, // 45
|
||||
{ "assign.v", "vv", &cmdUnknown }, // 46
|
||||
{ "printvar", "v", &cmdUnknown }, // 47 prints a variable (debug script)
|
||||
{ "get.priority", "nv", &cmdGetPriority }, // 48
|
||||
{ "ignore.objs", "n", &cmdIgnoreObjs }, // 49
|
||||
{ "observe.objs", "n", &cmdObserveObjs }, // 4A
|
||||
{ "distance", "nnv", &cmdDistance }, // 4B
|
||||
{ "object.on.land", "n", &cmdObjectOnLand }, // 4C
|
||||
{ "set.priority.v", "nv", &cmdUnknown }, // 4D # set.priority.v / set.priority.f
|
||||
{ "show.obj", "n", &cmdShowObj }, // 4E # show.obj (KQ2)
|
||||
{ "load.logics", "n", &cmdLoadLogic }, // 4F # load.global.logics
|
||||
{ "display", "nnns", &cmdDisplay }, // 50 TODO: 4 vs 3 args
|
||||
{ "prevent.input", "", &cmdUnknown }, // 51 TODO: disables input by clearing a global, reset on new.room
|
||||
{ "...", "", &cmdUnknown }, // 52 nop, 0 args
|
||||
{ "text.screen", "n", &cmdUnknown }, // 53
|
||||
{ "graphics", "", &cmdUnknown }, // 54
|
||||
{ "stop.motion", "", &cmdStopMotion }, // 55
|
||||
{ "discard.view", "n", &cmdDiscardView }, // 56
|
||||
{ "discard.pic", "v", &cmdDiscardPic }, // 57
|
||||
{ "set.item.view", "nn", &cmdSetItemView }, // 58
|
||||
{ "reverse.cycle", "n", &cmdReverseCycle }, // 59
|
||||
{ "last.cel", "nv", &cmdLastCel }, // 5A
|
||||
{ "set.cel.v", "nv", &cmdSetCelF }, // 5B
|
||||
{ "normal.cycle", "n", &cmdNormalCycle }, // 5C
|
||||
{ "load.view", "n", &cmdLoadView }, // 5D duplicate opcode: same table entry as 09
|
||||
{ "...", "n", &cmdUnknown }, // 5E nop, 1 arg
|
||||
{ "near.water", "nv", &cmdNearWater }, // 5F BC script 102 when attempting to fill flask
|
||||
{ "set.bit", "nv", &cmdSetBit }, // 60
|
||||
{ "clear.bit", "nv", &cmdClearBit }, // 61
|
||||
{ "set.upper.left", "nn", &cmdSetUpperLeft } // 62 BC Apple II
|
||||
};
|
||||
|
||||
static const AgiOpCodeDefinitionEntry opCodesV2Cond[] = {
|
||||
{ "", "", &condUnknown }, // 00
|
||||
{ "equaln", "vn", &condEqual }, // 01
|
||||
{ "equalv", "vv", &condEqualV }, // 02
|
||||
{ "lessn", "vn", &condLess }, // 03
|
||||
{ "lessv", "vv", &condLessV }, // 04
|
||||
{ "greatern", "vn", &condGreater }, // 05
|
||||
{ "greaterv", "vv", &condGreaterV }, // 06
|
||||
{ "isset", "n", &condIsSet }, // 07
|
||||
{ "issetv", "v", &condIsSetV }, // 08
|
||||
{ "has", "n", &condHas }, // 09
|
||||
{ "obj.in.room", "nv", &condObjInRoom}, // 0A
|
||||
{ "posn", "nnnnn", &condPosn }, // 0B
|
||||
{ "controller", "n", &condController }, // 0C
|
||||
{ "have.key", "", &condHaveKey}, // 0D
|
||||
{ "said", "", &condSaid }, // 0E
|
||||
{ "compare.strings", "ss", &condCompareStrings }, // 0F
|
||||
{ "obj.in.box", "nnnnn", &condObjInBox }, // 10
|
||||
{ "center.posn", "nnnnn", &condCenterPosn }, // 11
|
||||
{ "right.posn", "nnnnn", &condRightPosn }, // 12
|
||||
{ "in.motion.using.mouse", "", &condUnknown13 } // 13
|
||||
};
|
||||
|
||||
static const AgiOpCodeDefinitionEntry opCodesV2[] = {
|
||||
{ "return", "", nullptr }, // 00
|
||||
{ "increment", "v", &cmdIncrement }, // 01
|
||||
{ "decrement", "v", &cmdDecrement }, // 02
|
||||
{ "assignn", "vn", &cmdAssignN }, // 03
|
||||
{ "assignv", "vv", &cmdAssignV }, // 04
|
||||
{ "addn", "vn", &cmdAddN }, // 05
|
||||
{ "addv", "vv", &cmdAddV }, // 06
|
||||
{ "subn", "vn", &cmdSubN }, // 07
|
||||
{ "subv", "vv", &cmdSubV }, // 08
|
||||
{ "lindirectv", "vv", &cmdLindirectV }, // 09
|
||||
{ "lindirect", "vv", &cmdRindirect }, // 0A
|
||||
{ "lindirectn", "vn", &cmdLindirectN }, // 0B
|
||||
{ "set", "n", &cmdSet }, // 0C
|
||||
{ "reset", "n", &cmdReset }, // 0D
|
||||
{ "toggle", "n", &cmdToggle }, // 0E
|
||||
{ "set.v", "v", &cmdSetV }, // 0F
|
||||
{ "reset.v", "v", &cmdResetV }, // 10
|
||||
{ "toggle.v", "v", &cmdToggleV }, // 11
|
||||
{ "new.room", "n", &cmdNewRoom }, // 12
|
||||
{ "new.room.v", "v", &cmdNewRoomF }, // 13
|
||||
{ "load.logics", "n", &cmdLoadLogic }, // 14
|
||||
{ "load.logics.v", "v", &cmdLoadLogicF }, // 15
|
||||
{ "call", "n", &cmdCall }, // 16
|
||||
{ "call.v", "v", &cmdCallF }, // 17
|
||||
{ "load.pic", "v", &cmdLoadPic }, // 18
|
||||
{ "draw.pic", "v", &cmdDrawPic }, // 19
|
||||
{ "show.pic", "", &cmdShowPic }, // 1A
|
||||
{ "discard.pic", "v", &cmdDiscardPic }, // 1B
|
||||
{ "overlay.pic", "v", &cmdOverlayPic }, // 1C
|
||||
{ "show.pri.screen", "", &cmdShowPriScreen }, // 1D
|
||||
{ "load.view", "n", &cmdLoadView }, // 1E
|
||||
{ "load.view.v", "v", &cmdLoadViewF }, // 1F
|
||||
{ "discard.view", "n", &cmdDiscardView }, // 20
|
||||
{ "animate.obj", "n", &cmdAnimateObj }, // 21
|
||||
{ "unanimate.all", "", &cmdUnanimateAll }, // 22
|
||||
{ "draw", "n", &cmdDraw }, // 23
|
||||
{ "erase", "n", &cmdErase }, // 24
|
||||
{ "position", "nnn", &cmdPosition }, // 25
|
||||
{ "position.v", "nvv", &cmdPositionF }, // 26
|
||||
{ "get.posn", "nvv", &cmdGetPosn }, // 27
|
||||
{ "reposition", "nvv", &cmdReposition }, // 28
|
||||
{ "set.view", "nn", &cmdSetView }, // 29
|
||||
{ "set.view.v", "nv", &cmdSetViewF }, // 2A
|
||||
{ "set.loop", "nn", &cmdSetLoop }, // 2B
|
||||
{ "set.loop.v", "nv", &cmdSetLoopF }, // 2C
|
||||
{ "fix.loop", "n", &cmdFixLoop }, // 2D
|
||||
{ "release.loop", "n", &cmdReleaseLoop }, // 2E
|
||||
{ "set.cel", "nn", &cmdSetCel }, // 2F
|
||||
{ "set.cel.v", "nv", &cmdSetCelF }, // 30
|
||||
{ "last.cel", "nv", &cmdLastCel }, // 31
|
||||
{ "current.cel", "nv", &cmdCurrentCel }, // 32
|
||||
{ "current.loop", "nv", &cmdCurrentLoop }, // 33
|
||||
{ "current.view", "nv", &cmdCurrentView }, // 34
|
||||
{ "number.of.loops", "nv", &cmdNumberOfLoops }, // 35
|
||||
{ "set.priority", "nn", &cmdSetPriority }, // 36
|
||||
{ "set.priority.v", "nv", &cmdSetPriorityF }, // 37
|
||||
{ "release.priority", "n", &cmdReleasePriority }, // 38
|
||||
{ "get.priority", "nn", &cmdGetPriority }, // 39
|
||||
{ "stop.update", "n", &cmdStopUpdate }, // 3A
|
||||
{ "start.update", "n", &cmdStartUpdate }, // 3B
|
||||
{ "force.update", "n", &cmdForceUpdate }, // 3C
|
||||
{ "ignore.horizon", "n", &cmdIgnoreHorizon }, // 3D
|
||||
{ "observe.horizon", "n", &cmdObserveHorizon }, // 3E
|
||||
{ "set.horizon", "n", &cmdSetHorizon }, // 3F
|
||||
{ "object.on.water", "n", &cmdObjectOnWater }, // 40
|
||||
{ "object.on.land", "n", &cmdObjectOnLand }, // 41
|
||||
{ "object.on.anything", "n", &cmdObjectOnAnything }, // 42
|
||||
{ "ignore.objs", "n", &cmdIgnoreObjs }, // 43
|
||||
{ "observe.objs", "n", &cmdObserveObjs }, // 44
|
||||
{ "distance", "nnv", &cmdDistance }, // 45
|
||||
{ "stop.cycling", "n", &cmdStopCycling }, // 46
|
||||
{ "start.cycling", "n", &cmdStartCycling }, // 47
|
||||
{ "normal.cycle", "n", &cmdNormalCycle }, // 48
|
||||
{ "end.of.loop", "nn", &cmdEndOfLoop }, // 49
|
||||
{ "reverse.cycle", "n", &cmdReverseCycle }, // 4A
|
||||
{ "reverse.loop", "nn", &cmdReverseLoop }, // 4B
|
||||
{ "cycle.time", "nv", &cmdCycleTime }, // 4C
|
||||
{ "stop.motion", "n", &cmdStopMotion }, // 4D
|
||||
{ "start.motion", "n", &cmdStartMotion }, // 4E
|
||||
{ "step.size", "nv", &cmdStepSize }, // 4F
|
||||
{ "step.time", "nv", &cmdStepTime }, // 50
|
||||
{ "move.obj", "nnnnn", &cmdMoveObj }, // 51
|
||||
{ "move.obj.v", "nvvvv", &cmdMoveObjF }, // 52
|
||||
{ "follow.ego", "nnn", &cmdFollowEgo }, // 53
|
||||
{ "wander", "n", &cmdWander }, // 54
|
||||
{ "normal.motion", "n", &cmdNormalMotion }, // 55
|
||||
{ "set.dir", "nv", &cmdSetDir }, // 56
|
||||
{ "get.dir", "nv", &cmdGetDir }, // 57
|
||||
{ "ignore.blocks", "n", &cmdIgnoreBlocks }, // 58
|
||||
{ "observe.blocks", "n", &cmdObserveBlocks }, // 59
|
||||
{ "block", "nnnn", &cmdBlock }, // 5A
|
||||
{ "unblock", "", &cmdUnblock }, // 5B
|
||||
{ "get", "n", &cmdGet }, // 5C
|
||||
{ "get.v", "v", &cmdGetF }, // 5D
|
||||
{ "drop", "n", &cmdDrop }, // 5E
|
||||
{ "put", "nn", &cmdPut }, // 5F
|
||||
{ "put.v", "vv", &cmdPutF }, // 60
|
||||
{ "get.room.v", "vv", &cmdGetRoomF }, // 61
|
||||
{ "load.sound", "n", &cmdLoadSound }, // 62
|
||||
{ "sound", "nn", &cmdSound }, // 63
|
||||
{ "stop.sound", "", &cmdStopSound }, // 64
|
||||
{ "print", "s", &cmdPrint }, // 65
|
||||
{ "print.v", "v", &cmdPrintF }, // 66
|
||||
{ "display", "nns", &cmdDisplay }, // 67
|
||||
{ "display.v", "vvv", &cmdDisplayF }, // 68
|
||||
{ "clear.lines", "nns", &cmdClearLines }, // 69
|
||||
{ "text.screen", "", &cmdTextScreen }, // 6A
|
||||
{ "graphics", "", &cmdGraphics }, // 6B
|
||||
{ "set.cursor.char", "s", &cmdSetCursorChar }, // 6C
|
||||
{ "set.text.attribute", "nn", &cmdSetTextAttribute }, // 6D
|
||||
{ "shake.screen", "n", &cmdShakeScreen }, // 6E
|
||||
{ "configure.screen", "nnn", &cmdConfigureScreen }, // 6F
|
||||
{ "status.line.on", "", &cmdStatusLineOn }, // 70
|
||||
{ "status.line.off", "", &cmdStatusLineOff }, // 71
|
||||
{ "set.string", "ns", &cmdSetString }, // 72
|
||||
{ "get.string", "nsnnn", &cmdGetString }, // 73
|
||||
{ "word.to.string", "nn", &cmdWordToString }, // 74
|
||||
{ "parse", "n", &cmdParse }, // 75
|
||||
{ "get.num", "nv", &cmdGetNum }, // 76
|
||||
{ "prevent.input", "", &cmdPreventInput }, // 77
|
||||
{ "accept.input", "", &cmdAcceptInput }, // 78
|
||||
{ "set.key", "nnn", &cmdSetKey }, // 79
|
||||
{ "add.to.pic", "nnnnnnn", &cmdAddToPic }, // 7A
|
||||
{ "add.to.pic.v", "vvvvvvv", &cmdAddToPicF }, // 7B
|
||||
{ "status", "", &cmdStatus }, // 7C
|
||||
{ "save.game", "", &cmdSaveGame }, // 7D
|
||||
{ "restore.game", "", &cmdLoadGame }, // 7E
|
||||
{ "init.disk", "", &cmdInitDisk }, // 7F
|
||||
{ "restart.game", "", &cmdRestartGame }, // 80
|
||||
{ "show.obj", "n", &cmdShowObj }, // 81
|
||||
{ "random", "nnv", &cmdRandom }, // 82
|
||||
{ "program.control", "", &cmdProgramControl }, // 83
|
||||
{ "player.control", "", &cmdPlayerControl }, // 84
|
||||
{ "obj.status.v", "v", &cmdObjStatusF }, // 85
|
||||
{ "quit", "n", &cmdQuit }, // 86 0 args for AGI version 2.089
|
||||
{ "show.mem", "", &cmdShowMem }, // 87
|
||||
{ "pause", "", &cmdPause }, // 88
|
||||
{ "echo.line", "", &cmdEchoLine }, // 89
|
||||
{ "cancel.line", "", &cmdCancelLine }, // 8A
|
||||
{ "init.joy", "", &cmdInitJoy }, // 8B
|
||||
{ "toggle.monitor", "", &cmdToggleMonitor }, // 8C
|
||||
{ "version", "", &cmdVersion }, // 8D
|
||||
{ "script.size", "n", &cmdScriptSize }, // 8E
|
||||
{ "set.game.id", "s", &cmdSetGameID }, // 8F
|
||||
{ "log", "s", &cmdLog }, // 90
|
||||
{ "set.scan.start", "", &cmdSetScanStart }, // 91
|
||||
{ "reset.scan.start", "", &cmdResetScanStart }, // 92
|
||||
{ "reposition.to", "nnn", &cmdRepositionTo }, // 93
|
||||
{ "reposition.to.v", "nvv", &cmdRepositionToF }, // 94
|
||||
{ "trace.on", "", &cmdTraceOn }, // 95
|
||||
{ "trace.info", "nnn", &cmdTraceInfo }, // 96
|
||||
{ "print.at", "snnn", &cmdPrintAt }, // 3 args for AGI versions before 2.089
|
||||
{ "print.at.v", "vnnn", &cmdPrintAtV }, // 98
|
||||
{ "discard.view.v", "v", &cmdDiscardView}, // 99
|
||||
{ "clear.text.rect", "nnnnn", &cmdClearTextRect }, // 9A
|
||||
{ "set.upper.left", "nn", &cmdSetUpperLeft }, // 9B Apple II
|
||||
{ "set.menu", "s", &cmdSetMenu }, // 9C
|
||||
{ "set.menu.item", "sn", &cmdSetMenuItem }, // 9D
|
||||
{ "submit.menu", "", &cmdSubmitMenu }, // 9E
|
||||
{ "enable.item", "n", &cmdEnableItem }, // 9F
|
||||
{ "disable.item", "n", &cmdDisableItem }, // A0
|
||||
{ "menu.input", "", &cmdMenuInput }, // A1
|
||||
{ "show.obj.v", "v", &cmdShowObjV }, // A2
|
||||
{ "open.dialogue", "", &cmdOpenDialogue }, // A3
|
||||
{ "close.dialogue", "", &cmdCloseDialogue }, // A4
|
||||
{ "mul.n", "vn", &cmdMulN }, // A5
|
||||
{ "mul.v", "vv", &cmdMulV }, // A6
|
||||
{ "div.n", "vn", &cmdDivN }, // A7
|
||||
{ "div.v", "vv", &cmdDivV }, // A8
|
||||
{ "close.window", "", &cmdCloseWindow }, // A9
|
||||
{ "set.simple", "n", &cmdSetSimple }, // AA AGI2.425+, *BUT* not included in AGI2.440, discard.sound in some Apple IIgs
|
||||
{ "push.script", "", &cmdPushScript }, // AB
|
||||
{ "pop.script", "", &cmdPopScript }, // AC
|
||||
{ "hold.key", "", &cmdHoldKey }, // AD
|
||||
{ "set.pri.base", "n", &cmdSetPriBase }, // AE AGI2.936+ *AND* also inside AGI2.425, discard.sound in some Apple IIgs
|
||||
{ "discard.sound", "n", &cmdDiscardSound }, // AF Apple IIGS only
|
||||
{ "hide.mouse", "", &cmdHideMouse }, // B0 1 arg for AGI3 Apple IIGS and AGI 3.002.086. AGI3+ only starts here
|
||||
{ "allow.menu", "n", &cmdAllowMenu }, // B1
|
||||
{ "show.mouse", "", &cmdShowMouse }, // B2 1 arg for AGI3 Apple IIGS
|
||||
{ "fence.mouse", "nnnn", &cmdFenceMouse }, // B3
|
||||
{ "get.mse.posn", "vv", &cmdGetMousePosn }, // B4
|
||||
{ "release.key", "", &cmdReleaseKey }, // B5
|
||||
{ "adj.ego.move.to.x.y","", &cmdAdjEgoMoveToXY } // B6 2 args for Amiga/Atari ST GR, MH1, MH2
|
||||
};
|
||||
|
||||
//
|
||||
// Currently, there is no known difference between v3.002.098 -> v3.002.149
|
||||
// So version emulated;
|
||||
// 0x0086,
|
||||
// 0x0149
|
||||
//
|
||||
|
||||
void AgiEngine::setupOpCodes(uint16 version) {
|
||||
const AgiOpCodeDefinitionEntry *opCodesTable = nullptr;
|
||||
const AgiOpCodeDefinitionEntry *opCodesCondTable = nullptr;
|
||||
uint16 opCodesTableSize = 0;
|
||||
uint16 opCodesCondTableSize = 0;
|
||||
|
||||
debug(0, "Setting up for version 0x%04X", version);
|
||||
|
||||
if (version >= 0x2000) {
|
||||
opCodesTable = opCodesV2;
|
||||
opCodesCondTable = opCodesV2Cond;
|
||||
opCodesTableSize = ARRAYSIZE(opCodesV2);
|
||||
opCodesCondTableSize = ARRAYSIZE(opCodesV2Cond);
|
||||
} else {
|
||||
opCodesTable = opCodesV1;
|
||||
opCodesCondTable = opCodesV1Cond;
|
||||
opCodesTableSize = ARRAYSIZE(opCodesV1);
|
||||
opCodesCondTableSize = ARRAYSIZE(opCodesV1Cond);
|
||||
}
|
||||
|
||||
// copy data over
|
||||
for (int opCodeNr = 0; opCodeNr < opCodesTableSize; opCodeNr++) {
|
||||
_opCodes[opCodeNr].name = opCodesTable[opCodeNr].name;
|
||||
_opCodes[opCodeNr].parameters = opCodesTable[opCodeNr].parameters;
|
||||
_opCodes[opCodeNr].functionPtr = opCodesTable[opCodeNr].functionPtr;
|
||||
}
|
||||
|
||||
for (int opCodeNr = 0; opCodeNr < opCodesCondTableSize; opCodeNr++) {
|
||||
_opCodesCond[opCodeNr].name = opCodesCondTable[opCodeNr].name;
|
||||
_opCodesCond[opCodeNr].parameters = opCodesCondTable[opCodeNr].parameters;
|
||||
_opCodesCond[opCodeNr].functionPtr = opCodesCondTable[opCodeNr].functionPtr;
|
||||
}
|
||||
|
||||
// Alter opcode parameters for specific games
|
||||
if (0x2000 <= version && version < 0x3000) {
|
||||
// AGI2 adjustments
|
||||
|
||||
// 'quit' takes 0 args for 2.089
|
||||
if (version == 0x2089)
|
||||
_opCodes[0x86].parameters = "";
|
||||
|
||||
// 'print.at' and 'print.at.v' take three parameters before 2.089.
|
||||
// This is documented in the specs as only < 2.440, but SQ1 1.0X (2.089)
|
||||
// and KQ3 (2.272) take four. Bug #10872. No game scripts have been
|
||||
// discovered that call either opcode with only three parameters.
|
||||
if (version < 0x2089) {
|
||||
_opCodes[0x97].parameters = "vvv";
|
||||
_opCodes[0x98].parameters = "vvv";
|
||||
}
|
||||
}
|
||||
|
||||
if (version >= 0x3000) {
|
||||
// AGI3 adjustments
|
||||
|
||||
// hide.mouse and hold.key take 1 parameter for 3.002.086.
|
||||
// KQ4 is the only known game with this interpreter and
|
||||
// its scripts do not call either opcode. no game scripts
|
||||
// have been discovered that call hold.key with 1 parameter.
|
||||
if (version == 0x3086) {
|
||||
_opCodes[0xb0].parameters = "n"; // hide.mouse
|
||||
_opCodes[0xad].parameters = "n"; // hold.key
|
||||
}
|
||||
|
||||
// hide.mouse and show.mouse take 1 parameter on Apple IIGS.
|
||||
// Used by Black Cauldron, Gold Rush, King's Quest IV, and Manhunter 1.
|
||||
// Fixes bugs #6161 and #5885.
|
||||
if (getPlatform() == Common::kPlatformApple2GS) {
|
||||
_opCodes[0xb0].parameters = "n"; // hide.mouse
|
||||
_opCodes[0xb2].parameters = "n"; // show.mouse
|
||||
}
|
||||
|
||||
// adj.ego.move.to.x.y takes two parameters for Amiga/Atari ST
|
||||
// versions of Gold Rush, Manhunter 1, and Manhunter 2.
|
||||
// No scripts have been discovered that call adj.ego.move.to.x.y
|
||||
// with zero parameters.
|
||||
if ((getGameID() == GID_GOLDRUSH ||
|
||||
getGameID() == GID_MH1 ||
|
||||
getGameID() == GID_MH2) &&
|
||||
(getPlatform() == Common::kPlatformAmiga ||
|
||||
getPlatform() == Common::kPlatformAtariST)) {
|
||||
_opCodes[0xb6].parameters = "vv";
|
||||
}
|
||||
}
|
||||
|
||||
// Apple IIgs adjustments
|
||||
if (getPlatform() == Common::kPlatformApple2GS) {
|
||||
// A2GS has platform-specific opcodes whose values changed over time.
|
||||
// Our A2GS version numbering isn't as precise as for DOS interpreters,
|
||||
// so the following version checks are just meant to separate A2GS games
|
||||
// into three broad groups. The use of these opcodes has been audited in
|
||||
// all known A2GS games and versions. Although all of these are currently
|
||||
// no-ops in our implementation, what's important is that they prevent
|
||||
// the "normal" opcodes from being unexpectedly called.
|
||||
|
||||
if (version <= 0x2440) {
|
||||
// opcode 170: discard.sound.
|
||||
// called by AGIDEMO, KQ1, LSL1, PQ1.
|
||||
memcpy(&_opCodes[0xaa], &_opCodes[0xaf], sizeof(AgiOpCodeDefinitionEntry));
|
||||
} else if (version < 0x3000) {
|
||||
// opcode 174: discard.sound.
|
||||
// called by MMMG, KQ2, KQ3, SQ2.
|
||||
memcpy(&_opCodes[0xae], &_opCodes[0xaf], sizeof(AgiOpCodeDefinitionEntry));
|
||||
|
||||
// TODO: opcode 175: unknown opcode that takes one unknown parameter.
|
||||
// called by KQ3 and SQ2. possibly related to sound. example:
|
||||
// SQ2 Logic 20: called once during music after kicking spores
|
||||
_opCodes[0xaf].name = "unknown";
|
||||
_opCodes[0xaf].parameters = "n";
|
||||
_opCodes[0xaf].functionPtr = &cmdUnknown;
|
||||
|
||||
// TODO: opcode 176: unknown opcode that takes one variable parameter.
|
||||
// called by SQ2 in only two places:
|
||||
// Logic 1: during the spaceship cutscene in the intro, called with 53
|
||||
// Logic 23: called twice with 39.
|
||||
_opCodes[0xb0].name = "unknown";
|
||||
_opCodes[0xb0].parameters = "v";
|
||||
_opCodes[0xb0].functionPtr = &cmdUnknown;
|
||||
} else {
|
||||
// AGI3 opcodes are already in the table:
|
||||
// opcode 175: discard sound.
|
||||
// called by KQ4 and MH1.
|
||||
}
|
||||
}
|
||||
|
||||
// AGI256 games use a modified opcode (set.simple) that loads 256 color pictures
|
||||
if (getFeatures() & GF_AGI256) {
|
||||
_opCodes[0xaa].functionPtr = &cmdAgi256LoadPic;
|
||||
}
|
||||
|
||||
// AGIMOUSE games use a modified opcode (push.script) that gets mouse state
|
||||
if (getFeatures() & GF_AGIMOUSE) {
|
||||
_opCodes[0xab].functionPtr = &cmdAgiMouseGetMouseState;
|
||||
}
|
||||
|
||||
// add invalid entries for every opcode, that is not defined at all
|
||||
for (int opCodeNr = opCodesTableSize; opCodeNr < ARRAYSIZE(_opCodes); opCodeNr++) {
|
||||
_opCodes[opCodeNr].name = "illegal";
|
||||
_opCodes[opCodeNr].parameters = "";
|
||||
_opCodes[opCodeNr].functionPtr = nullptr;
|
||||
_opCodes[opCodeNr].parameterSize = 0;
|
||||
}
|
||||
|
||||
for (int opCodeNr = opCodesCondTableSize; opCodeNr < ARRAYSIZE(_opCodesCond); opCodeNr++) {
|
||||
_opCodesCond[opCodeNr].name = "illegal";
|
||||
_opCodesCond[opCodeNr].parameters = "";
|
||||
_opCodesCond[opCodeNr].functionPtr = nullptr;
|
||||
_opCodesCond[opCodeNr].parameterSize = 0;
|
||||
}
|
||||
|
||||
// calculate parameter size
|
||||
for (int opCodeNr = 0; opCodeNr < opCodesTableSize; opCodeNr++) {
|
||||
_opCodes[opCodeNr].parameterSize = strlen(_opCodes[opCodeNr].parameters);
|
||||
}
|
||||
|
||||
for (int opCodeNr = 0; opCodeNr < opCodesCondTableSize; opCodeNr++) {
|
||||
_opCodesCond[opCodeNr].parameterSize = strlen(_opCodesCond[opCodeNr].parameters);
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
263
engines/agi/opcodes.h
Normal file
263
engines/agi/opcodes.h
Normal file
@@ -0,0 +1,263 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_OPCODES_H
|
||||
#define AGI_OPCODES_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
void cmdIncrement(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDecrement(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAssignN(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAssignV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAddN(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAddV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSubN(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSubV(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x08
|
||||
void cmdLindirectV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdRindirect(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLindirectN(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSet(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReset(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdToggle(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdResetV(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x10
|
||||
void cmdToggleV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdNewRoom(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdNewRoomF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLoadLogic(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLoadLogicF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCall(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCallF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLoadPic(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x18
|
||||
void cmdLoadPicV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDrawPic(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDrawPicV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdShowPic(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDiscardPic(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdOverlayPic(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdShowPriScreen(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLoadView(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLoadViewF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDiscardView(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x20
|
||||
void cmdAnimateObj(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdUnanimateAll(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDraw(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdErase(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPosition(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPositionV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPositionF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPositionFV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGetPosn(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReposition(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x28
|
||||
void cmdRepositionV1(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x28
|
||||
void cmdSetView(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetViewF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetLoop(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetLoopF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdFixLoop(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReleaseLoop(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetCel(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetCelF(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x30
|
||||
void cmdLastCel(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCurrentCel(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCurrentLoop(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCurrentView(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdNumberOfLoops(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetPriority(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetPriorityF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReleasePriority(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x38
|
||||
void cmdGetPriority(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStopUpdate(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStartUpdate(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdForceUpdate(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdIgnoreHorizon(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdObserveHorizon(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetHorizon(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdObjectOnWater(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x40
|
||||
void cmdObjectOnLand(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdObjectOnAnything(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdIgnoreObjs(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdObserveObjs(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDistance(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStopCycling(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStartCycling(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdNormalCycle(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x48
|
||||
void cmdEndOfLoop(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdEndOfLoopV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReverseCycle(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReverseLoop(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReverseLoopV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCycleTime(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStopMotion(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStopMotionV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStartMotion(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStartMotionV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStepSize(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStepTime(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x50
|
||||
void cmdMoveObj(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdMoveObjF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdFollowEgo(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdWander(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdNormalMotion(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetDir(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGetDir(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdIgnoreBlocks(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x58
|
||||
void cmdObserveBlocks(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdBlock(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdUnblock(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGet(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGetV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGetF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDrop(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPut(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPutF(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x60
|
||||
void cmdGetRoomF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLoadSound(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSound(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStopSound(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPrint(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPrintF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDisplay(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDisplayF(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x68
|
||||
void cmdClearLines(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdTextScreen(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGraphics(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetCursorChar(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetTextAttribute(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdShakeScreen(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdConfigureScreen(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStatusLineOn(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x70
|
||||
void cmdStatusLineOff(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetString(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGetString(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdWordToString(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdParse(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGetNum(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPreventInput(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAcceptInput(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x78
|
||||
void cmdSetKey(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAddToPic(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAddToPicV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAddToPicF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdStatus(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSaveGame(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLoadGame(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdInitDisk(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdRestartGame(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x80
|
||||
void cmdShowObj(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdRandom(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdRandomV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdProgramControl(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPlayerControl(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdObjStatusF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdQuit(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdQuitV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdShowMem(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPause(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x88
|
||||
void cmdEchoLine(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCancelLine(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdInitJoy(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdToggleMonitor(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdVersion(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdScriptSize(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetGameID(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdLog(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x90
|
||||
void cmdSetScanStart(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdResetScanStart(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdRepositionTo(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdRepositionToF(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdTraceOn(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdTraceInfo(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPrintAt(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdPrintAtV(AgiGame *state, AgiEngine *vm, uint8 *p); // 0x98
|
||||
//void cmdDiscardView(AgiGame *state, AgiEngine *vm, uint8 *p); // Opcode repeated from 0x20 ?
|
||||
void cmdClearTextRect(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetUpperLeft(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetMenu(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetMenuItem(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSubmitMenu(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdEnableItem(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDisableItem(AgiGame *state, AgiEngine *vm, uint8 *p); // 0xa0
|
||||
void cmdMenuInput(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdShowObjV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdOpenDialogue(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCloseDialogue(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdMulN(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdMulV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDivN(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDivV(AgiGame *state, AgiEngine *vm, uint8 *p); // 0xa8
|
||||
void cmdCloseWindow(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetSimple(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAgi256LoadPic(AgiGame *state, AgiEngine *vm, uint8 *p); // modified 0xaa
|
||||
void cmdPushScript(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAgiMouseGetMouseState(AgiGame *state, AgiEngine *vm, uint8 *p); // modified 0xab
|
||||
void cmdPopScript(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdHoldKey(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetPriBase(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdDiscardSound(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdHideMouse(AgiGame *state, AgiEngine *vm, uint8 *p); // 0xb0
|
||||
void cmdAllowMenu(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdShowMouse(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdFenceMouse(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdGetMousePosn(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdReleaseKey(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdAdjEgoMoveToXY(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
|
||||
void cmdSetSpeed(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetItemView(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdCallV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdNearWater(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdSetBit(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdClearBit(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void cmdUnknown(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
|
||||
void condEqual(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condEqualV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condLess(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condLessV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condGreater(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condGreaterV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condIsSet(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condIsSetV(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condHas(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condHasV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condObjInRoom(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condPosn(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condController(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condHaveKey(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condSaid(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condCompareStrings(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condObjInBox(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condCenterPosn(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condRightPosn(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condUnknown13(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condUnknown(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
|
||||
void condIsSetV1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condSaid1(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condSaid2(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condSaid3(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
void condBit(AgiGame *state, AgiEngine *vm, uint8 *p);
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_OPCODES_H */
|
||||
405
engines/agi/palette.h
Normal file
405
engines/agi/palette.h
Normal file
@@ -0,0 +1,405 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PALETTE_H
|
||||
#define AGI_PALETTE_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* 16 color RGB palette.
|
||||
* This array contains the 6-bit RGB values of the EGA palette exported
|
||||
* to the console drivers.
|
||||
*/
|
||||
static const uint8 PALETTE_EGA[16 * 3] = {
|
||||
0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x2a,
|
||||
0x00, 0x2a, 0x00,
|
||||
0x00, 0x2a, 0x2a,
|
||||
0x2a, 0x00, 0x00,
|
||||
0x2a, 0x00, 0x2a,
|
||||
0x2a, 0x15, 0x00,
|
||||
0x2a, 0x2a, 0x2a,
|
||||
0x15, 0x15, 0x15,
|
||||
0x15, 0x15, 0x3f,
|
||||
0x15, 0x3f, 0x15,
|
||||
0x15, 0x3f, 0x3f,
|
||||
0x3f, 0x15, 0x15,
|
||||
0x3f, 0x15, 0x3f,
|
||||
0x3f, 0x3f, 0x15,
|
||||
0x3f, 0x3f, 0x3f
|
||||
};
|
||||
|
||||
/**
|
||||
* 4 color CGA palette.
|
||||
*/
|
||||
static const uint8 PALETTE_CGA[4 * 3] = {
|
||||
0x00, 0x00, 0x00, // black
|
||||
0x55, 0xff, 0xff, // cyan
|
||||
0xff, 0x55, 0xff, // magenta
|
||||
0xff, 0xff, 0xff
|
||||
};
|
||||
|
||||
/**
|
||||
* 2 color Hercules (green) palette. Using 8-bit RGB values.
|
||||
*/
|
||||
static const uint8 PALETTE_HERCULES_GREEN[2 * 3] = {
|
||||
0x00, 0x00, 0x00, // black
|
||||
0x00, 0xdc, 0x28 // green
|
||||
};
|
||||
|
||||
/**
|
||||
* 2 color Hercules (amber) palette. Using 8-bit RGB values.
|
||||
*/
|
||||
static const uint8 PALETTE_HERCULES_AMBER[2 * 3] = {
|
||||
0x00, 0x00, 0x00, // black
|
||||
0xdc, 0xb4, 0x00 // amber
|
||||
};
|
||||
|
||||
/**
|
||||
* Atari ST AGI palette.
|
||||
* Used by all of the tested Atari ST AGI games
|
||||
* from Donald Duck's Playground (1986) to Manhunter II (1989).
|
||||
* 16 RGB colors. 3 bits per color component.
|
||||
*/
|
||||
static const uint8 PALETTE_ATARI_ST[16 * 3] = {
|
||||
0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x7,
|
||||
0x0, 0x4, 0x0,
|
||||
0x0, 0x5, 0x4,
|
||||
0x5, 0x0, 0x0,
|
||||
0x5, 0x3, 0x6,
|
||||
0x4, 0x3, 0x0,
|
||||
0x5, 0x5, 0x5,
|
||||
0x3, 0x3, 0x2,
|
||||
0x0, 0x5, 0x7,
|
||||
0x0, 0x6, 0x0,
|
||||
0x0, 0x7, 0x6,
|
||||
0x7, 0x2, 0x3,
|
||||
0x7, 0x4, 0x7,
|
||||
0x7, 0x7, 0x4,
|
||||
0x7, 0x7, 0x7
|
||||
};
|
||||
|
||||
/**
|
||||
* Second generation Apple IIGS AGI palette.
|
||||
* A 16-color, 12-bit RGB palette.
|
||||
*
|
||||
* Used by at least the following Apple IIGS AGI versions:
|
||||
* 1.003 (Leisure Suit Larry I v1.0E, intro says 1987)
|
||||
* 1.005 (AGI Demo 2 1987-06-30?)
|
||||
* 1.006 (King's Quest I v1.0S 1988-02-23)
|
||||
* 1.007 (Police Quest I v2.0B 1988-04-21 8:00am)
|
||||
* 1.013 (King's Quest II v2.0A 1988-06-16 (CE))
|
||||
* 1.013 (Mixed-Up Mother Goose v2.0A 1988-05-31 10:00am)
|
||||
* 1.014 (King's Quest III v2.0A 1988-08-28 (CE))
|
||||
* 1.014 (Space Quest II v2.0A, LOGIC.141 says 1988)
|
||||
* 2.004 (Manhunter I v2.0E 1988-10-05 (CE))
|
||||
* 2.006 (King's Quest IV v1.0K 1988-11-22 (CE))
|
||||
* 3.001 (Black Cauldron v1.0O 1989-02-24 (CE))
|
||||
* 3.003 (Gold Rush! v1.0M 1989-02-28 (CE))
|
||||
*/
|
||||
// *NOT* identical to Amiga generation 2 palette
|
||||
static const uint8 PALETTE_APPLE_II_GS[16 * 3] = {
|
||||
0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0xF,
|
||||
0x0, 0x8, 0x0,
|
||||
0x0, 0xD, 0xB,
|
||||
0xC, 0x0, 0x0,
|
||||
0xB, 0x7, 0xD,
|
||||
0x8, 0x5, 0x0,
|
||||
0xB, 0xB, 0xB,
|
||||
0x7, 0x7, 0x7,
|
||||
0x0, 0xB, 0xF,
|
||||
0x0, 0xE, 0x0,
|
||||
0x0, 0xF, 0xD,
|
||||
0xF, 0x9, 0x8,
|
||||
0xD, 0x9, 0xF, // difference between Amiga v2 palette and Apple II GS palette, gotten from emulator (SQ2)
|
||||
0xE, 0xE, 0x0,
|
||||
0xF, 0xF, 0xF
|
||||
};
|
||||
|
||||
// Re-use Amiga v1 palette for Apple IIgs Space Quest 1
|
||||
#define PALETTE_APPLE_II_GS_SQ1 PALETTE_AMIGA_V1
|
||||
|
||||
/**
|
||||
* First generation Amiga & Apple IIGS AGI palette.
|
||||
* A 16-color, 12-bit RGB palette.
|
||||
*
|
||||
* Used by at least the following Amiga AGI versions:
|
||||
* 2.082 (King's Quest I v1.0U 1986)
|
||||
* 2.082 (Space Quest I v1.2 1986)
|
||||
* 2.090 (King's Quest III v1.01 1986-11-08)
|
||||
* 2.107 (King's Quest II v2.0J 1987-01-29)
|
||||
* x.yyy (Black Cauldron v2.00 1987-06-14)
|
||||
* x.yyy (Larry I v1.05 1987-06-26)
|
||||
*
|
||||
* Also used by at least the following Apple IIGS AGI versions:
|
||||
* 1.002 (Space Quest I, intro says v2.2 1987)
|
||||
*/
|
||||
static const uint8 PALETTE_AMIGA_V1[16 * 3] = {
|
||||
0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0xF,
|
||||
0x0, 0x8, 0x0,
|
||||
0x0, 0xD, 0xB,
|
||||
0xC, 0x0, 0x0,
|
||||
0xB, 0x7, 0xD,
|
||||
0x8, 0x5, 0x0,
|
||||
0xB, 0xB, 0xB,
|
||||
0x7, 0x7, 0x7,
|
||||
0x0, 0xB, 0xF,
|
||||
0x0, 0xE, 0x0,
|
||||
0x0, 0xF, 0xD,
|
||||
0xF, 0x9, 0x8,
|
||||
0xF, 0x7, 0x0,
|
||||
0xE, 0xE, 0x0,
|
||||
0xF, 0xF, 0xF
|
||||
};
|
||||
|
||||
/**
|
||||
* Second generation Amiga AGI palette.
|
||||
* A 16-color, 12-bit RGB palette.
|
||||
*
|
||||
* Used by at least the following Amiga AGI versions:
|
||||
* 2.202 (Space Quest II v2.0F. Intro says 1988. ScummVM 0.10.0 detects as 1986-12-09)
|
||||
*/
|
||||
static const uint8 PALETTE_AMIGA_V2[16 * 3] = {
|
||||
0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0xF,
|
||||
0x0, 0x8, 0x0,
|
||||
0x0, 0xD, 0xB,
|
||||
0xC, 0x0, 0x0,
|
||||
0xB, 0x7, 0xD,
|
||||
0x8, 0x5, 0x0,
|
||||
0xB, 0xB, 0xB,
|
||||
0x7, 0x7, 0x7,
|
||||
0x0, 0xB, 0xF,
|
||||
0x0, 0xE, 0x0,
|
||||
0x0, 0xF, 0xD,
|
||||
0xF, 0x9, 0x8,
|
||||
0xD, 0x0, 0xF,
|
||||
0xE, 0xE, 0x0,
|
||||
0xF, 0xF, 0xF
|
||||
};
|
||||
|
||||
/**
|
||||
* Third generation Amiga AGI palette.
|
||||
* A 16-color, 12-bit RGB palette.
|
||||
*
|
||||
* Used by at least the following Amiga AGI versions:
|
||||
* 2.310 (Police Quest I v2.0B 1989-02-22)
|
||||
* 2.316 (Gold Rush! v2.05 1989-03-09)
|
||||
* x.yyy (Manhunter I v1.06 1989-03-18)
|
||||
* 2.333 (Manhunter II v3.06 1989-08-17)
|
||||
* 2.333 (King's Quest III v2.15 1989-11-15)
|
||||
*/
|
||||
static const uint8 PALETTE_AMIGA_V3[16 * 3] = {
|
||||
0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0xB,
|
||||
0x0, 0xB, 0x0,
|
||||
0x0, 0xB, 0xB,
|
||||
0xB, 0x0, 0x0,
|
||||
0xB, 0x0, 0xB,
|
||||
0xC, 0x7, 0x0,
|
||||
0xB, 0xB, 0xB,
|
||||
0x7, 0x7, 0x7,
|
||||
0x0, 0x0, 0xF,
|
||||
0x0, 0xF, 0x0,
|
||||
0x0, 0xF, 0xF,
|
||||
0xF, 0x0, 0x0,
|
||||
0xF, 0x0, 0xF,
|
||||
0xF, 0xF, 0x0,
|
||||
0xF, 0xF, 0xF
|
||||
};
|
||||
|
||||
/**
|
||||
* 16 color amiga-ish palette.
|
||||
*/
|
||||
static const uint8 PALETTE_AMIGA_ALT[16 * 3] = {
|
||||
0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x3f,
|
||||
0x00, 0x2A, 0x00,
|
||||
0x00, 0x2A, 0x2A,
|
||||
0x33, 0x00, 0x00,
|
||||
0x2f, 0x1c, 0x37,
|
||||
0x23, 0x14, 0x00,
|
||||
0x2f, 0x2f, 0x2f,
|
||||
0x15, 0x15, 0x15,
|
||||
0x00, 0x2f, 0x3f,
|
||||
0x00, 0x33, 0x15,
|
||||
0x15, 0x3F, 0x3F,
|
||||
0x3f, 0x27, 0x23,
|
||||
0x3f, 0x15, 0x3f,
|
||||
0x3b, 0x3b, 0x00,
|
||||
0x3F, 0x3F, 0x3F
|
||||
};
|
||||
|
||||
/**
|
||||
* 16 color Macintosh palette (CLUT format).
|
||||
*
|
||||
* Used by at least the following Macintosh AGI versions:
|
||||
* ?.??? (Police Quest 1 v1.50 23.03.1988)
|
||||
* ?.??? (King's Quest 3 v1.52 11.04.1988)
|
||||
*/
|
||||
static const uint16 PALETTE_MACINTOSH_CLUT[16 * 3] = {
|
||||
0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0xC000,
|
||||
0x0000, 0xA800, 0x0000,
|
||||
0x0000, 0xA000, 0xA000,
|
||||
0xCE50, 0x0000, 0x0000,
|
||||
0xC080, 0x0000, 0xFFFF,
|
||||
0xD000, 0x6130, 0x32D0,
|
||||
0xC000, 0xC000, 0xC000,
|
||||
0x6000, 0x6000, 0x6000,
|
||||
0x6800, 0x6800, 0xFFFF,
|
||||
0x0000, 0xFFFF, 0x0000,
|
||||
0x0000, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0x5390, 0x64B0,
|
||||
0xFFFF, 0x8000, 0x0000,
|
||||
0xFFFF, 0xFFFF, 0x0000,
|
||||
0xFFFF, 0xFFFF, 0xFFFF
|
||||
};
|
||||
|
||||
/**
|
||||
* 16 color Macintosh palette (CLUT format).
|
||||
*
|
||||
* Used by at least the following Macintosh AGI versions:
|
||||
* ?.??? (Gold Rush v1.78 28.07.1989)
|
||||
*/
|
||||
static const uint16 PALETTE_MACINTOSH_CLUT2[16 * 3] = {
|
||||
0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0xC000,
|
||||
0x6524, 0xC2FF, 0x0000,
|
||||
0x0000, 0xA000, 0xA000,
|
||||
0xDD6B, 0x08C2, 0x06A2,
|
||||
0x8000, 0x0000, 0xFFFF,
|
||||
0x93FF, 0x281A, 0x12CC,
|
||||
0xC000, 0xC000, 0xC000,
|
||||
0x8000, 0x8000, 0x8000,
|
||||
0x0000, 0x0000, 0xD400,
|
||||
0x0000, 0xFFFF, 0x04F1,
|
||||
0x0241, 0xAB54, 0xEAFF,
|
||||
0xFFFF, 0xC3DC, 0x8160,
|
||||
0xFFFF, 0x648A, 0x028C,
|
||||
0xFC00, 0xF37D, 0x052F,
|
||||
0xFFFF, 0xFFFF, 0xFFFF
|
||||
};
|
||||
|
||||
/**
|
||||
* 16 color Macintosh palette (CLUT format).
|
||||
*
|
||||
* Used by at least the following Macintosh AGI versions:
|
||||
* ?.??? (Space Quest 2 v1.51 04.04.1988)
|
||||
*/
|
||||
static const uint16 PALETTE_MACINTOSH_CLUT3[16 * 3] = {
|
||||
0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0xC000,
|
||||
0x0000, 0xA7FF, 0x0000,
|
||||
0x0000, 0x9FFF, 0x9FFF,
|
||||
0xCE50, 0x0000, 0x0000,
|
||||
0xC079, 0x0000, 0xFFFF,
|
||||
0xCFFF, 0x6130, 0x32D0,
|
||||
0xC000, 0xC000, 0xC000,
|
||||
0x6000, 0x6000, 0x6000,
|
||||
0x6800, 0x6800, 0xFFFF,
|
||||
0x0000, 0xFFFF, 0x0000,
|
||||
0x0000, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0x538C, 0x64B1,
|
||||
0xFDCE, 0x1AC0, 0xFFFF,
|
||||
0xFFFF, 0xFFFF, 0x0000,
|
||||
0xFFFF, 0xFFFF, 0xFFFF,
|
||||
};
|
||||
|
||||
/**
|
||||
* 256 color palette used with AGI256 and AGI256-2 games.
|
||||
* Uses full 8 bits per color component.
|
||||
* This is NOT the standard VGA palette.
|
||||
*/
|
||||
static const uint8 PALETTE_VGA[256 * 3] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xA8, 0x00, 0xA8, 0x00, 0x00, 0xA8, 0xA8,
|
||||
0xA8, 0x00, 0x00, 0xA8, 0x00, 0xA8, 0xA8, 0x54, 0x00, 0xA8, 0xA8, 0xA8,
|
||||
0x54, 0x54, 0x54, 0x54, 0x54, 0xFC, 0x54, 0xFC, 0x54, 0x54, 0xFC, 0xFC,
|
||||
0xFC, 0x54, 0x54, 0xFC, 0x54, 0xFC, 0xFC, 0xFC, 0x54, 0xFC, 0xFC, 0xFC,
|
||||
0x00, 0x00, 0x00, 0x14, 0x14, 0x14, 0x20, 0x20, 0x20, 0x2C, 0x2C, 0x2C,
|
||||
0x38, 0x38, 0x38, 0x44, 0x44, 0x44, 0x50, 0x50, 0x50, 0x60, 0x60, 0x60,
|
||||
0x70, 0x70, 0x70, 0x80, 0x80, 0x80, 0x90, 0x90, 0x90, 0xA0, 0xA0, 0xA0,
|
||||
0xB4, 0xB4, 0xB4, 0xC8, 0xC8, 0xC8, 0xE0, 0xE0, 0xE0, 0xFC, 0xFC, 0xFC,
|
||||
0x00, 0x00, 0xFC, 0x40, 0x00, 0xFC, 0x7C, 0x00, 0xFC, 0xBC, 0x00, 0xFC,
|
||||
0xFC, 0x00, 0xFC, 0xFC, 0x00, 0xBC, 0xFC, 0x00, 0x7C, 0xFC, 0x00, 0x40,
|
||||
0xFC, 0x00, 0x00, 0xFC, 0x40, 0x00, 0xFC, 0x7C, 0x00, 0xFC, 0xBC, 0x00,
|
||||
0xFC, 0xFC, 0x00, 0xBC, 0xFC, 0x00, 0x7C, 0xFC, 0x00, 0x40, 0xFC, 0x00,
|
||||
0x00, 0xFC, 0x00, 0x00, 0xFC, 0x40, 0x00, 0xFC, 0x7C, 0x00, 0xFC, 0xBC,
|
||||
0x00, 0xFC, 0xFC, 0x00, 0xBC, 0xFC, 0x00, 0x7C, 0xFC, 0x00, 0x40, 0xFC,
|
||||
0x7C, 0x7C, 0xFC, 0x9C, 0x7C, 0xFC, 0xBC, 0x7C, 0xFC, 0xDC, 0x7C, 0xFC,
|
||||
0xFC, 0x7C, 0xFC, 0xFC, 0x7C, 0xDC, 0xFC, 0x7C, 0xBC, 0xFC, 0x7C, 0x9C,
|
||||
0xFC, 0x7C, 0x7C, 0xFC, 0x9C, 0x7C, 0xFC, 0xBC, 0x7C, 0xFC, 0xDC, 0x7C,
|
||||
0xFC, 0xFC, 0x7C, 0xDC, 0xFC, 0x7C, 0xBC, 0xFC, 0x7C, 0x9C, 0xFC, 0x7C,
|
||||
0x7C, 0xFC, 0x7C, 0x7C, 0xFC, 0x9C, 0x7C, 0xFC, 0xBC, 0x7C, 0xFC, 0xDC,
|
||||
0x7C, 0xFC, 0xFC, 0x7C, 0xDC, 0xFC, 0x7C, 0xBC, 0xFC, 0x7C, 0x9C, 0xFC,
|
||||
0xB4, 0xB4, 0xFC, 0xC4, 0xB4, 0xFC, 0xD8, 0xB4, 0xFC, 0xE8, 0xB4, 0xFC,
|
||||
0xFC, 0xB4, 0xFC, 0xFC, 0xB4, 0xE8, 0xFC, 0xB4, 0xD8, 0xFC, 0xB4, 0xC4,
|
||||
0xFC, 0xB4, 0xB4, 0xFC, 0xC4, 0xB4, 0xFC, 0xD8, 0xB4, 0xFC, 0xE8, 0xB4,
|
||||
0xFC, 0xFC, 0xB4, 0xE8, 0xFC, 0xB4, 0xD8, 0xFC, 0xB4, 0xC4, 0xFC, 0xB4,
|
||||
0xB4, 0xFC, 0xB4, 0xB4, 0xFC, 0xC4, 0xB4, 0xFC, 0xD8, 0xB4, 0xFC, 0xE8,
|
||||
0xB4, 0xFC, 0xFC, 0xB4, 0xE8, 0xFC, 0xB4, 0xD8, 0xFC, 0xB4, 0xC4, 0xFC,
|
||||
0x00, 0x00, 0x70, 0x1C, 0x00, 0x70, 0x38, 0x00, 0x70, 0x54, 0x00, 0x70,
|
||||
0x70, 0x00, 0x70, 0x70, 0x00, 0x54, 0x70, 0x00, 0x38, 0x70, 0x00, 0x1C,
|
||||
0x70, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x70, 0x38, 0x00, 0x70, 0x54, 0x00,
|
||||
0x70, 0x70, 0x00, 0x54, 0x70, 0x00, 0x38, 0x70, 0x00, 0x1C, 0x70, 0x00,
|
||||
0x00, 0x70, 0x00, 0x00, 0x70, 0x1C, 0x00, 0x70, 0x38, 0x00, 0x70, 0x54,
|
||||
0x00, 0x70, 0x70, 0x00, 0x54, 0x70, 0x00, 0x38, 0x70, 0x00, 0x1C, 0x70,
|
||||
0x38, 0x38, 0x70, 0x44, 0x38, 0x70, 0x54, 0x38, 0x70, 0x60, 0x38, 0x70,
|
||||
0x70, 0x38, 0x70, 0x70, 0x38, 0x60, 0x70, 0x38, 0x54, 0x70, 0x38, 0x44,
|
||||
0x70, 0x38, 0x38, 0x70, 0x44, 0x38, 0x70, 0x54, 0x38, 0x70, 0x60, 0x38,
|
||||
0x70, 0x70, 0x38, 0x60, 0x70, 0x38, 0x54, 0x70, 0x38, 0x44, 0x70, 0x38,
|
||||
0x38, 0x70, 0x38, 0x38, 0x70, 0x44, 0x38, 0x70, 0x54, 0x38, 0x70, 0x60,
|
||||
0x38, 0x70, 0x70, 0x38, 0x60, 0x70, 0x38, 0x54, 0x70, 0x38, 0x44, 0x70,
|
||||
0x50, 0x50, 0x70, 0x58, 0x50, 0x70, 0x60, 0x50, 0x70, 0x68, 0x50, 0x70,
|
||||
0x70, 0x50, 0x70, 0x70, 0x50, 0x68, 0x70, 0x50, 0x60, 0x70, 0x50, 0x58,
|
||||
0x70, 0x50, 0x50, 0x70, 0x58, 0x50, 0x70, 0x60, 0x50, 0x70, 0x68, 0x50,
|
||||
0x70, 0x70, 0x50, 0x68, 0x70, 0x50, 0x60, 0x70, 0x50, 0x58, 0x70, 0x50,
|
||||
0x50, 0x70, 0x50, 0x50, 0x70, 0x58, 0x50, 0x70, 0x60, 0x50, 0x70, 0x68,
|
||||
0x50, 0x70, 0x70, 0x50, 0x68, 0x70, 0x50, 0x60, 0x70, 0x50, 0x58, 0x70,
|
||||
0x00, 0x00, 0x40, 0x10, 0x00, 0x40, 0x20, 0x00, 0x40, 0x30, 0x00, 0x40,
|
||||
0x40, 0x00, 0x40, 0x40, 0x00, 0x30, 0x40, 0x00, 0x20, 0x40, 0x00, 0x10,
|
||||
0x40, 0x00, 0x00, 0x40, 0x10, 0x00, 0x40, 0x20, 0x00, 0x40, 0x30, 0x00,
|
||||
0x40, 0x40, 0x00, 0x30, 0x40, 0x00, 0x20, 0x40, 0x00, 0x10, 0x40, 0x00,
|
||||
0x00, 0x40, 0x00, 0x00, 0x40, 0x10, 0x00, 0x40, 0x20, 0x00, 0x40, 0x30,
|
||||
0x00, 0x40, 0x40, 0x00, 0x30, 0x40, 0x00, 0x20, 0x40, 0x00, 0x10, 0x40,
|
||||
0x20, 0x20, 0x40, 0x28, 0x20, 0x40, 0x30, 0x20, 0x40, 0x38, 0x20, 0x40,
|
||||
0x40, 0x20, 0x40, 0x40, 0x20, 0x38, 0x40, 0x20, 0x30, 0x40, 0x20, 0x28,
|
||||
0x40, 0x20, 0x20, 0x40, 0x28, 0x20, 0x40, 0x30, 0x20, 0x40, 0x38, 0x20,
|
||||
0x40, 0x40, 0x20, 0x38, 0x40, 0x20, 0x30, 0x40, 0x20, 0x28, 0x40, 0x20,
|
||||
0x20, 0x40, 0x20, 0x20, 0x40, 0x28, 0x20, 0x40, 0x30, 0x20, 0x40, 0x38,
|
||||
0x20, 0x40, 0x40, 0x20, 0x38, 0x40, 0x20, 0x30, 0x40, 0x20, 0x28, 0x40,
|
||||
0x2C, 0x2C, 0x40, 0x30, 0x2C, 0x40, 0x34, 0x2C, 0x40, 0x3C, 0x2C, 0x40,
|
||||
0x40, 0x2C, 0x40, 0x40, 0x2C, 0x3C, 0x40, 0x2C, 0x34, 0x40, 0x2C, 0x30,
|
||||
0x40, 0x2C, 0x2C, 0x40, 0x30, 0x2C, 0x40, 0x34, 0x2C, 0x40, 0x3C, 0x2C,
|
||||
0x40, 0x40, 0x2C, 0x3C, 0x40, 0x2C, 0x34, 0x40, 0x2C, 0x30, 0x40, 0x2C,
|
||||
0x2C, 0x40, 0x2C, 0x2C, 0x40, 0x30, 0x2C, 0x40, 0x34, 0x2C, 0x40, 0x3C,
|
||||
0x2C, 0x40, 0x40, 0x2C, 0x3C, 0x40, 0x2C, 0x34, 0x40, 0x2C, 0x30, 0x40,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_PALETTE_H */
|
||||
864
engines/agi/picture.cpp
Normal file
864
engines/agi/picture.cpp
Normal file
@@ -0,0 +1,864 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
|
||||
#include "common/textconsole.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// PictureMgr decodes and draws AGI picture resources.
|
||||
//
|
||||
// AGI pictures are vector-based, and contain the visual and priority screens.
|
||||
// Drawing instructions begin with an opcode byte within the range F0-FF.
|
||||
// Opcode parameters are each one byte, with the exception of AGIv3 nibble
|
||||
// compression. Some opcodes take a variable number of parameters. The end of
|
||||
// an instruction is detected by the next byte with a value in the opcode range.
|
||||
// If an instruction has extra bytes, or a picture contains an unknown opcode
|
||||
// byte, then these bytes ignored. Pictures end with opcode FF.
|
||||
//
|
||||
// AGIv3 introduced a compression scheme where two opcode parameters were
|
||||
// each reduced to one nibble; this is indicated by a flag in the picture's
|
||||
// resource directory entry.
|
||||
//
|
||||
// AGI's picture format evolved from variants used in earlier Sierra games.
|
||||
// We implement support for these formats as subclasses of PictureMgr.
|
||||
// In this way, we treat AGI as the baseline to be overridden as needed.
|
||||
|
||||
PictureMgr::PictureMgr(AgiBase *agi, GfxMgr *gfx) {
|
||||
_vm = agi;
|
||||
_gfx = gfx;
|
||||
|
||||
_resourceNr = 0;
|
||||
_data = nullptr;
|
||||
_dataSize = 0;
|
||||
_dataOffset = 0;
|
||||
_dataOffsetNibble = false;
|
||||
|
||||
_patCode = 0;
|
||||
_patNum = 0;
|
||||
_priOn = 0;
|
||||
_scrOn = 0;
|
||||
_scrColor = 0;
|
||||
_priColor = 0;
|
||||
|
||||
_minCommand = 0xf0;
|
||||
|
||||
_width = 0;
|
||||
_height = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a pixel to the visual and/or control screen.
|
||||
*/
|
||||
void PictureMgr::putVirtPixel(int16 x, int16 y) {
|
||||
if (!getGraphicsCoordinates(x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte drawMask= 0;
|
||||
if (_priOn)
|
||||
drawMask |= GFX_SCREEN_MASK_PRIORITY;
|
||||
if (_scrOn)
|
||||
drawMask |= GFX_SCREEN_MASK_VISUAL;
|
||||
|
||||
_gfx->putPixel(x, y, drawMask, _scrColor, _priColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next byte in the picture data.
|
||||
*/
|
||||
byte PictureMgr::getNextByte() {
|
||||
if (!_dataOffsetNibble) {
|
||||
return _data[_dataOffset++];
|
||||
} else {
|
||||
byte curByte = _data[_dataOffset++] << 4;
|
||||
return (_data[_dataOffset] >> 4) | curByte;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next byte in the current picture instruction.
|
||||
* If the next byte in the picture data is an opcode, then this
|
||||
* function returns false and the data offset is not advanced.
|
||||
*/
|
||||
bool PictureMgr::getNextParamByte(byte &b) {
|
||||
byte value = getNextByte();
|
||||
if (value >= _minCommand) {
|
||||
_dataOffset--;
|
||||
return false;
|
||||
}
|
||||
b = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next nibble in the picture data.
|
||||
*/
|
||||
byte PictureMgr::getNextNibble() {
|
||||
if (!_dataOffsetNibble) {
|
||||
_dataOffsetNibble = true;
|
||||
return _data[_dataOffset] >> 4;
|
||||
} else {
|
||||
_dataOffsetNibble = false;
|
||||
return _data[_dataOffset++] & 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next x coordinate in the current picture instruction.
|
||||
*
|
||||
* Subclasses override this to implement coordinate clipping.
|
||||
*/
|
||||
bool PictureMgr::getNextXCoordinate(byte &x) {
|
||||
return getNextParamByte(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next y coordinate in the current picture instruction.
|
||||
*
|
||||
* Subclasses override this to implement coordinate clipping.
|
||||
*/
|
||||
bool PictureMgr::getNextYCoordinate(byte &y) {
|
||||
return getNextParamByte(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next x and y coordinates in the current picture instruction.
|
||||
*
|
||||
* Returns false if both coordinates are not present. If only an x coordinate is
|
||||
* present, then the data offset is only advanced by one, and the x coordinate
|
||||
* will be ignored.
|
||||
*/
|
||||
bool PictureMgr::getNextCoordinates(byte &x, byte &y) {
|
||||
return getNextXCoordinate(x) && getNextYCoordinate(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates picture coordinates and translates them to GfxMgr coordinates.
|
||||
*
|
||||
* Subclasses can override this to implement the PreAGI offset feature that
|
||||
* allowed a picture to be drawn at an arbitrary point on the screen.
|
||||
* Returns false if picture coordinates are out of bounds, or for subclasses,
|
||||
* if the PreAGI offset would place the coordinate outside of GfxMgr's screen.
|
||||
*/
|
||||
bool PictureMgr::getGraphicsCoordinates(int16 &x, int16 &y) {
|
||||
return (0 <= x && x < _width && 0 <= y && y < _height);
|
||||
}
|
||||
|
||||
/**
|
||||
* xCorner
|
||||
*
|
||||
* Draws a series of lines with right angles between them.
|
||||
* The first two bytes are the start point, followed by alternating
|
||||
* x and y coordinates for subsequent points.
|
||||
*
|
||||
* Set skipOtherCoords to ignore extra coordinates in Troll's pictures.
|
||||
* Troll includes both the x and y coordinate of each point.
|
||||
*/
|
||||
void PictureMgr::xCorner(bool skipOtherCoords) {
|
||||
byte x1, x2, y1, y2, dummy;
|
||||
|
||||
if (!getNextCoordinates(x1, y1))
|
||||
return;
|
||||
|
||||
putVirtPixel(x1, y1);
|
||||
|
||||
for (;;) {
|
||||
if (!getNextXCoordinate(x2))
|
||||
break;
|
||||
|
||||
if (skipOtherCoords)
|
||||
if (!getNextParamByte(dummy))
|
||||
break;
|
||||
|
||||
draw_Line(x1, y1, x2, y1);
|
||||
x1 = x2;
|
||||
|
||||
if (skipOtherCoords)
|
||||
if (!getNextParamByte(dummy))
|
||||
break;
|
||||
|
||||
if (!getNextYCoordinate(y2))
|
||||
break;
|
||||
|
||||
draw_Line(x1, y1, x1, y2);
|
||||
y1 = y2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* yCorner
|
||||
*
|
||||
* Draws a series of lines with right angles between them.
|
||||
* The first two bytes are the start point, followed by alternating
|
||||
* y and x coordinates for subsequent points.
|
||||
*
|
||||
* Set skipOtherCoords to ignore extra coordinates in Troll's pictures.
|
||||
* Troll includes both the x and y coordinate of each point.
|
||||
*/
|
||||
void PictureMgr::yCorner(bool skipOtherCoords) {
|
||||
byte x1, x2, y1, y2, dummy;
|
||||
|
||||
if (!getNextCoordinates(x1, y1))
|
||||
return;
|
||||
|
||||
putVirtPixel(x1, y1);
|
||||
|
||||
for (;;) {
|
||||
if (skipOtherCoords)
|
||||
if (!getNextParamByte(dummy))
|
||||
break;
|
||||
|
||||
if (!getNextYCoordinate(y2))
|
||||
break;
|
||||
|
||||
draw_Line(x1, y1, x1, y2);
|
||||
y1 = y2;
|
||||
if (!getNextXCoordinate(x2))
|
||||
break;
|
||||
|
||||
if (skipOtherCoords)
|
||||
if (!getNextParamByte(dummy))
|
||||
break;
|
||||
|
||||
draw_Line(x1, y1, x2, y1);
|
||||
x1 = x2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* plotPattern
|
||||
*
|
||||
* Draws a circle or square. Size and optional splatter brush pattern
|
||||
* are determined by the current pattern code.
|
||||
*
|
||||
* This routine is originally from NAGI.
|
||||
*/
|
||||
void PictureMgr::plotPattern(byte x, byte y) {
|
||||
static const uint16 binary_list[] = {
|
||||
0x8000, 0x4000, 0x2000, 0x1000, 0x800, 0x400, 0x200, 0x100,
|
||||
0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1
|
||||
};
|
||||
|
||||
static const uint8 circle_list[] = {
|
||||
0, 1, 4, 9, 16, 25, 37, 50
|
||||
};
|
||||
|
||||
static const uint16 circle_data[] = {
|
||||
0x8000,
|
||||
0x0000, 0xE000, 0x0000,
|
||||
0x7000, 0xF800, 0x0F800, 0x0F800, 0x7000,
|
||||
0x3800, 0x7C00, 0x0FE00, 0x0FE00, 0x0FE00, 0x7C00, 0x3800,
|
||||
0x1C00, 0x7F00, 0x0FF80, 0x0FF80, 0x0FF80, 0x0FF80, 0x0FF80, 0x7F00, 0x1C00,
|
||||
0x0E00, 0x3F80, 0x7FC0, 0x7FC0, 0x0FFE0, 0x0FFE0, 0x0FFE0, 0x7FC0, 0x7FC0, 0x3F80, 0x1F00, 0x0E00,
|
||||
0x0F80, 0x3FE0, 0x7FF0, 0x7FF0, 0x0FFF8, 0x0FFF8, 0x0FFF8, 0x0FFF8, 0x0FFF8, 0x7FF0, 0x7FF0, 0x3FE0, 0x0F80,
|
||||
0x07C0, 0x1FF0, 0x3FF8, 0x7FFC, 0x7FFC, 0x0FFFE, 0x0FFFE, 0x0FFFE, 0x0FFFE, 0x0FFFE, 0x7FFC, 0x7FFC, 0x3FF8, 0x1FF0, 0x07C0
|
||||
};
|
||||
|
||||
uint16 circle_word;
|
||||
const uint16 *circle_ptr;
|
||||
uint16 counter;
|
||||
uint16 pen_width = 0;
|
||||
int pen_final_x = 0;
|
||||
int pen_final_y = 0;
|
||||
|
||||
uint8 t = 0;
|
||||
uint8 temp8;
|
||||
uint16 temp16;
|
||||
|
||||
int pen_x = x;
|
||||
int pen_y = y;
|
||||
uint16 pen_size = (_patCode & 0x07);
|
||||
|
||||
circle_ptr = &circle_data[circle_list[pen_size]];
|
||||
|
||||
// setup the X position
|
||||
// = pen_x - pen.size/2
|
||||
|
||||
pen_x = (pen_x * 2) - pen_size;
|
||||
if (pen_x < 0) pen_x = 0;
|
||||
|
||||
temp16 = (_width * 2) - (2 * pen_size);
|
||||
if (pen_x >= temp16)
|
||||
pen_x = temp16;
|
||||
|
||||
pen_x /= 2;
|
||||
pen_final_x = pen_x; // original starting point?? -> used in plotrelated
|
||||
|
||||
// Setup the Y Position
|
||||
// = pen_y - pen.size
|
||||
pen_y = pen_y - pen_size;
|
||||
if (pen_y < 0) pen_y = 0;
|
||||
|
||||
temp16 = 167 - (2 * pen_size);
|
||||
if (pen_y >= temp16)
|
||||
pen_y = temp16;
|
||||
|
||||
pen_final_y = pen_y; // used in plotrelated
|
||||
|
||||
t = (uint8)(_patNum | 0x01); // even
|
||||
|
||||
// new purpose for temp16
|
||||
|
||||
temp16 = (pen_size << 1) + 1; // pen size
|
||||
pen_final_y += temp16; // the last row of this shape
|
||||
temp16 = temp16 << 1;
|
||||
pen_width = temp16; // width of shape?
|
||||
|
||||
bool circleCond = ((_patCode & 0x10) != 0);
|
||||
int counterStep = 4;
|
||||
int ditherCond = 0x02;
|
||||
|
||||
for (; pen_y < pen_final_y; pen_y++) {
|
||||
circle_word = *circle_ptr++;
|
||||
|
||||
for (counter = 0; counter <= pen_width; counter += counterStep) {
|
||||
if (circleCond || ((binary_list[counter >> 1] & circle_word) != 0)) {
|
||||
if ((_patCode & 0x20) != 0) {
|
||||
temp8 = t % 2;
|
||||
t = t >> 1;
|
||||
if (temp8 != 0)
|
||||
t = t ^ 0xB8;
|
||||
}
|
||||
|
||||
// == box plot, != circle plot
|
||||
if ((_patCode & 0x20) == 0 || (t & 0x03) == ditherCond)
|
||||
putVirtPixel(pen_x, pen_y);
|
||||
}
|
||||
pen_x++;
|
||||
}
|
||||
|
||||
pen_x = pen_final_x;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* plotBrush
|
||||
*
|
||||
* Plots the current brush pattern.
|
||||
*/
|
||||
void PictureMgr::plotBrush() {
|
||||
for (;;) {
|
||||
if (_patCode & 0x20) {
|
||||
if (!getNextParamByte(_patNum))
|
||||
break;
|
||||
}
|
||||
|
||||
byte x1, y1;
|
||||
if (!getNextCoordinates(x1, y1))
|
||||
break;
|
||||
|
||||
plotPattern(x1, y1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the current picture to the visual and priority screens.
|
||||
*/
|
||||
void PictureMgr::drawPicture() {
|
||||
debugC(kDebugLevelPictures, "Drawing picture %d", _resourceNr);
|
||||
|
||||
_dataOffset = 0;
|
||||
_dataOffsetNibble = false;
|
||||
_patCode = 0;
|
||||
_patNum = 0;
|
||||
_priOn = false;
|
||||
_scrOn = false;
|
||||
_scrColor = 15;
|
||||
_priColor = 4;
|
||||
|
||||
// AGIv3 nibble parameters are indicated by a flag in the picture's directory entry
|
||||
bool nibbleMode = (_vm->_game.dirPic[_resourceNr].flags & RES_PICTURE_V3_NIBBLE_PARM) != 0;
|
||||
|
||||
while (_dataOffset < _dataSize) {
|
||||
byte curByte = getNextByte();
|
||||
|
||||
switch (curByte) {
|
||||
case 0xf0:
|
||||
if (!nibbleMode) {
|
||||
draw_SetColor();
|
||||
} else {
|
||||
draw_SetNibbleColor();
|
||||
}
|
||||
_scrOn = true;
|
||||
break;
|
||||
case 0xf1:
|
||||
_scrOn = false;
|
||||
break;
|
||||
case 0xf2:
|
||||
if (!nibbleMode) {
|
||||
draw_SetPriority();
|
||||
} else {
|
||||
draw_SetNibblePriority();
|
||||
}
|
||||
_priOn = true;
|
||||
break;
|
||||
case 0xf3:
|
||||
_priOn = false;
|
||||
break;
|
||||
case 0xf4:
|
||||
yCorner();
|
||||
break;
|
||||
case 0xf5:
|
||||
xCorner();
|
||||
break;
|
||||
case 0xf6:
|
||||
draw_LineAbsolute();
|
||||
break;
|
||||
case 0xf7:
|
||||
draw_LineShort();
|
||||
break;
|
||||
case 0xf8:
|
||||
draw_Fill();
|
||||
break;
|
||||
case 0xf9:
|
||||
_patCode = getNextByte();
|
||||
break;
|
||||
case 0xfa:
|
||||
plotBrush();
|
||||
break;
|
||||
case 0xff: // end of data
|
||||
return;
|
||||
default:
|
||||
warning("Unknown picture opcode %02x at %04x", curByte, _dataOffset - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the current AGI256 picture to the visual screen.
|
||||
*/
|
||||
void PictureMgr::drawPicture_AGI256() {
|
||||
const uint32 maxFlen = _width * _height;
|
||||
int16 x = 0;
|
||||
int16 y = 0;
|
||||
byte *dataPtr = _data;
|
||||
byte *dataEndPtr = _data + _dataSize;
|
||||
|
||||
debugC(kDebugLevelPictures, "Drawing AGI256 picture");
|
||||
|
||||
while (dataPtr < dataEndPtr) {
|
||||
byte color = *dataPtr++;
|
||||
_gfx->putPixel(x, y, GFX_SCREEN_MASK_VISUAL, color, 0);
|
||||
|
||||
x++;
|
||||
if (x >= _width) {
|
||||
x = 0;
|
||||
y++;
|
||||
if (y >= _height) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_dataSize < maxFlen) {
|
||||
warning("Undersized AGI256 picture resource %d, using it anyway. Filling rest with white", _resourceNr);
|
||||
while (_dataSize < maxFlen) {
|
||||
x++;
|
||||
if (x >= _width) {
|
||||
x = 0;
|
||||
y++;
|
||||
if (y >= _height)
|
||||
break;
|
||||
}
|
||||
_gfx->putPixel(x, y, GFX_SCREEN_MASK_VISUAL, 15, 0);
|
||||
}
|
||||
} else if (_dataSize > maxFlen)
|
||||
warning("Oversized AGI256 picture resource %d, decoding only %ux%u part of it", _resourceNr, _width, _height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visual screen color to the next byte in the picture data.
|
||||
*/
|
||||
void PictureMgr::draw_SetColor() {
|
||||
_scrColor = getNextByte();
|
||||
|
||||
// For CGA, replace the color with its mixture color
|
||||
if (_vm->_renderMode == Common::kRenderCGA) {
|
||||
_scrColor = _gfx->getCGAMixtureColor(_scrColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the priority screen color to the next byte in the picture data.
|
||||
*/
|
||||
void PictureMgr::draw_SetPriority() {
|
||||
_priColor = getNextByte();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visual screen color to the next nibble in the picture data.
|
||||
* Used in AGIv3 to compress the set-color instructions when the flag
|
||||
* RES_PICTURE_V3_NIBBLE_PARM is set in the picture's directory entry.
|
||||
*/
|
||||
void PictureMgr::draw_SetNibbleColor() {
|
||||
_scrColor = getNextNibble();
|
||||
|
||||
// For CGA, replace the color with its mixture color
|
||||
if (_vm->_renderMode == Common::kRenderCGA) {
|
||||
_scrColor = _gfx->getCGAMixtureColor(_scrColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the priority screen color to the next nibble in the picture data.
|
||||
* Used in AGIv3 to compress the set-color instructions when the flag
|
||||
* RES_PICTURE_V3_NIBBLE_PARM is set in the picture's directory entry.
|
||||
*/
|
||||
void PictureMgr::draw_SetNibblePriority() {
|
||||
_priColor = getNextNibble();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a horizontal, vertical, or diagonal line.
|
||||
*
|
||||
* This routine is originally from Sarien. Original comment:
|
||||
*
|
||||
* A line drawing routine sent by Joshua Neal, modified by Stuart George
|
||||
* (fixed >>2 to >>1 and some other bugs like x1 instead of y1, etc.)
|
||||
*/
|
||||
void PictureMgr::draw_Line(int16 x1, int16 y1, int16 x2, int16 y2) {
|
||||
x1 = CLIP<int16>(x1, 0, _width - 1);
|
||||
x2 = CLIP<int16>(x2, 0, _width - 1);
|
||||
y1 = CLIP<int16>(y1, 0, _height - 1);
|
||||
y2 = CLIP<int16>(y2, 0, _height - 1);
|
||||
|
||||
// Vertical line
|
||||
|
||||
if (x1 == x2) {
|
||||
if (y1 > y2) {
|
||||
SWAP(y1, y2);
|
||||
}
|
||||
|
||||
for (; y1 <= y2; y1++)
|
||||
putVirtPixel(x1, y1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Horizontal line
|
||||
|
||||
if (y1 == y2) {
|
||||
if (x1 > x2) {
|
||||
SWAP(x1, x2);
|
||||
}
|
||||
for (; x1 <= x2; x1++)
|
||||
putVirtPixel(x1, y1);
|
||||
return;
|
||||
}
|
||||
|
||||
int stepX = 1;
|
||||
int deltaX = x2 - x1;
|
||||
if (deltaX < 0) {
|
||||
stepX = -1;
|
||||
deltaX = -deltaX;
|
||||
}
|
||||
|
||||
int stepY = 1;
|
||||
int deltaY = y2 - y1;
|
||||
if (deltaY < 0) {
|
||||
stepY = -1;
|
||||
deltaY = -deltaY;
|
||||
}
|
||||
|
||||
int i, detdelta, errorX, errorY;
|
||||
if (deltaY > deltaX) {
|
||||
i = deltaY;
|
||||
detdelta = deltaY;
|
||||
errorX = deltaY / 2;
|
||||
errorY = 0;
|
||||
} else {
|
||||
i = deltaX;
|
||||
detdelta = deltaX;
|
||||
errorX = 0;
|
||||
errorY = deltaX / 2;
|
||||
}
|
||||
|
||||
int x = x1;
|
||||
int y = y1;
|
||||
putVirtPixel(x, y);
|
||||
|
||||
do {
|
||||
errorY += deltaY;
|
||||
if (errorY >= detdelta) {
|
||||
errorY -= detdelta;
|
||||
y += stepY;
|
||||
}
|
||||
|
||||
errorX += deltaX;
|
||||
if (errorX >= detdelta) {
|
||||
errorX -= detdelta;
|
||||
x += stepX;
|
||||
}
|
||||
|
||||
putVirtPixel(x, y);
|
||||
i--;
|
||||
} while (i > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* draw_LineShort
|
||||
*
|
||||
* Draws short lines between positions in relative coordinates.
|
||||
*/
|
||||
void PictureMgr::draw_LineShort() {
|
||||
byte x1, y1, disp;
|
||||
|
||||
if (!getNextCoordinates(x1, y1))
|
||||
return;
|
||||
|
||||
putVirtPixel(x1, y1);
|
||||
|
||||
for (;;) {
|
||||
if (!getNextParamByte(disp))
|
||||
break;
|
||||
|
||||
int dx = ((disp & 0xf0) >> 4) & 0x0f;
|
||||
int dy = (disp & 0x0f);
|
||||
|
||||
if (dx & 0x08)
|
||||
dx = -(dx & 0x07);
|
||||
if (dy & 0x08)
|
||||
dy = -(dy & 0x07);
|
||||
|
||||
draw_Line(x1, y1, x1 + dx, y1 + dy);
|
||||
x1 += dx;
|
||||
y1 += dy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* draw_LineAbsolute
|
||||
*
|
||||
* Draws lines between positions in absolute coordinates.
|
||||
*/
|
||||
void PictureMgr::draw_LineAbsolute() {
|
||||
byte x1, y1, x2, y2;
|
||||
|
||||
if (!getNextCoordinates(x1, y1))
|
||||
return;
|
||||
|
||||
putVirtPixel(x1, y1);
|
||||
|
||||
for (;;) {
|
||||
if (!getNextCoordinates(x2, y2))
|
||||
break;
|
||||
|
||||
draw_Line(x1, y1, x2, y2);
|
||||
x1 = x2;
|
||||
y1 = y2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood fills from a series of start positions.
|
||||
*/
|
||||
void PictureMgr::draw_Fill() {
|
||||
byte x, y;
|
||||
|
||||
while (getNextCoordinates(x, y)) {
|
||||
draw_Fill(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood fills from a start position.
|
||||
*/
|
||||
void PictureMgr::draw_Fill(int16 x, int16 y) {
|
||||
if (!_scrOn && !_priOn)
|
||||
return;
|
||||
|
||||
// Push initial pixel on the stack
|
||||
Common::Stack<Common::Point> stack;
|
||||
stack.push(Common::Point(x, y));
|
||||
|
||||
// Exit if stack is empty
|
||||
while (!stack.empty()) {
|
||||
Common::Point p = stack.pop();
|
||||
|
||||
if (!draw_FillCheck(p.x, p.y, false))
|
||||
continue;
|
||||
|
||||
// Scan for left border
|
||||
uint c;
|
||||
for (c = p.x - 1; draw_FillCheck(c, p.y, true); c--)
|
||||
;
|
||||
|
||||
bool newspanUp = true;
|
||||
bool newspanDown = true;
|
||||
for (c++; draw_FillCheck(c, p.y, true); c++) {
|
||||
putVirtPixel(c, p.y);
|
||||
if (draw_FillCheck(c, p.y - 1, false)) {
|
||||
if (newspanUp) {
|
||||
stack.push(Common::Point(c, p.y - 1));
|
||||
newspanUp = false;
|
||||
}
|
||||
} else {
|
||||
newspanUp = true;
|
||||
}
|
||||
|
||||
if (draw_FillCheck(c, p.y + 1, false)) {
|
||||
if (newspanDown) {
|
||||
stack.push(Common::Point(c, p.y + 1));
|
||||
newspanDown = false;
|
||||
}
|
||||
} else {
|
||||
newspanDown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if flood fill is allowed at a position.
|
||||
*
|
||||
* horizontalCheck indicates if the flood fill algorithm is scanning the current
|
||||
* line horizontally for a boundary. This is used by PictureMgr_Troll to handle
|
||||
* Troll's Tale custom flood fill behavior when drawing the Troll over pictures.
|
||||
*/
|
||||
bool PictureMgr::draw_FillCheck(int16 x, int16 y, bool horizontalCheck) {
|
||||
if (!getGraphicsCoordinates(x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte screenColor = _gfx->getColor(x, y);
|
||||
byte screenPriority = _gfx->getPriority(x, y);
|
||||
|
||||
if (!_priOn && _scrOn && _scrColor != 15)
|
||||
return (screenColor == 15);
|
||||
|
||||
if (_priOn && !_scrOn && _priColor != 4)
|
||||
return (screenPriority == 4);
|
||||
|
||||
return (_scrOn && screenColor == 15 && _scrColor != 15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a picture by resource number to the visual and control screens.
|
||||
* This interface is used by AGI games and GAL (KQ1 early).
|
||||
*
|
||||
* The picture resource must already be loaded. This function sets the current
|
||||
* picture and optionally clears the screens before drawing.
|
||||
*/
|
||||
void PictureMgr::decodePicture(int16 resourceNr, bool clearScreen, bool agi256, int16 width, int16 height) {
|
||||
_resourceNr = resourceNr;
|
||||
_data = _vm->_game.pictures[resourceNr].rdata;
|
||||
_dataSize = _vm->_game.dirPic[resourceNr].len;
|
||||
_width = width;
|
||||
_height = height;
|
||||
|
||||
if (clearScreen) {
|
||||
_gfx->clear(15, getInitialPriorityColor()); // white, priority 4 or 1
|
||||
}
|
||||
|
||||
if (!agi256) {
|
||||
drawPicture();
|
||||
} else {
|
||||
drawPicture_AGI256();
|
||||
}
|
||||
|
||||
if (clearScreen) {
|
||||
_vm->clearImageStack();
|
||||
}
|
||||
_vm->recordImageStackCall(ADD_PIC, resourceNr, clearScreen, agi256, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a picture from a buffer to the visual and control screens.
|
||||
* This interface is used by PreAGI games.
|
||||
*
|
||||
* This function sets the current picture and optionally clears the screens
|
||||
* before drawing.
|
||||
*/
|
||||
void PictureMgr::decodePictureFromBuffer(byte *data, uint32 length, bool clearScreen, int16 width, int16 height) {
|
||||
_data = data;
|
||||
_dataSize = length;
|
||||
_width = width;
|
||||
_height = height;
|
||||
|
||||
if (clearScreen) {
|
||||
_gfx->clear(15, getInitialPriorityColor()); // white, priority 4 or 1
|
||||
}
|
||||
|
||||
drawPicture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a drawn picture from the active screen to the display screen.
|
||||
*
|
||||
* The active screen is usually the visual screen, but this can be toggled
|
||||
* to the priority screen in debug modes.
|
||||
*/
|
||||
void PictureMgr::showPicture(int16 x, int16 y, int16 width, int16 height) {
|
||||
debugC(kDebugLevelPictures, "Show picture");
|
||||
|
||||
_gfx->render_Block(x, y, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a drawn picture from the active screen to the display screen
|
||||
* with transition effects. The effect is determined by the render mode.
|
||||
*
|
||||
* The active screen is usually the visual screen, but this can be toggled
|
||||
* to the priority screen in debug modes.
|
||||
*/
|
||||
void PictureMgr::showPictureWithTransition() {
|
||||
_width = SCRIPT_WIDTH;
|
||||
_height = SCRIPT_HEIGHT;
|
||||
|
||||
debugC(kDebugLevelPictures, "Show picture");
|
||||
|
||||
if (!_vm->_game.automaticRestoreGame) {
|
||||
// only do transitions when we are not restoring a saved game
|
||||
|
||||
if (!_vm->_game.gfxMode) {
|
||||
// if we are not yet in graphics mode, set graphics mode palette now
|
||||
// TODO: maybe change text mode to use different colors for drawing
|
||||
// so that we don't have to change palettes at all
|
||||
_gfx->setPalette(true);
|
||||
}
|
||||
|
||||
switch (_vm->_renderMode) {
|
||||
case Common::kRenderAmiga:
|
||||
case Common::kRenderApple2GS:
|
||||
// Platform Amiga/Apple II GS -> render and do Amiga transition
|
||||
_gfx->render_Block(0, 0, SCRIPT_WIDTH, SCRIPT_HEIGHT, false);
|
||||
_gfx->transition_Amiga();
|
||||
return;
|
||||
case Common::kRenderAtariST:
|
||||
// Platform Atari ST used a different transition, looks "high-res" (full 320x168)
|
||||
_gfx->render_Block(0, 0, SCRIPT_WIDTH, SCRIPT_HEIGHT, false);
|
||||
_gfx->transition_AtariSt();
|
||||
return;
|
||||
default:
|
||||
// Platform PC -> render directly
|
||||
// Macintosh AGI also doesn't seem to have any transitions
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_gfx->render_Block(0, 0, SCRIPT_WIDTH, SCRIPT_HEIGHT);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
124
engines/agi/picture.h
Normal file
124
engines/agi/picture.h
Normal file
@@ -0,0 +1,124 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PICTURE_H
|
||||
#define AGI_PICTURE_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define _DEFAULT_WIDTH 160
|
||||
#define _DEFAULT_HEIGHT 168
|
||||
|
||||
/**
|
||||
* AGI picture resource.
|
||||
*/
|
||||
struct AgiPicture {
|
||||
uint32 flen; /**< size of raw data */
|
||||
uint8 *rdata; /**< raw vector image data */
|
||||
|
||||
void reset() {
|
||||
flen = 0;
|
||||
rdata = nullptr;
|
||||
}
|
||||
|
||||
AgiPicture() { reset(); }
|
||||
};
|
||||
|
||||
class AgiBase;
|
||||
class GfxMgr;
|
||||
|
||||
class PictureMgr {
|
||||
public:
|
||||
PictureMgr(AgiBase *agi, GfxMgr *gfx);
|
||||
virtual ~PictureMgr() { }
|
||||
|
||||
int16 getResourceNr() const { return _resourceNr; };
|
||||
|
||||
protected:
|
||||
virtual byte getInitialPriorityColor() const { return 4; }
|
||||
|
||||
void putVirtPixel(int16 x, int16 y);
|
||||
void xCorner(bool skipOtherCoords = false);
|
||||
void yCorner(bool skipOtherCoords = false);
|
||||
virtual void plotPattern(byte x, byte y);
|
||||
virtual void plotBrush();
|
||||
|
||||
byte getNextByte();
|
||||
bool getNextParamByte(byte &b);
|
||||
byte getNextNibble();
|
||||
|
||||
virtual bool getNextXCoordinate(byte &x);
|
||||
virtual bool getNextYCoordinate(byte &y);
|
||||
bool getNextCoordinates(byte &x, byte &y);
|
||||
|
||||
virtual bool getGraphicsCoordinates(int16 &x, int16 &y);
|
||||
|
||||
public:
|
||||
void decodePicture(int16 resourceNr, bool clearScreen, bool agi256 = false, int16 width = _DEFAULT_WIDTH, int16 height = _DEFAULT_HEIGHT);
|
||||
void decodePictureFromBuffer(byte *data, uint32 length, bool clearScreen, int16 width = _DEFAULT_WIDTH, int16 height = _DEFAULT_HEIGHT);
|
||||
|
||||
protected:
|
||||
virtual void drawPicture();
|
||||
void drawPicture_AGI256();
|
||||
|
||||
void draw_SetColor();
|
||||
void draw_SetPriority();
|
||||
void draw_SetNibbleColor();
|
||||
void draw_SetNibblePriority();
|
||||
|
||||
virtual void draw_Line(int16 x1, int16 y1, int16 x2, int16 y2);
|
||||
void draw_LineShort();
|
||||
void draw_LineAbsolute();
|
||||
|
||||
virtual bool draw_FillCheck(int16 x, int16 y, bool horizontalCheck);
|
||||
virtual void draw_Fill(int16 x, int16 y);
|
||||
virtual void draw_Fill();
|
||||
|
||||
public:
|
||||
void showPicture(int16 x = 0, int16 y = 0, int16 width = _DEFAULT_WIDTH, int16 height = _DEFAULT_HEIGHT);
|
||||
void showPictureWithTransition();
|
||||
|
||||
protected:
|
||||
AgiBase *_vm;
|
||||
GfxMgr *_gfx;
|
||||
|
||||
int16 _resourceNr;
|
||||
uint8 *_data;
|
||||
uint32 _dataSize;
|
||||
uint32 _dataOffset;
|
||||
bool _dataOffsetNibble;
|
||||
|
||||
uint8 _patCode;
|
||||
uint8 _patNum;
|
||||
uint8 _priOn;
|
||||
uint8 _scrOn;
|
||||
uint8 _scrColor;
|
||||
uint8 _priColor;
|
||||
|
||||
uint8 _minCommand;
|
||||
|
||||
int16 _width;
|
||||
int16 _height;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_PICTURE_H */
|
||||
267
engines/agi/picture_gal.cpp
Normal file
267
engines/agi/picture_gal.cpp
Normal file
@@ -0,0 +1,267 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/picture.h"
|
||||
|
||||
#include "agi/picture_gal.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// PictureMgr_GAL decodes and draws picture resources in early King's Quest 1.
|
||||
//
|
||||
// This "Game Adaptation Language" format was used in PC Booters and Apple II.
|
||||
//
|
||||
// This format supports lines and flood fills, and visual and priority screens.
|
||||
//
|
||||
// As this is the format that evolved into AGI, it is quite similar apart from
|
||||
// the specific opcodes. The major difference is the line drawing routine;
|
||||
// it produces different results than AGI. Flood fills implicitly rely on this.
|
||||
// When KQ1 was ported to AGI, the new lines prevented some fills from working,
|
||||
// so they just added more. There are still a few white pixels they missed.
|
||||
//
|
||||
// As with Troll's Tale, room pictures depend on the game first drawing a frame
|
||||
// within the entire picture area. When KQ1 was converted to AGI, this frame
|
||||
// was added to each individual picture resource.
|
||||
|
||||
PictureMgr_GAL::PictureMgr_GAL(AgiBase *agi, GfxMgr *gfx) :
|
||||
PictureMgr(agi, gfx) {
|
||||
}
|
||||
|
||||
void PictureMgr_GAL::drawPicture() {
|
||||
debugC(kDebugLevelPictures, "Drawing picture %d", _resourceNr);
|
||||
|
||||
drawBlackFrame();
|
||||
|
||||
_dataOffset = 0;
|
||||
_dataOffsetNibble = false;
|
||||
_patCode = 0;
|
||||
_patNum = 0;
|
||||
_priOn = true; // initially off in AGI
|
||||
_scrOn = false;
|
||||
_scrColor = 15;
|
||||
_priColor = 1;
|
||||
|
||||
// GAL toggles the current screen between visual and priority
|
||||
// with opcode F0. This affects opcodes F4-F7, but the rest of
|
||||
// the opcodes are explicit about which screen(s) they draw to.
|
||||
byte prevScrOn = _scrOn;
|
||||
byte prevPriOn = _priOn;
|
||||
|
||||
while (_dataOffset < _dataSize) {
|
||||
byte curByte = getNextByte();
|
||||
|
||||
switch (curByte) {
|
||||
case 0xf0: // toggle current screen
|
||||
draw_SetScreens(!_scrOn, !_priOn);
|
||||
break;
|
||||
case 0xf1:
|
||||
draw_SetColor();
|
||||
break;
|
||||
case 0xf2:
|
||||
draw_SetPriority();
|
||||
break;
|
||||
case 0xf3:
|
||||
draw_SetColor();
|
||||
draw_SetPriority();
|
||||
break;
|
||||
|
||||
// Line operations drawn to both visual and priority screens
|
||||
case 0xf4:
|
||||
draw_SetScreens(true, true, prevScrOn, prevPriOn);
|
||||
yCorner();
|
||||
draw_SetScreens(prevScrOn, prevPriOn);
|
||||
break;
|
||||
case 0xf5:
|
||||
draw_SetScreens(true, true, prevScrOn, prevPriOn);
|
||||
xCorner();
|
||||
draw_SetScreens(prevScrOn, prevPriOn);
|
||||
break;
|
||||
case 0xf6:
|
||||
draw_SetScreens(true, true, prevScrOn, prevPriOn);
|
||||
draw_LineAbsolute();
|
||||
draw_SetScreens(prevScrOn, prevPriOn);
|
||||
break;
|
||||
case 0xf7:
|
||||
draw_SetScreens(true, true, prevScrOn, prevPriOn);
|
||||
draw_LineShort();
|
||||
draw_SetScreens(prevScrOn, prevPriOn);
|
||||
break;
|
||||
|
||||
// Line operations drawn to the current screen
|
||||
case 0xf8:
|
||||
yCorner();
|
||||
break;
|
||||
case 0xf9:
|
||||
xCorner();
|
||||
break;
|
||||
case 0xfa:
|
||||
draw_LineAbsolute();
|
||||
break;
|
||||
case 0xfb:
|
||||
draw_LineShort();
|
||||
break;
|
||||
|
||||
// Fill operations drawn to one or both screens
|
||||
case 0xfc:
|
||||
draw_SetScreens(true, true, prevScrOn, prevPriOn);
|
||||
draw_SetColor();
|
||||
draw_SetPriority();
|
||||
draw_Fill();
|
||||
draw_SetScreens(prevScrOn, prevPriOn);
|
||||
break;
|
||||
case 0xfd:
|
||||
draw_SetScreens(false, true, prevScrOn, prevPriOn);
|
||||
draw_SetPriority();
|
||||
draw_Fill();
|
||||
draw_SetScreens(prevScrOn, prevPriOn);
|
||||
break;
|
||||
case 0xfe:
|
||||
draw_SetScreens(true, false, prevScrOn, prevPriOn);
|
||||
draw_SetColor();
|
||||
draw_Fill();
|
||||
draw_SetScreens(prevScrOn, prevPriOn);
|
||||
break;
|
||||
|
||||
case 0xff: // end of data
|
||||
return;
|
||||
default:
|
||||
warning("Unknown picture opcode %02x at %04x", curByte, _dataOffset - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status of the visual and priority screens.
|
||||
*/
|
||||
void PictureMgr_GAL::draw_SetScreens(byte scrOn, byte priOn) {
|
||||
_scrOn = scrOn;
|
||||
_priOn = priOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status of the visual and priority screens,
|
||||
* and returns their previous values.
|
||||
*/
|
||||
void PictureMgr_GAL::draw_SetScreens(byte scrOn, byte priOn, byte &prevScrOn, byte &prevPriOn) {
|
||||
prevScrOn = _scrOn;
|
||||
prevPriOn = _priOn;
|
||||
_scrOn = scrOn;
|
||||
_priOn = priOn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a hard-coded black frame in both screens.
|
||||
* All room pictures require this to draw correctly.
|
||||
*
|
||||
* Original data: F3 00 00 F5 00 00 9F A7 00 00 FF
|
||||
*/
|
||||
void PictureMgr_GAL::drawBlackFrame() {
|
||||
_scrOn = true;
|
||||
_scrColor = 0;
|
||||
_priOn = true;
|
||||
_priColor = 0;
|
||||
draw_Line(0, 0, _width - 1, 0);
|
||||
draw_Line(_width - 1, 0, _width - 1, _height - 1);
|
||||
draw_Line(_width - 1, _height - 1, 0, _height - 1);
|
||||
draw_Line(0, _height - 1, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a horizontal, vertical, or diagonal line using the GAL drawing routine.
|
||||
*
|
||||
* This routine produces different diagonal lines than the AGI routine.
|
||||
*/
|
||||
void PictureMgr_GAL::draw_Line(int16 x1, int16 y1, int16 x2, int16 y2) {
|
||||
x1 = CLIP<int16>(x1, 0, _width - 1);
|
||||
x2 = CLIP<int16>(x2, 0, _width - 1);
|
||||
y1 = CLIP<int16>(y1, 0, _height - 1);
|
||||
y2 = CLIP<int16>(y2, 0, _height - 1);
|
||||
|
||||
const byte width = (x2 > x1) ? (x2 - x1) : (x1 - x2);
|
||||
const byte height = (y2 > y1) ? (y2 - y1) : (y1 - y2);
|
||||
|
||||
byte x = 0;
|
||||
byte y = 0;
|
||||
if (width > height) {
|
||||
while (x != width) {
|
||||
x++;
|
||||
y = (x * height) / width;
|
||||
if (((x * height) % width) * 2 > width) {
|
||||
y++;
|
||||
}
|
||||
|
||||
byte pixelX = (x2 > x1) ? (x1 + x) : (x1 - x);
|
||||
byte pixelY = (y2 > y1) ? (y1 + y) : (y1 - y);
|
||||
putVirtPixel(pixelX, pixelY);
|
||||
}
|
||||
} else {
|
||||
while (y != height) {
|
||||
y++;
|
||||
x = (y * width) / height;
|
||||
if (((y * width) % height) * 2 > height) {
|
||||
x++;
|
||||
}
|
||||
|
||||
byte pixelX = (x2 > x1) ? (x1 + x) : (x1 - x);
|
||||
byte pixelY = (y2 > y1) ? (y1 + y) : (y1 - y);
|
||||
putVirtPixel(pixelX, pixelY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next x coordinate in the current picture instruction,
|
||||
* and clip it to the picture width.
|
||||
*/
|
||||
bool PictureMgr_GAL::getNextXCoordinate(byte &x) {
|
||||
if (!getNextParamByte(x)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x >= _width) { // 160
|
||||
debugC(kDebugLevelPictures, "clipping %c from %d to %d", 'x', x, _width - 1);
|
||||
x = _width - 1; // 159
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next y coordinate in the current picture instruction,
|
||||
* and clip it to the picture height.
|
||||
*/
|
||||
bool PictureMgr_GAL::getNextYCoordinate(byte &y) {
|
||||
if (!getNextParamByte(y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (y >= _height) { // 168
|
||||
debugC(kDebugLevelPictures, "clipping %c from %d to %d", 'y', y, _height);
|
||||
y = _height - 1; // 167
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
50
engines/agi/picture_gal.h
Normal file
50
engines/agi/picture_gal.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PICTURE_GAL_H
|
||||
#define AGI_PICTURE_GAL_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class PictureMgr_GAL : public PictureMgr {
|
||||
public:
|
||||
PictureMgr_GAL(AgiBase *agi, GfxMgr *gfx);
|
||||
|
||||
protected:
|
||||
byte getInitialPriorityColor() const override { return 1; }
|
||||
|
||||
void drawPicture() override;
|
||||
|
||||
void draw_Line(int16 x1, int16 y1, int16 x2, int16 y2) override;
|
||||
|
||||
bool getNextXCoordinate(byte &x) override;
|
||||
bool getNextYCoordinate(byte &y) override;
|
||||
|
||||
private:
|
||||
void draw_SetScreens(byte scrOn, byte priOn);
|
||||
void draw_SetScreens(byte scrOn, byte priOn, byte &prevScrOn, byte &prevPriOn);
|
||||
|
||||
void drawBlackFrame();
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif
|
||||
2322
engines/agi/preagi/mickey.cpp
Normal file
2322
engines/agi/preagi/mickey.cpp
Normal file
File diff suppressed because it is too large
Load Diff
768
engines/agi/preagi/mickey.h
Normal file
768
engines/agi/preagi/mickey.h
Normal file
@@ -0,0 +1,768 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PREAGI_MICKEY_H
|
||||
#define AGI_PREAGI_MICKEY_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define MSA_SAVEGAME_VERSION 2
|
||||
|
||||
// strings
|
||||
#define IDS_MSA_PATH_DAT "dat/%s"
|
||||
#define IDS_MSA_PATH_OBJ "obj/%s.ooo"
|
||||
#define IDS_MSA_PATH_PIC "%d.pic"
|
||||
#define IDS_MSA_PATH_LOGO "logos.bcg"
|
||||
|
||||
#define IDS_MSA_INVENTORY "MICKEY IS CARRYING THE FOLLOWING:"
|
||||
#define IDS_MSA_CRYSTALS "%s CRYSTALS"
|
||||
|
||||
const char IDS_MSA_CRYSTAL_NO[][3] = {
|
||||
"NO", " 1", " 2", " 3", " 4", " 5", " 6", " 7", " 8", " 9"
|
||||
};
|
||||
const char IDS_MSA_TEMP_C[][5] = {
|
||||
" 20 ", " 480", "-200", " 430", "-185", "-230", "-130", "-150", "-215"
|
||||
};
|
||||
const char IDS_MSA_TEMP_F[][5] = {
|
||||
" 68 ", " 897", "-328", " 807", "-301", "-382", "-202", "-238", "-355"
|
||||
};
|
||||
const char IDS_MSA_PLANETS[][10] = {
|
||||
"EARTH. ", "VENUS. ", "TRITON. ", "MERCURY.", "TITAN. ",
|
||||
"PLUTO. ", "IO. ", "MARS. ", "OBERON. "
|
||||
};
|
||||
|
||||
const char IDS_MSA_ERRORS[][40] = {
|
||||
"THAT CANNOT BE UNDERSTOOD",
|
||||
"TRY GOING THERE INSTEAD",
|
||||
"THAT CAN'T BE DONE",
|
||||
"MICKEY WOULDN'T WANT TO DO THAT!",
|
||||
"WHICH DIRECTION?",
|
||||
"THAT DOESN'T MAKE SENSE!",
|
||||
"MICKEY WOULDN'T WANT TO DO THAT!"
|
||||
};
|
||||
|
||||
// patch Mickey.exe offset 0x21E to value 0x01 to enable debug mode
|
||||
|
||||
const char IDS_MSA_INSERT_DISK[][40] = {
|
||||
"Please insert disk 1 and press any key", "Please insert disk 2 and press any key"
|
||||
};
|
||||
|
||||
// max values
|
||||
|
||||
#define IDI_MSA_MAX_PLANET 9
|
||||
#define IDI_MSA_MAX_DAT 10
|
||||
#define IDI_MSA_MAX_PIC_ROOM 224
|
||||
#define IDI_MSA_MAX_ROOM 160
|
||||
|
||||
#define IDI_MSA_MAX_BUTTON 6
|
||||
#define IDI_MSA_MAX_ITEM 11
|
||||
|
||||
#define IDI_MSA_ANIM_DELAY 25
|
||||
|
||||
#define IDI_MSA_LEN_STORY 1372
|
||||
|
||||
// rows
|
||||
|
||||
#define IDI_MSA_ROW_MENU_0 20
|
||||
#define IDI_MSA_ROW_MENU_1 21
|
||||
#define IDI_MSA_ROW_INV_TITLE 2
|
||||
#define IDI_MSA_ROW_INV_CRYSTALS 4
|
||||
#define IDI_MSA_ROW_INV_ITEMS 5
|
||||
#define IDI_MSA_ROW_TEMPERATURE 21
|
||||
#define IDI_MSA_ROW_PLANET 22
|
||||
#define IDI_MSA_ROW_INSERT_DISK 23
|
||||
|
||||
#define IDI_MSA_COL_INV_TITLE 4
|
||||
#define IDI_MSA_COL_INV_ITEMS 15
|
||||
#define IDI_MSA_COL_PLANET 28
|
||||
#define IDI_MSA_COL_INSERT_DISK 1
|
||||
|
||||
// screen
|
||||
|
||||
#define IDI_MSA_PIC_WIDTH 140
|
||||
#define IDI_MSA_PIC_HEIGHT 159
|
||||
#define IDI_MSA_PIC_X0 10
|
||||
#define IDI_MSA_PIC_Y0 0
|
||||
|
||||
// pictures
|
||||
|
||||
#define IDI_MSA_PIC_EARTH_TIRE_SWING 1
|
||||
#define IDI_MSA_PIC_EARTH_TIRE_SWING_1 200 // rope taken, swing on ground
|
||||
#define IDI_MSA_PIC_EARTH_DOGHOUSE 2
|
||||
#define IDI_MSA_PIC_EARTH_IN_DOGHOUSE 154
|
||||
#define IDI_MSA_PIC_EARTH_TREE 3
|
||||
#define IDI_MSA_PIC_EARTH_GARDEN 4
|
||||
#define IDI_MSA_PIC_EARTH_FRONT_HOUSE 5
|
||||
#define IDI_MSA_PIC_EARTH_HAMMOCK 6
|
||||
#define IDI_MSA_PIC_EARTH_BUTTERFLY 7
|
||||
#define IDI_MSA_PIC_EARTH_MAILBOX 8
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_0 9
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_1 10
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_2 11
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_3 12
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_4 13 // starting room
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_5 14
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_6 15
|
||||
#define IDI_MSA_PIC_EARTH_ROAD_7 18
|
||||
#define IDI_MSA_PIC_EARTH_UNDER_TREE 16
|
||||
#define IDI_MSA_PIC_EARTH_UP_IN_TREE 155 // CRYSTAL
|
||||
#define IDI_MSA_PIC_EARTH_SHIP 17
|
||||
#define IDI_MSA_PIC_EARTH_LIVING_ROOM 19
|
||||
#define IDI_MSA_PIC_EARTH_KITCHEN 20
|
||||
#define IDI_MSA_PIC_EARTH_KITCHEN_1 159 // cupboard open
|
||||
#define IDI_MSA_PIC_EARTH_GARAGE 21
|
||||
#define IDI_MSA_PIC_EARTH_GARAGE_1 160 // cabinet open
|
||||
#define IDI_MSA_PIC_EARTH_BEDROOM 22
|
||||
#define IDI_MSA_PIC_EARTH_BEDROOM_1 161 // closet open
|
||||
#define IDI_MSA_PIC_EARTH_BATHROOM 23 // WEIGH MICKEY
|
||||
#define IDI_MSA_PIC_EARTH_SHIP_LEAVING 24
|
||||
#define IDI_MSA_PIC_EARTH_MINNIE 25
|
||||
|
||||
#define IDI_MSA_PIC_SHIP_AIRLOCK 25
|
||||
#define IDI_MSA_PIC_SHIP_AIRLOCK_0 201 // door closed
|
||||
#define IDI_MSA_PIC_SHIP_AIRLOCK_1 202 // door open
|
||||
#define IDI_MSA_PIC_SHIP_AIRLOCK_2 203 // door closed, spacesuits on
|
||||
#define IDI_MSA_PIC_SHIP_AIRLOCK_3 204 // door open, spacesuits on
|
||||
#define IDI_MSA_PIC_SHIP_BEDROOM 29
|
||||
#define IDI_MSA_PIC_SHIP_CONTROLS 26
|
||||
#define IDI_MSA_PIC_SHIP_CORRIDOR 27
|
||||
#define IDI_MSA_PIC_SHIP_KITCHEN 28
|
||||
#define IDI_MSA_PIC_SHIP_KITCHEN_1 172 // cabinet open
|
||||
|
||||
#define IDI_MSA_PIC_SHIP_VENUS 146
|
||||
#define IDI_MSA_PIC_SHIP_NEPTUNE 147
|
||||
#define IDI_MSA_PIC_SHIP_MERCURY 148
|
||||
#define IDI_MSA_PIC_SHIP_SATURN 149
|
||||
#define IDI_MSA_PIC_SHIP_PLUTO 150
|
||||
#define IDI_MSA_PIC_SHIP_JUPITER 151
|
||||
#define IDI_MSA_PIC_SHIP_MARS 152
|
||||
#define IDI_MSA_PIC_SHIP_URANUS 153
|
||||
|
||||
#define IDI_MSA_PIC_VENUS_0 30
|
||||
#define IDI_MSA_PIC_VENUS_1 31
|
||||
#define IDI_MSA_PIC_VENUS_2 32
|
||||
#define IDI_MSA_PIC_VENUS_3 34
|
||||
#define IDI_MSA_PIC_VENUS_4 36
|
||||
#define IDI_MSA_PIC_VENUS_5 38
|
||||
#define IDI_MSA_PIC_VENUS_CHASM 35
|
||||
#define IDI_MSA_PIC_VENUS_CHASM_1 183 // rope lowered
|
||||
#define IDI_MSA_PIC_VENUS_PROBE 39 // CRYSTAL, USE WRENCH
|
||||
#define IDI_MSA_PIC_VENUS_PROBE_1 184 // hatch open
|
||||
#define IDI_MSA_PIC_VENUS_SHIP 33
|
||||
#define IDI_MSA_PIC_VENUS_WEIGH 37 // WEIGH MICKEY
|
||||
|
||||
#define IDI_MSA_PIC_NEPTUNE_0 40
|
||||
#define IDI_MSA_PIC_NEPTUNE_1 42
|
||||
#define IDI_MSA_PIC_NEPTUNE_2 43
|
||||
#define IDI_MSA_PIC_NEPTUNE_3 44
|
||||
#define IDI_MSA_PIC_NEPTUNE_4 45
|
||||
#define IDI_MSA_PIC_NEPTUNE_5 48
|
||||
#define IDI_MSA_PIC_NEPTUNE_6 50
|
||||
#define IDI_MSA_PIC_NEPTUNE_7 52
|
||||
#define IDI_MSA_PIC_NEPTUNE_8 53
|
||||
#define IDI_MSA_PIC_NEPTUNE_9 54
|
||||
#define IDI_MSA_PIC_NEPTUNE_10 55
|
||||
#define IDI_MSA_PIC_NEPTUNE_11 56
|
||||
#define IDI_MSA_PIC_NEPTUNE_BABIES 61
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_0 46
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_1 51
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_2 57
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_3 58
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_4 59
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_5 60
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_6 66
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_7 67
|
||||
#define IDI_MSA_PIC_NEPTUNE_CASTLE_8 68
|
||||
#define IDI_MSA_PIC_NEPTUNE_EATING_AREA 62
|
||||
#define IDI_MSA_PIC_NEPTUNE_ENTRANCE 47
|
||||
#define IDI_MSA_PIC_NEPTUNE_ENTRANCE_1 185 // entrance open
|
||||
#define IDI_MSA_PIC_NEPTUNE_ENTRYWAY 63
|
||||
#define IDI_MSA_PIC_NEPTUNE_GUARD 69
|
||||
#define IDI_MSA_PIC_NEPTUNE_LEADER 64 // CRYSTAL, GIVE SCARF
|
||||
#define IDI_MSA_PIC_NEPTUNE_SHIP 49
|
||||
#define IDI_MSA_PIC_NEPTUNE_SLEEP_AREA 65
|
||||
#define IDI_MSA_PIC_NEPTUNE_WEIGH 41
|
||||
|
||||
#define IDI_MSA_PIC_MERCURY_0 71
|
||||
#define IDI_MSA_PIC_MERCURY_1 73
|
||||
#define IDI_MSA_PIC_MERCURY_2 75
|
||||
#define IDI_MSA_PIC_MERCURY_3 77
|
||||
#define IDI_MSA_PIC_MERCURY_4 80
|
||||
#define IDI_MSA_PIC_MERCURY_ALIEN_0 72 // CRYSTAL, GIVE SUNGLASSES
|
||||
#define IDI_MSA_PIC_MERCURY_ALIEN_1 74
|
||||
#define IDI_MSA_PIC_MERCURY_ALIEN_2 81
|
||||
#define IDI_MSA_PIC_MERCURY_CAVE_0 70 // hidden feature, press '2' here
|
||||
#define IDI_MSA_PIC_MERCURY_CAVE_1 78
|
||||
#define IDI_MSA_PIC_MERCURY_CAVE_2 79
|
||||
#define IDI_MSA_PIC_MERCURY_SHIP 76
|
||||
#define IDI_MSA_PIC_MERCURY_WEIGH 82
|
||||
|
||||
#define IDI_MSA_PIC_SATURN_0 84
|
||||
#define IDI_MSA_PIC_SATURN_1 86
|
||||
#define IDI_MSA_PIC_SATURN_2 90
|
||||
#define IDI_MSA_PIC_SATURN_3 91
|
||||
#define IDI_MSA_PIC_SATURN_ISLAND 89 // CRYSTAL
|
||||
#define IDI_MSA_PIC_SATURN_LAKE_0 85 // USE MATTRESS
|
||||
#define IDI_MSA_PIC_SATURN_LAKE_1 88 // USE MATTRESS
|
||||
#define IDI_MSA_PIC_SATURN_LAKE_2 92 // USE MATTRESS
|
||||
#define IDI_MSA_PIC_SATURN_SHIP 87
|
||||
#define IDI_MSA_PIC_SATURN_WEIGH 83 // WEIGH MICKEY
|
||||
|
||||
#define IDI_MSA_PIC_PLUTO_0 93
|
||||
#define IDI_MSA_PIC_PLUTO_1 96
|
||||
#define IDI_MSA_PIC_PLUTO_2 97
|
||||
#define IDI_MSA_PIC_PLUTO_3 98
|
||||
#define IDI_MSA_PIC_PLUTO_4 101
|
||||
#define IDI_MSA_PIC_PLUTO_ALIENS 100 // CRYSTAL, GIVE BONE
|
||||
#define IDI_MSA_PIC_PLUTO_CAVE_0 99
|
||||
#define IDI_MSA_PIC_PLUTO_CAVE_1 103
|
||||
#define IDI_MSA_PIC_PLUTO_CRATER 102
|
||||
#define IDI_MSA_PIC_PLUTO_SHIP 95
|
||||
#define IDI_MSA_PIC_PLUTO_WEIGH 94 // WEIGH MICKEY
|
||||
|
||||
#define IDI_MSA_PIC_JUPITER_0 106
|
||||
#define IDI_MSA_PIC_JUPITER_1 107
|
||||
#define IDI_MSA_PIC_JUPITER_2 108
|
||||
#define IDI_MSA_PIC_JUPITER_3 109
|
||||
#define IDI_MSA_PIC_JUPITER_4 113
|
||||
#define IDI_MSA_PIC_JUPITER_5 116
|
||||
#define IDI_MSA_PIC_JUPITER_6 117
|
||||
#define IDI_MSA_PIC_JUPITER_7 120
|
||||
#define IDI_MSA_PIC_JUPITER_CRACK 114
|
||||
#define IDI_MSA_PIC_JUPITER_LAVA 110 // CRYSTAL, THROW ROCK
|
||||
#define IDI_MSA_PIC_JUPITER_ROCK_0 112 // GET ROCK
|
||||
#define IDI_MSA_PIC_JUPITER_ROCK_1 119 // GET ROCK
|
||||
#define IDI_MSA_PIC_JUPITER_SHIP 115
|
||||
#define IDI_MSA_PIC_JUPITER_WEIGH 118 // WEIGH MICKEY
|
||||
|
||||
#define IDI_MSA_PIC_MARS_0 121
|
||||
#define IDI_MSA_PIC_MARS_1 124
|
||||
#define IDI_MSA_PIC_MARS_2 125
|
||||
#define IDI_MSA_PIC_MARS_3 126
|
||||
#define IDI_MSA_PIC_MARS_4 127
|
||||
#define IDI_MSA_PIC_MARS_5 128
|
||||
#define IDI_MSA_PIC_MARS_6 130
|
||||
#define IDI_MSA_PIC_MARS_SHIP 123
|
||||
#define IDI_MSA_PIC_MARS_TUBE_0 129
|
||||
#define IDI_MSA_PIC_MARS_TUBE_1 131
|
||||
#define IDI_MSA_PIC_MARS_VOLCANO 132 // CRYSTAL, DIG PLUTO
|
||||
#define IDI_MSA_PIC_MARS_WEIGH 122 // WEIGH MICKEY
|
||||
|
||||
#define IDI_MSA_PIC_URANUS_0 133
|
||||
#define IDI_MSA_PIC_URANUS_1 134
|
||||
#define IDI_MSA_PIC_URANUS_2 135
|
||||
#define IDI_MSA_PIC_URANUS_3 138
|
||||
#define IDI_MSA_PIC_URANUS_4 139
|
||||
#define IDI_MSA_PIC_URANUS_5 140
|
||||
#define IDI_MSA_PIC_URANUS_6 142
|
||||
#define IDI_MSA_PIC_URANUS_CHAMBER 145 // CRYSTAL, USE CROWBAR
|
||||
#define IDI_MSA_PIC_URANUS_SHIP 137
|
||||
#define IDI_MSA_PIC_URANUS_STEPS 144
|
||||
#define IDI_MSA_PIC_URANUS_ENTRANCE 141 // ENTER TEMPLE
|
||||
#define IDI_MSA_PIC_URANUS_TEMPLE 143 // USE CRYSTAL, ENTER DOOR
|
||||
#define IDI_MSA_PIC_URANUS_TEMPLE_1 206 // crystal used
|
||||
#define IDI_MSA_PIC_URANUS_TEMPLE_2 207 // door open
|
||||
#define IDI_MSA_PIC_URANUS_WEIGH 136 // WEIGH MICKEY
|
||||
|
||||
#define IDI_MSA_PIC_STAR_MAP 165
|
||||
#define IDI_MSA_PIC_TITLE 240
|
||||
|
||||
// objects
|
||||
|
||||
enum ENUM_MSA_OBJECT {
|
||||
IDI_MSA_OBJECT_NONE = -1,
|
||||
IDI_MSA_OBJECT_ROCK_0,
|
||||
IDI_MSA_OBJECT_WRENCH,
|
||||
IDI_MSA_OBJECT_SCALE,
|
||||
IDI_MSA_OBJECT_CROWBAR,
|
||||
IDI_MSA_OBJECT_BONE,
|
||||
IDI_MSA_OBJECT_SUNGLASSES,
|
||||
IDI_MSA_OBJECT_DESK_STUFF,
|
||||
IDI_MSA_OBJECT_MATTRESS,
|
||||
IDI_MSA_OBJECT_SCARF,
|
||||
IDI_MSA_OBJECT_FLASHLIGHT,
|
||||
IDI_MSA_OBJECT_ROPE,
|
||||
IDI_MSA_OBJECT_ROCK_1,
|
||||
IDI_MSA_OBJECT_SCARF_C64,
|
||||
IDI_MSA_OBJECT_ROCK_2,
|
||||
IDI_MSA_OBJECT_ROCK_3,
|
||||
IDI_MSA_OBJECT_W_EARTH,
|
||||
IDI_MSA_OBJECT_W_VENUS,
|
||||
IDI_MSA_OBJECT_W_TRITON,
|
||||
IDI_MSA_OBJECT_W_MERCURY,
|
||||
IDI_MSA_OBJECT_W_TITAN,
|
||||
IDI_MSA_OBJECT_W_PLUTO,
|
||||
IDI_MSA_OBJECT_W_IO,
|
||||
IDI_MSA_OBJECT_W_MARS,
|
||||
IDI_MSA_OBJECT_W_OBERON,
|
||||
IDI_MSA_OBJECT_W_SPACE,
|
||||
IDI_MSA_OBJECT_XL31,
|
||||
IDI_MSA_OBJECT_XL31E,
|
||||
IDI_MSA_OBJECT_XL32,
|
||||
IDI_MSA_OBJECT_XL32E,
|
||||
IDI_MSA_OBJECT_XL33,
|
||||
IDI_MSA_OBJECT_XL33E,
|
||||
IDI_MSA_OBJECT_CRYSTAL
|
||||
};
|
||||
|
||||
const char IDS_MSA_NAME_OBJ[][9] = {
|
||||
"rok1", "wrench", "scale", "cbar", "bone", "glasses", "deskstuf", "raft",
|
||||
"scarf", "flashlit", "rope", "rok1", "scarfc64", "rok2", "rock35", "earthw",
|
||||
"venw", "trw", "merw", "titw", "plw", "iow", "mrw", "obw", "spw", "xl31",
|
||||
"xl31e", "xl32", "xl32e", "xl33", "xl33e", "crys1"
|
||||
};
|
||||
|
||||
const int IDI_MSA_XTAL_ROOM_XY[IDI_MSA_MAX_PLANET][3] = {
|
||||
// room x y
|
||||
{IDI_MSA_PIC_EARTH_UP_IN_TREE, 14, 76},
|
||||
{IDI_MSA_PIC_VENUS_PROBE, 74, 80},
|
||||
{IDI_MSA_PIC_NEPTUNE_LEADER, 70, 27},
|
||||
{IDI_MSA_PIC_MERCURY_ALIEN_0, 123, 64},
|
||||
{IDI_MSA_PIC_SATURN_ISLAND, 110, 115},
|
||||
{IDI_MSA_PIC_PLUTO_ALIENS, 60, 104},
|
||||
{IDI_MSA_PIC_JUPITER_LAVA, 56, 54},
|
||||
{IDI_MSA_PIC_MARS_VOLCANO, 107, 100},
|
||||
{IDI_MSA_PIC_URANUS_CHAMBER, 90, 4}
|
||||
};
|
||||
|
||||
// planets
|
||||
|
||||
enum ENUM_MSA_PLANET {
|
||||
IDI_MSA_PLANET_EARTH = 0,
|
||||
IDI_MSA_PLANET_VENUS,
|
||||
IDI_MSA_PLANET_NEPTUNE,
|
||||
IDI_MSA_PLANET_MERCURY,
|
||||
IDI_MSA_PLANET_SATURN,
|
||||
IDI_MSA_PLANET_PLUTO,
|
||||
IDI_MSA_PLANET_JUPITER,
|
||||
IDI_MSA_PLANET_MARS,
|
||||
IDI_MSA_PLANET_URANUS,
|
||||
IDI_MSA_PLANET_SPACESHIP
|
||||
};
|
||||
|
||||
const char IDS_MSA_NAME_DAT[][13] = {
|
||||
"earth.dat", "venus.dat", "neptune.dat", "mercury.dat", "saturn.dat",
|
||||
"pluto.dat", "jupiter.dat", "mars.dat", "uranus.dat", "spacship.dat"
|
||||
};
|
||||
|
||||
const char IDS_MSA_NAME_PLANET[][10] = {
|
||||
"EARTH", "VENUS", "TRITON", "MERCURY", "TITAN",
|
||||
"PLUTO", "IO", "MARS", "OBERON"
|
||||
};
|
||||
|
||||
const char IDS_MSA_NAME_PLANET_2[][10] = {
|
||||
"EARTH", "VENUS", "NEPTUNE", "MERCURY", "SATURN",
|
||||
"PLUTO", "JUPITER", "MARS", "URANUS"
|
||||
};
|
||||
|
||||
const char IDS_MSA_ADDR_PLANET[][7] = {
|
||||
"OB", "B", "OOBBB", "O", "OOBB",
|
||||
"OOOBBB", "OBB", "OOB", "OOOBB"
|
||||
};
|
||||
|
||||
const int IDI_MSA_HOME_PLANET[] = {
|
||||
IDI_MSA_PIC_EARTH_SHIP, IDI_MSA_PIC_VENUS_SHIP, IDI_MSA_PIC_NEPTUNE_SHIP, IDI_MSA_PIC_MERCURY_SHIP,
|
||||
IDI_MSA_PIC_SATURN_SHIP, IDI_MSA_PIC_PLUTO_SHIP, IDI_MSA_PIC_JUPITER_SHIP, IDI_MSA_PIC_MARS_SHIP,
|
||||
IDI_MSA_PIC_URANUS_SHIP
|
||||
};
|
||||
|
||||
const int IDI_MSA_SHIP_PLANET[] = {
|
||||
0, IDI_MSA_PIC_SHIP_VENUS, IDI_MSA_PIC_SHIP_NEPTUNE, IDI_MSA_PIC_SHIP_MERCURY, IDI_MSA_PIC_SHIP_SATURN,
|
||||
IDI_MSA_PIC_SHIP_PLUTO, IDI_MSA_PIC_SHIP_JUPITER, IDI_MSA_PIC_SHIP_MARS, IDI_MSA_PIC_SHIP_URANUS
|
||||
};
|
||||
|
||||
// items
|
||||
|
||||
enum ENUM_MSA_ITEM {
|
||||
IDI_MSA_ITEM_FLASHLIGHT = 0,
|
||||
IDI_MSA_ITEM_ROPE,
|
||||
IDI_MSA_ITEM_BONE,
|
||||
IDI_MSA_ITEM_LETTER,
|
||||
IDI_MSA_ITEM_CROWBAR,
|
||||
IDI_MSA_ITEM_WRENCH,
|
||||
IDI_MSA_ITEM_MATTRESS,
|
||||
IDI_MSA_ITEM_SCARF,
|
||||
IDI_MSA_ITEM_SUNGLASSES,
|
||||
IDI_MSA_ITEM_SCALE,
|
||||
IDI_MSA_ITEM_ROCK
|
||||
};
|
||||
|
||||
const char IDS_MSA_NAME_ITEM[][15] = {
|
||||
"A FLASHLIGHT", "A ROPE ", "A BONE ", "A LETTER", "A CROWBAR", "A WRENCH",
|
||||
"A MATTRESS", "A SCARF", "SUNGLASSES", "A SCALE ", "A ROCK "
|
||||
};
|
||||
|
||||
// buttons
|
||||
|
||||
#define IDI_MSA_BUTTON_ORANGE 0x4F // 'O'
|
||||
#define IDI_MSA_BUTTON_BLUE 0x42 // 'B'
|
||||
|
||||
// file structures
|
||||
|
||||
struct MSA_TEXT_ENTRY {
|
||||
uint8 x0;
|
||||
uint8 szText[11];
|
||||
};
|
||||
|
||||
struct MSA_TEXT_BLOCK {
|
||||
uint8 count;
|
||||
MSA_TEXT_ENTRY entry[5];
|
||||
};
|
||||
|
||||
struct MSA_MSG_BLOCK {
|
||||
uint8 data[5];
|
||||
};
|
||||
|
||||
struct MSA_MENU {
|
||||
MSA_TEXT_BLOCK row[2];
|
||||
MSA_MSG_BLOCK cmd[5];
|
||||
MSA_MSG_BLOCK arg[5];
|
||||
};
|
||||
|
||||
struct MSA_DAT_HEADER {
|
||||
uint16 filelen;
|
||||
uint16 ofsRoom[IDI_MSA_MAX_ROOM];
|
||||
uint16 ofsDesc[IDI_MSA_MAX_ROOM];
|
||||
uint16 ofsStr[IDI_MSA_MAX_ROOM];
|
||||
};
|
||||
|
||||
struct MSA_SND_NOTE {
|
||||
uint16 counter; // freq = 1193180 / counter
|
||||
uint8 length; // msec = length / 0.0182
|
||||
};
|
||||
|
||||
// file offset modifiers
|
||||
|
||||
#define IDI_MSA_OFS_DAT 0x0002
|
||||
#define IDI_MSA_OFS_EXE 0x35C0
|
||||
|
||||
// actions
|
||||
|
||||
#define IDI_MSA_ACTION_GOTO_ROOM 0x00
|
||||
#define IDI_MSA_ACTION_SHOW_INT_STR 0x01
|
||||
#define IDI_MSA_ACTION_UNUSED 0x02
|
||||
#define IDI_MSA_ACTION_SHOW_DAT_STR 0x03
|
||||
|
||||
#define IDI_MSA_ACTION_GET_ROPE 0x7F
|
||||
#define IDI_MSA_ACTION_UNTIE_ROPE 0x80
|
||||
#define IDI_MSA_ACTION_GET_BONE 0x81
|
||||
#define IDI_MSA_ACTION_GET_XTAL_EARTH 0x82
|
||||
#define IDI_MSA_ACTION_LOOK_DESK 0x83
|
||||
#define IDI_MSA_ACTION_WRITE_LETTER 0x84
|
||||
#define IDI_MSA_ACTION_MAIL_LETTER 0x85
|
||||
#define IDI_MSA_ACTION_OPEN_CUPBOARD 0x86
|
||||
#define IDI_MSA_ACTION_GET_FLASHLIGHT 0x87
|
||||
#define IDI_MSA_ACTION_OPEN_CABINET 0x88
|
||||
#define IDI_MSA_ACTION_GET_CROWBAR 0x89
|
||||
#define IDI_MSA_ACTION_GET_WRENCH 0x8A
|
||||
#define IDI_MSA_ACTION_OPEN_CLOSET 0x8B
|
||||
#define IDI_MSA_ACTION_GET_MATTRESS 0x8C
|
||||
#define IDI_MSA_ACTION_GET_SCARF 0x8D
|
||||
#define IDI_MSA_ACTION_GET_SUNGLASSES 0x8E
|
||||
#define IDI_MSA_ACTION_GET_SCALE 0x8F
|
||||
#define IDI_MSA_ACTION_GOTO_SPACESHIP 0x90
|
||||
|
||||
#define IDI_MSA_ACTION_DOWN_CHASM 0x91
|
||||
#define IDI_MSA_ACTION_DOWN_ROPE 0x92
|
||||
#define IDI_MSA_ACTION_USE_ROPE 0x93
|
||||
#define IDI_MSA_ACTION_OPEN_HATCH 0x94
|
||||
#define IDI_MSA_ACTION_USE_WRENCH 0x95
|
||||
#define IDI_MSA_ACTION_GET_XTAL_VENUS 0x96
|
||||
|
||||
#define IDI_MSA_ACTION_LOOK_CASTLE 0x97
|
||||
#define IDI_MSA_ACTION_ENTER_OPENING 0x98
|
||||
#define IDI_MSA_ACTION_USE_CROWBAR 0x99
|
||||
#define IDI_MSA_ACTION_GET_XTAL_NEPTUNE 0x9A
|
||||
#define IDI_MSA_ACTION_TALK_LEADER 0x9B
|
||||
#define IDI_MSA_ACTION_GIVE_SCARF 0x9C
|
||||
|
||||
#define IDI_MSA_ACTION_GET_XTAL_MERCURY 0x9D
|
||||
#define IDI_MSA_ACTION_GIVE_SUNGLASSES 0x9E
|
||||
#define IDI_MSA_ACTION_CROSS_LAKE 0x9F
|
||||
#define IDI_MSA_ACTION_USE_MATTRESS 0xA0
|
||||
#define IDI_MSA_ACTION_GET_XTAL_SATURN 0xA1
|
||||
#define IDI_MSA_ACTION_LEAVE_ISLAND 0xA2
|
||||
|
||||
#define IDI_MSA_ACTION_GET_XTAL_PLUTO 0xA3
|
||||
#define IDI_MSA_ACTION_GIVE_BONE 0xA4
|
||||
|
||||
#define IDI_MSA_ACTION_GET_ROCK_0 0xA5
|
||||
#define IDI_MSA_ACTION_GET_ROCK_1 0xA6
|
||||
#define IDI_MSA_ACTION_GET_XTAL_JUPITER 0xA7
|
||||
#define IDI_MSA_ACTION_THROW_ROCK 0xA8
|
||||
|
||||
#define IDI_MSA_ACTION_GO_TUBE 0xA9
|
||||
#define IDI_MSA_ACTION_USE_FLASHLIGHT 0xAA
|
||||
#define IDI_MSA_ACTION_PLUTO_DIG 0xAB
|
||||
#define IDI_MSA_ACTION_GET_XTAL_MARS 0xAC
|
||||
|
||||
#define IDI_MSA_ACTION_USE_CRYSTAL 0xAD
|
||||
#define IDI_MSA_ACTION_OPEN_DOOR 0xAE
|
||||
#define IDI_MSA_ACTION_ENTER_DOOR 0xAF
|
||||
#define IDI_MSA_ACTION_GET_XTAL_URANUS 0xB0
|
||||
#define IDI_MSA_ACTION_USE_CROWBAR_1 0xB1
|
||||
|
||||
#define IDI_MSA_ACTION_GO_NORTH 0xB2
|
||||
#define IDI_MSA_ACTION_GO_PLANET 0xB3
|
||||
#define IDI_MSA_ACTION_PRESS_BUTTON 0xB4
|
||||
#define IDI_MSA_ACTION_WEAR_SPACESUIT 0xB5
|
||||
#define IDI_MSA_ACTION_READ_GAUGE 0xB6
|
||||
#define IDI_MSA_ACTION_PRESS_ORANGE 0xB7
|
||||
#define IDI_MSA_ACTION_PRESS_BLUE 0xB8
|
||||
#define IDI_MSA_ACTION_FLIP_SWITCH 0xB9
|
||||
#define IDI_MSA_ACTION_PUSH_THROTTLE 0xBA
|
||||
#define IDI_MSA_ACTION_PULL_THROTTLE 0xBB
|
||||
#define IDI_MSA_ACTION_LEAVE_ROOM 0xBC
|
||||
#define IDI_MSA_ACTION_OPEN_CABINET_1 0xBD
|
||||
#define IDI_MSA_ACTION_READ_MAP 0xBE
|
||||
#define IDI_MSA_ACTION_GO_WEST 0xBF
|
||||
|
||||
#define IDI_MSA_ACTION_PLANET_INFO 0xC0
|
||||
#define IDI_MSA_ACTION_ENTER_TEMPLE 0xC1
|
||||
#define IDI_MSA_ACTION_OPEN_MAILBOX 0xC2
|
||||
#define IDI_MSA_ACTION_SAVE_GAME 0xC3
|
||||
#define IDI_MSA_ACTION_LOOK_MICKEY 0xC4
|
||||
|
||||
// sounds
|
||||
|
||||
enum ENUM_MSA_SOUND {
|
||||
IDI_MSA_SND_THEME,
|
||||
IDI_MSA_SND_CRYSTAL,
|
||||
IDI_MSA_SND_TAKE,
|
||||
IDI_MSA_SND_GAME_OVER,
|
||||
IDI_MSA_SND_PRESS_BLUE,
|
||||
IDI_MSA_SND_PRESS_ORANGE,
|
||||
IDI_MSA_SND_SHIP_LAND,
|
||||
IDI_MSA_SND_XL30
|
||||
};
|
||||
|
||||
// message offsets within mickey.exe
|
||||
|
||||
const int IDO_MSA_HIDDEN_MSG[] = {
|
||||
0x8C44, 0x8C83, 0x8D23, 0x8D97, 0x8E2A
|
||||
};
|
||||
|
||||
const int IDO_MSA_GAME_OVER[] = {
|
||||
0x7914, 0x7978, 0x7A17, 0x7A94, 0x7B04, 0x7B8F, 0x7BEB, 0x7C79
|
||||
};
|
||||
|
||||
const int IDO_MSA_SAVE_GAME[] = {
|
||||
0x73FA, 0x7436, 0x746C, 0x74E9, 0x75F6, 0x766A, 0x758B
|
||||
// do you have a formatted disk, insert disk, insert disk 2, save by number
|
||||
// everything will be lost, previous game will be lost, game saved
|
||||
};
|
||||
|
||||
const int IDO_MSA_LOAD_GAME[] = {
|
||||
0x76CE, 0x770B, 0x7777
|
||||
// do you want to load game, insert game save disk, game restored
|
||||
};
|
||||
|
||||
const int IDO_MSA_AIR_SUPPLY[] = {
|
||||
0x7D10, 0x7D31, 0x7D51, 0x7D9B
|
||||
// be aware, low, dangerously low, out of air
|
||||
};
|
||||
|
||||
const int IDI_MSA_AIR_SUPPLY[] = { 30, 20, 10, 0 };
|
||||
|
||||
// planet information
|
||||
|
||||
const int IDO_MSA_PLANET_INFO[IDI_MSA_MAX_PLANET][4] = {
|
||||
{0x6313, 0x63B2, 0x6449, 0}, // EARTH
|
||||
{0x61EB, 0x6288, 0, 0}, // VENUS
|
||||
{0x6B64, 0x6C06, 0x6CA3, 0}, // NEPTUNE
|
||||
{0x609B, 0x612C, 0x61CA, 0}, // MERCURY
|
||||
{0x6879, 0x6916, 0x6984, 0}, // SATURN
|
||||
{0x6CCF, 0x6D72, 0x6E10, 0}, // PLUTO
|
||||
{0x667C, 0x6714, 0x67B1, 0x684E}, // JUPITER
|
||||
{0x6471, 0x650F, 0x65AD, 0x6651}, // MARS
|
||||
{0x69C3, 0x6A62, 0x6B00, 0} // URANUS
|
||||
};
|
||||
|
||||
// next crystal piece hints
|
||||
|
||||
const int IDO_MSA_NEXT_PIECE[IDI_MSA_MAX_PLANET][5] = {
|
||||
{0, 0, 0, 0, 0}, // earth
|
||||
{0x4DCC, 0x4E20, 0x4E64, 0x4E9E, 0x4F0B}, // venus
|
||||
{0x5900, 0x599B, 0x5A07, 0x5A8E, 0x5B07}, // neptune
|
||||
{0x4F57, 0x4FA3, 0x4FF1, 0x5056, 0x50BD}, // mercury
|
||||
{0x5471, 0x54DF, 0x5548, 0x55C2, 0x562A}, // saturn
|
||||
{0x5B78, 0x5BB6, 0x5C29, 0x5C76, 0x5CE1}, // pluto
|
||||
{0x526B, 0x52DA, 0x5340, 0x53A1, 0x540C}, // jupiter
|
||||
{0x50F6, 0x512C, 0x5170, 0x51D5, 0x5228}, // mars
|
||||
{0x56AA, 0x571C, 0x579E, 0x5807, 0x5875} // uranus
|
||||
};
|
||||
|
||||
// message offsets
|
||||
|
||||
#define IDO_MSA_COPYRIGHT 0x7801
|
||||
#define IDO_MSA_INTRO 0x4679
|
||||
#define IDO_MSA_GAME_STORY 0x6E9C
|
||||
|
||||
#define IDO_MSA_PRESS_1_TO_9 0x7530
|
||||
#define IDO_MSA_PRESS_YES_OR_NO 0x480D
|
||||
#define IDO_MSA_MICKEY_HAS_PRESSED 0x5D90
|
||||
#define IDO_MSA_TOO_MANY_BUTTONS_PRESSED 0x5DF7
|
||||
|
||||
#define IDO_MSA_XL30_SPEAKING 0x4725
|
||||
#define IDO_MSA_CRYSTAL_PIECE_FOUND 0x600C
|
||||
|
||||
#define IDO_MSA_ROOM_TEXT_OFFSETS 0x8B01
|
||||
#define IDO_MSA_ROOM_OBJECT_XY_OFFSETS 0x8EA8
|
||||
#define IDO_MSA_ROOM_MENU_FIX 0x4a27
|
||||
|
||||
// offsets to offset arrays
|
||||
|
||||
#define IDOFS_MSA_MENU_PATCHES 0x5e7a
|
||||
#define IDOFS_MSA_SOUND_DATA 0x9deb
|
||||
|
||||
// game structure
|
||||
|
||||
struct MSA_GAME {
|
||||
uint8 iRoom;
|
||||
uint8 iPlanet;
|
||||
uint8 iDisk;
|
||||
|
||||
uint8 nAir;
|
||||
uint8 nButtons;
|
||||
uint8 nRocks;
|
||||
|
||||
uint8 nXtals;
|
||||
uint8 iPlanetXtal[IDI_MSA_MAX_DAT];
|
||||
uint16 iClue[IDI_MSA_MAX_PLANET];
|
||||
char szAddr[IDI_MSA_MAX_BUTTON + 1];
|
||||
|
||||
// Flags
|
||||
bool fHasXtal;
|
||||
bool fIntro;
|
||||
bool fSuit;
|
||||
bool fShipDoorOpen;
|
||||
bool fFlying;
|
||||
bool fStoryShown;
|
||||
bool fPlanetsInitialized;
|
||||
bool fTempleDoorOpen;
|
||||
bool fAnimXL30;
|
||||
bool fItem[IDI_MSA_MAX_ITEM];
|
||||
bool fItemUsed[IDI_MSA_MAX_ITEM];
|
||||
|
||||
int8 iItem[IDI_MSA_MAX_ITEM];
|
||||
uint8 nItems;
|
||||
|
||||
//uint8 fRmTxt[IDI_MSA_MAX_ROOM];
|
||||
int8 iRmObj[IDI_MSA_MAX_ROOM];
|
||||
uint8 iRmPic[IDI_MSA_MAX_ROOM];
|
||||
uint16 oRmTxt[IDI_MSA_MAX_ROOM];
|
||||
|
||||
uint8 iRmMenu[IDI_MSA_MAX_ROOM];
|
||||
uint8 nRmMenu[IDI_MSA_MAX_ROOM];
|
||||
|
||||
int8 nFrame;
|
||||
};
|
||||
|
||||
class PreAgiEngine;
|
||||
class PictureMgr_Mickey_Winnie;
|
||||
|
||||
class MickeyEngine : public PreAgiEngine {
|
||||
public:
|
||||
MickeyEngine(OSystem *syst, const AGIGameDescription *gameDesc);
|
||||
~MickeyEngine() override;
|
||||
|
||||
void init();
|
||||
Common::Error go() override;
|
||||
|
||||
void debugCurRoom();
|
||||
void debugGotoRoom(int);
|
||||
void drawPic(int);
|
||||
void drawObj(ENUM_MSA_OBJECT, int, int);
|
||||
|
||||
protected:
|
||||
PictureMgr_Mickey_Winnie *_picture;
|
||||
|
||||
MSA_GAME _gameStateMickey;
|
||||
bool _clickToMove;
|
||||
bool _isGameOver;
|
||||
|
||||
int getDat(int);
|
||||
void readExe(int, uint8 *, long);
|
||||
void getDatFileName(int, char *);
|
||||
void readDatHdr(char *, MSA_DAT_HEADER *);
|
||||
void readOfsData(int, int, uint8 *, long);
|
||||
bool chooseY_N(int, bool);
|
||||
int choose1to9(int);
|
||||
void printStr(char *);
|
||||
void printLine(const char *);
|
||||
void printExeStr(int);
|
||||
void printExeMsg(int);
|
||||
void printDesc(int);
|
||||
bool checkMenu();
|
||||
void drawMenu(MSA_MENU &, int, int);
|
||||
void getMouseMenuSelRow(MSA_MENU &, int *, int *, int, int, int);
|
||||
bool getMenuSelRow(MSA_MENU &, int *, int *, int);
|
||||
void getMenuSel(char *, int *, int *);
|
||||
void centerMenu(MSA_MENU *);
|
||||
void patchMenu(MSA_MENU *);
|
||||
void printDatString(int);
|
||||
void printDatMessage(int);
|
||||
bool playNote(MSA_SND_NOTE note, WaitOptions options);
|
||||
bool playSound(ENUM_MSA_SOUND iSound, WaitOptions options = kWaitProcessEvents);
|
||||
void drawRoomAnimation();
|
||||
void drawRoom();
|
||||
bool drawLogo();
|
||||
void animate();
|
||||
void printRoomDesc();
|
||||
bool loadGame();
|
||||
void saveGame();
|
||||
void showPlanetInfo();
|
||||
void printStory();
|
||||
int getPlanet();
|
||||
void pressOB(int);
|
||||
void insertDisk(int);
|
||||
void gameOver();
|
||||
void inventory();
|
||||
void intro();
|
||||
void getItem(ENUM_MSA_ITEM);
|
||||
void getXtal(int);
|
||||
bool parse(int, int);
|
||||
void flipSwitch();
|
||||
void waitAnyKey(bool anim = false);
|
||||
|
||||
bool planetIsAlreadyAssigned(int planet) {
|
||||
for (int j = 0; j < IDI_MSA_MAX_PLANET; j++) {
|
||||
if (_gameStateMickey.iPlanetXtal[j] == planet)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mickeyHasItem(int item) {
|
||||
if (_gameStateMickey.fItem[item]) {
|
||||
printDatMessage(90); // Mickey already has item
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isCrystalOnCurrentPlanet() const;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif
|
||||
348
engines/agi/preagi/picture_mickey_winnie.cpp
Normal file
348
engines/agi/preagi/picture_mickey_winnie.cpp
Normal file
@@ -0,0 +1,348 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/picture.h"
|
||||
|
||||
#include "agi/preagi/picture_mickey_winnie.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// PictureMgr_Mickey_Winnie decodes and draws picture resources in Mickey's
|
||||
// Space Adventure (DOS) and Winnie the Pooh (DOS/Amiga/A2/C64/CoCo).
|
||||
//
|
||||
// Mickey and Winnie DOS/Amiga use the same format. The picture code in
|
||||
// their executables appears to be the same.
|
||||
//
|
||||
// The A2/C64/CoCo versions of Winnie use a completely different format, but
|
||||
// they do support the same features. These games start in ScummVM but they
|
||||
// don't really work yet. TODO: display the right colors, figure out the line
|
||||
// differences, support these versions.
|
||||
//
|
||||
// Both formats support lines, flood fills, and patterns. No priority screen.
|
||||
//
|
||||
// Unique features to these formats:
|
||||
//
|
||||
// 1. Pictures can be drawn on top of others at arbitrary locations. Used to
|
||||
// draw items in rooms, and to draw room pictures with a buffer on each side
|
||||
// in DOS/Amiga. The pictures don't fill the screen width because they were
|
||||
// designed for the Apple II.
|
||||
//
|
||||
// 2. Mickey's crystals animate. Most of the work is done in MickeyEngine;
|
||||
// this class just allows the engine to set a maximum number of picture
|
||||
// instructions to execute. Unclear if this is same effect as the original.
|
||||
//
|
||||
// 3. The pattern opcode draws solid circles in up to 17 sizes.
|
||||
//
|
||||
// Mickey features animating spaceship lights, but the engine handles that.
|
||||
// The lights are a picture whose instructions are modified before drawing.
|
||||
//
|
||||
// TODO: There are extremely minor inaccuracies in several Winnie pictures.
|
||||
// The F1 opcode's effects are not fully understood, and it creates subtle
|
||||
// discrepancies. It may be related to dithering. However, so few pictures
|
||||
// contain F3, and even fewer are affected by ignoring it or not, and only
|
||||
// by a few pixels, that it doesn't matter except for completeness.
|
||||
// See: picture 34 door handles (Rabbit's kitchen)
|
||||
|
||||
PictureMgr_Mickey_Winnie::PictureMgr_Mickey_Winnie(AgiBase *agi, GfxMgr *gfx) :
|
||||
PictureMgr(agi, gfx) {
|
||||
|
||||
switch (agi->getPlatform()) {
|
||||
case Common::kPlatformAmiga:
|
||||
case Common::kPlatformDOS:
|
||||
_isDosOrAmiga = true;
|
||||
break;
|
||||
default:
|
||||
_isDosOrAmiga = false;
|
||||
_minCommand = 0xe0;
|
||||
break;
|
||||
}
|
||||
|
||||
_xOffset = 0;
|
||||
_yOffset = 0;
|
||||
_maxStep = 0;
|
||||
}
|
||||
|
||||
void PictureMgr_Mickey_Winnie::drawPicture() {
|
||||
debugC(kDebugLevelPictures, "Drawing picture");
|
||||
|
||||
_dataOffset = 0;
|
||||
_dataOffsetNibble = false;
|
||||
_patCode = 0;
|
||||
_patNum = 0;
|
||||
_priOn = false;
|
||||
_scrOn = false;
|
||||
_priColor = 4;
|
||||
|
||||
if (_isDosOrAmiga) {
|
||||
_scrColor = 15;
|
||||
drawPicture_DOS_Amiga();
|
||||
} else {
|
||||
_scrColor = 0;
|
||||
drawPicture_A2_C64_CoCo();
|
||||
}
|
||||
}
|
||||
|
||||
void PictureMgr_Mickey_Winnie::drawPicture_DOS_Amiga() {
|
||||
int step = 0;
|
||||
while (_dataOffset < _dataSize) {
|
||||
byte curByte = getNextByte();
|
||||
|
||||
switch (curByte) {
|
||||
case 0xf0:
|
||||
draw_SetColor();
|
||||
_scrOn = true;
|
||||
break;
|
||||
case 0xf1:
|
||||
_scrOn = false;
|
||||
break;
|
||||
case 0xf4:
|
||||
yCorner();
|
||||
break;
|
||||
case 0xf5:
|
||||
xCorner();
|
||||
break;
|
||||
case 0xf6:
|
||||
draw_LineAbsolute();
|
||||
break;
|
||||
case 0xf7:
|
||||
draw_LineShort();
|
||||
break;
|
||||
case 0xf8: {
|
||||
// The screen-on flag does not prevent PreAGI flood fills.
|
||||
// Winnie picture 7 (Roo) contains F1 before several fills.
|
||||
byte prevScrOn = _scrOn;
|
||||
_scrOn = true;
|
||||
PictureMgr::draw_Fill();
|
||||
_scrOn = prevScrOn;
|
||||
break;
|
||||
}
|
||||
case 0xf9:
|
||||
plotBrush();
|
||||
break;
|
||||
case 0xff: // end of data
|
||||
return;
|
||||
default:
|
||||
warning("Unknown picture opcode %02x at %04x", curByte, _dataOffset - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
// Limit drawing to the optional maximum number of opcodes.
|
||||
// Used by Mickey for crystal animation.
|
||||
step++;
|
||||
if (step == _maxStep) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PictureMgr_Mickey_Winnie::drawPicture_A2_C64_CoCo() {
|
||||
while (_dataOffset < _dataSize) {
|
||||
byte curByte = getNextByte();
|
||||
|
||||
if ((curByte >= 0xF0) && (curByte <= 0xFE)) {
|
||||
_scrColor = curByte & 0x0F;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (curByte) {
|
||||
case 0xe0: // x-corner
|
||||
xCorner();
|
||||
break;
|
||||
case 0xe1: // y-corner
|
||||
yCorner();
|
||||
break;
|
||||
case 0xe2: // dynamic draw lines
|
||||
draw_LineShort();
|
||||
break;
|
||||
case 0xe3: // absolute draw lines
|
||||
draw_LineAbsolute();
|
||||
break;
|
||||
case 0xe4: // fill
|
||||
draw_SetColor();
|
||||
PictureMgr::draw_Fill();
|
||||
break;
|
||||
case 0xe5: // enable screen drawing
|
||||
_scrOn = true;
|
||||
break;
|
||||
case 0xe6: // plot brush
|
||||
plotBrush();
|
||||
break;
|
||||
case 0xff: // end of data
|
||||
return;
|
||||
default:
|
||||
warning("Unknown picture opcode %02x at %04x", curByte, _dataOffset - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* plotBrush (PreAGI)
|
||||
*
|
||||
* Plots the given brush pattern. All brushes are solid circles.
|
||||
*/
|
||||
void PictureMgr_Mickey_Winnie::plotBrush() {
|
||||
_patCode = getNextByte();
|
||||
if (_patCode > 12) {
|
||||
_patCode = 12;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
byte x, y;
|
||||
if (!getNextCoordinates(x, y))
|
||||
break;
|
||||
|
||||
plotPattern(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* plotPattern
|
||||
*
|
||||
* Draws a solid circle. Size is determined by the pattern code.
|
||||
*/
|
||||
void PictureMgr_Mickey_Winnie::plotPattern(byte x, byte y) {
|
||||
// PreAGI patterns are 13 solid circles
|
||||
static const byte circleData[] = {
|
||||
0x00,
|
||||
0x01, 0x01,
|
||||
0x01, 0x02, 0x02,
|
||||
0x01, 0x02, 0x03, 0x03,
|
||||
0x02, 0x03, 0x04, 0x04, 0x04,
|
||||
0x02, 0x03, 0x04, 0x05, 0x05, 0x05,
|
||||
0x02, 0x04, 0x05, 0x05, 0x06, 0x06, 0x06,
|
||||
0x02, 0x04, 0x05, 0x06, 0x06, 0x07, 0x07, 0x07,
|
||||
0x02, 0x04, 0x06, 0x06, 0x07, 0x07, 0x08, 0x08, 0x08,
|
||||
0x03, 0x05, 0x06, 0x07, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09,
|
||||
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x09, 0x0a, 0x0a, 0x0a, 0x0a,
|
||||
0x03, 0x05, 0x07, 0x08, 0x09, 0x09, 0x0a, 0x0a, 0x0b, 0x0b, 0x0b, 0x0b,
|
||||
0x03, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0a, 0x0b, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c
|
||||
};
|
||||
|
||||
int circleDataIndex = (_patCode * (_patCode + 1)) / 2;
|
||||
|
||||
// draw the circle by drawing its vertical lines two at a time, starting at the
|
||||
// left and right edges and working inwards. circles have odd widths, so the
|
||||
// final iteration draws the middle line twice.
|
||||
for (int i = _patCode; i >= 0; i--) {
|
||||
const byte height = circleData[circleDataIndex++];
|
||||
int16 x1, y1, x2, y2;
|
||||
|
||||
// left vertical line
|
||||
x1 = x - i;
|
||||
x2 = x1;
|
||||
y1 = y - height;
|
||||
y2 = y + height;
|
||||
draw_Line(x1, y1, x2, y2);
|
||||
|
||||
// right vertical line
|
||||
x1 = x + i;
|
||||
x2 = x1;
|
||||
draw_Line(x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood fills from a start position, with a clipped height.
|
||||
*/
|
||||
void PictureMgr_Mickey_Winnie::draw_Fill(int16 x, int16 y) {
|
||||
// Flood fill does extra height clipping, and pictures rely on this.
|
||||
// The get-coordinates routine clips to (139, 159) and then the
|
||||
// flood fill routine checks if y >= 159 and decrements to 158.
|
||||
// The flood fill clip is not in in Apple II/C64/CoCo versions
|
||||
// of Winnie, as can be seen by the table edge being a different
|
||||
// color than Winnie's shirt in the first room, but the same
|
||||
// color as the shirt in DOS/Amiga. (Picture 28)
|
||||
if (_isDosOrAmiga) {
|
||||
if (y >= _height) { // 159
|
||||
debugC(kDebugLevelPictures, "clipping %c from %d to %d", 'y', y, _height - 1);
|
||||
y = _height - 1; // 158
|
||||
}
|
||||
}
|
||||
|
||||
PictureMgr::draw_Fill(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next x coordinate in the current picture instruction,
|
||||
* and clip it to the picture width. Many Winnie pictures contain
|
||||
* out of bounds coordinates and rely on this clipping.
|
||||
*/
|
||||
bool PictureMgr_Mickey_Winnie::getNextXCoordinate(byte &x) {
|
||||
if (!getNextParamByte(x)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_isDosOrAmiga) {
|
||||
if (x >= _width) { // 140
|
||||
debugC(kDebugLevelPictures, "clipping %c from %d to %d", 'x', x, _width - 1);
|
||||
x = _width - 1; // 139
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next y coordinate in the current picture instruction,
|
||||
* and clip it to the picture height. Many Winnie pictures contain
|
||||
* out of bounds coordinates and rely on this clipping.
|
||||
*/
|
||||
bool PictureMgr_Mickey_Winnie::getNextYCoordinate(byte &y) {
|
||||
if (!getNextParamByte(y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_isDosOrAmiga) {
|
||||
// note that this is a different clip than for the x coordinate
|
||||
if (y > _height) { // 159
|
||||
debugC(kDebugLevelPictures, "clipping %c from %d to %d", 'y', y, _height);
|
||||
y = _height; // 159
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates picture coordinates and translates them to GfxMgr coordinates.
|
||||
*
|
||||
* This function applies the current picture object and validates that the
|
||||
* graphics coordinates are within GfxMgr's boundaries. Validation is necessary
|
||||
* because Winnie places tall objects at the bottom of the screen in several
|
||||
* rooms, and GfxMgr does not validate coordinates.
|
||||
*/
|
||||
bool PictureMgr_Mickey_Winnie::getGraphicsCoordinates(int16 &x, int16 &y) {
|
||||
// validate that the coordinates are within the picture's boundaries
|
||||
if (!PictureMgr::getGraphicsCoordinates(x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
x += _xOffset;
|
||||
y += _yOffset;
|
||||
|
||||
// validate that the offset coordinates are within the screen's boundaries
|
||||
return (x < SCRIPT_WIDTH && y < SCRIPT_HEIGHT);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
62
engines/agi/preagi/picture_mickey_winnie.h
Normal file
62
engines/agi/preagi/picture_mickey_winnie.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PREAGI_PICTURE_MICKEY_WINNIE_H
|
||||
#define AGI_PREAGI_PICTURE_MICKEY_WINNIE_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class PictureMgr_Mickey_Winnie : public PictureMgr {
|
||||
public:
|
||||
PictureMgr_Mickey_Winnie(AgiBase *agi, GfxMgr *gfx);
|
||||
|
||||
void drawPicture() override;
|
||||
void drawPicture_DOS_Amiga();
|
||||
void drawPicture_A2_C64_CoCo();
|
||||
|
||||
void plotPattern(byte x, byte y) override;
|
||||
void plotBrush() override;
|
||||
|
||||
void draw_Fill(int16 x, int16 y) override;
|
||||
|
||||
bool getNextXCoordinate(byte &x) override;
|
||||
bool getNextYCoordinate(byte &y) override;
|
||||
|
||||
bool getGraphicsCoordinates(int16 &x, int16 &y) override;
|
||||
|
||||
void setOffset(int xOffset, int yOffset) {
|
||||
_xOffset = xOffset;
|
||||
_yOffset = yOffset;
|
||||
}
|
||||
|
||||
void setMaxStep(int maxStep) { _maxStep = maxStep; }
|
||||
int getMaxStep() const { return _maxStep; }
|
||||
|
||||
private:
|
||||
bool _isDosOrAmiga;
|
||||
int16 _xOffset;
|
||||
int16 _yOffset;
|
||||
int _maxStep; // Max opcodes to draw, zero for all. Used by Mickey
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif
|
||||
184
engines/agi/preagi/picture_troll.cpp
Normal file
184
engines/agi/preagi/picture_troll.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/picture.h"
|
||||
|
||||
#include "agi/preagi/picture_troll.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// PictureMgr_Troll decodes and draws Troll's Tale picture resources.
|
||||
//
|
||||
// Troll's Tale supports lines and flood fills. There is no priority screen.
|
||||
//
|
||||
// There are two unique picture features:
|
||||
//
|
||||
// 1. The F3 opcode can dynamically act as a no-op or terminator (FF).
|
||||
// This allows pictures to have an optional set of instructions for
|
||||
// drawing or hiding a room's object or the king's crown.
|
||||
//
|
||||
// 2. A custom flood fill technique is used for drawing the Troll over
|
||||
// room pictures. Normally, flood fill requires an empty (white) area.
|
||||
//
|
||||
// One quirk is that the xCorner and yCorner instructions contain a redundant
|
||||
// coordinate, even though it is ignored because it is derived from the others.
|
||||
//
|
||||
// Each room picture depends on the game first drawing a frame within the entire
|
||||
// picture area. This is not decorative; the flood fill routines rely on this
|
||||
// border because they do not do boundary test, and pictures are drawn for it.
|
||||
|
||||
PictureMgr_Troll::PictureMgr_Troll(AgiBase *agi, GfxMgr *gfx) :
|
||||
PictureMgr(agi, gfx) {
|
||||
_stopOnF3 = false;
|
||||
_trollMode = false;
|
||||
}
|
||||
|
||||
void PictureMgr_Troll::drawPicture() {
|
||||
debugC(kDebugLevelPictures, "Drawing picture");
|
||||
|
||||
_dataOffset = 0;
|
||||
_dataOffsetNibble = false;
|
||||
_patCode = 0;
|
||||
_patNum = 0;
|
||||
_priOn = false;
|
||||
_scrOn = false;
|
||||
_priColor = 4;
|
||||
_scrColor = 15;
|
||||
|
||||
while (_dataOffset < _dataSize) {
|
||||
byte curByte = getNextByte();
|
||||
|
||||
switch (curByte) {
|
||||
case 0xf0: // F0 is in all Troll's Tale pictures, but it is a no-op.
|
||||
break; // Confirmed in disassembly of opcode table.
|
||||
case 0xf1:
|
||||
draw_SetColor();
|
||||
_scrOn = true;
|
||||
break;
|
||||
case 0xf3: // F3 would impersonate opcode F0 (no-op) or FF (terminator)
|
||||
if (_stopOnF3)
|
||||
return;
|
||||
break;
|
||||
case 0xf8:
|
||||
yCorner(true); // skip extra (redundant) coordinates when parsing
|
||||
break;
|
||||
case 0xf9:
|
||||
xCorner(true); // skip extra (redundant) coordinates when parsing
|
||||
break;
|
||||
case 0xfa: // FA and FB are both used, but they are the same.
|
||||
case 0xfb: // Confirmed in disassembly of opcode table.
|
||||
draw_LineAbsolute();
|
||||
break;
|
||||
case 0xfe:
|
||||
draw_Fill();
|
||||
break;
|
||||
case 0xff: // end of data
|
||||
return;
|
||||
default:
|
||||
warning("Unknown picture opcode %02x at %04x", curByte, _dataOffset - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood fills from a series of start positions.
|
||||
*
|
||||
* Troll's Tale contains two separate flood fill implementations to handle the
|
||||
* special case of drawing the Troll. The game sets a global before drawing to
|
||||
* to activate Troll mode. We implement this by overriding this method and
|
||||
* the check method.
|
||||
*/
|
||||
void PictureMgr_Troll::draw_Fill() {
|
||||
draw_SetColor();
|
||||
_scrOn = true;
|
||||
|
||||
byte x, y;
|
||||
if (_scrColor == 15) { // white
|
||||
// White flood fills are only allowed when drawing the Troll, otherwise they
|
||||
// are completely ignored. Several room pictures contain white flood fills.
|
||||
while (getNextCoordinates(x, y)) {
|
||||
if (_trollMode) {
|
||||
PictureMgr::draw_Fill(x, y);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// When not drawing the Troll, do a regular flood fill.
|
||||
// When drawing the Troll, first fill with white, and then fill normally.
|
||||
byte fillColor = _scrColor;
|
||||
while (getNextCoordinates(x, y)) {
|
||||
if (_trollMode) {
|
||||
_scrColor = 15; // white
|
||||
PictureMgr::draw_Fill(x, y);
|
||||
_scrColor = fillColor;
|
||||
}
|
||||
PictureMgr::draw_Fill(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if flood fill is allowed at a position.
|
||||
*
|
||||
* Troll's Tale contains two separate flood fill implementations to handle the
|
||||
* special case of drawing the Troll. The game sets a global before drawing to
|
||||
* to activate Troll mode.
|
||||
*
|
||||
* The Troll is a large picture with flood fills that is drawn over many busy
|
||||
* room pictures, and always in the same location. This is a problem because the
|
||||
* picture format is only meant to fill white areas. Sierra handled this by
|
||||
* reserving a color for the Troll's lines (11, light blue) and implementing a
|
||||
* second set of routines that fill white and treat the Troll's color as a
|
||||
* boundary, and sometimes white as well. When drawing the Troll, a non-white
|
||||
* fill is preceded by a special white fill to clear the area. This does not
|
||||
* work well if there are existing white pixels, and rooms do have these.
|
||||
* The Troll has incomplete fills in these rooms, but this is original behavior.
|
||||
* In some rooms, such as those with white checkered floors, the results are
|
||||
* so bad that Sierra added them to the list of rooms the Troll never visits.
|
||||
*
|
||||
* We implement Troll mode without a second flood fill algorithm; instead we
|
||||
* override the check method, and our AGI algorithm in the base class provides
|
||||
* the context so we know which of the two color checks to use.
|
||||
*/
|
||||
bool PictureMgr_Troll::draw_FillCheck(int16 x, int16 y, bool horizontalCheck) {
|
||||
if (_trollMode && _scrColor == 15) {
|
||||
if (!getGraphicsCoordinates(x, y)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte screenColor = _gfx->getColor(x, y);
|
||||
|
||||
// when filling white during troll mode...
|
||||
if (horizontalCheck) {
|
||||
// horizontal checks only stop on troll line color
|
||||
return (screenColor != 11);
|
||||
} else {
|
||||
// all other checks stop on troll line color or white
|
||||
return (screenColor != 11) && (screenColor != 15);
|
||||
}
|
||||
}
|
||||
|
||||
return PictureMgr::draw_FillCheck(x, y, horizontalCheck);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
46
engines/agi/preagi/picture_troll.h
Normal file
46
engines/agi/preagi/picture_troll.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PREAGI_PICTURE_TROLL_H
|
||||
#define AGI_PREAGI_PICTURE_TROLL_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class PictureMgr_Troll : public PictureMgr {
|
||||
public:
|
||||
PictureMgr_Troll(AgiBase *agi, GfxMgr *gfx);
|
||||
|
||||
void drawPicture() override;
|
||||
|
||||
void draw_Fill() override;
|
||||
bool draw_FillCheck(int16 x, int16 y, bool horizontalCheck) override;
|
||||
|
||||
void setStopOnF3(bool stopOnF3) { _stopOnF3 = stopOnF3; }
|
||||
void setTrollMode(bool trollMode) { _trollMode = trollMode; }
|
||||
|
||||
private:
|
||||
bool _stopOnF3;
|
||||
bool _trollMode;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif
|
||||
351
engines/agi/preagi/preagi.cpp
Normal file
351
engines/agi/preagi/preagi.cpp
Normal file
@@ -0,0 +1,351 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "audio/mixer.h"
|
||||
#include "audio/softsynth/pcspk.h"
|
||||
|
||||
#include "common/debug-channels.h"
|
||||
#include "common/events.h"
|
||||
#include "common/random.h"
|
||||
|
||||
#include "agi/preagi/preagi.h"
|
||||
#include "agi/graphics.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
PreAgiEngine::PreAgiEngine(OSystem *syst, const AGIGameDescription *gameDesc) : AgiBase(syst, gameDesc) {
|
||||
|
||||
// Setup mixer
|
||||
syncSoundSettings();
|
||||
|
||||
memset(&_debug, 0, sizeof(struct AgiDebug));
|
||||
}
|
||||
|
||||
void PreAgiEngine::initialize() {
|
||||
initRenderMode();
|
||||
|
||||
_font = new GfxFont(this);
|
||||
_gfx = new GfxMgr(this, _font);
|
||||
|
||||
_font->init();
|
||||
|
||||
//_game._vm->_text->charAttrib_Set(15, 0);
|
||||
|
||||
_defaultColor = IDA_DEFAULT;
|
||||
|
||||
//_game._vm->_text->configureScreen(0); // hardcoded
|
||||
|
||||
_gfx->initVideo();
|
||||
|
||||
_speaker = new Audio::PCSpeaker();
|
||||
_speaker->init();
|
||||
|
||||
debugC(2, kDebugLevelMain, "Detect game");
|
||||
|
||||
// clear all resources and events
|
||||
for (int i = 0; i < MAX_DIRECTORY_ENTRIES; i++) {
|
||||
_game.pictures[i].reset();
|
||||
_game.sounds[i] = nullptr; // _game.sounds contains pointers now
|
||||
_game.dirPic[i].reset();
|
||||
_game.dirSound[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
PreAgiEngine::~PreAgiEngine() {
|
||||
delete _speaker;
|
||||
|
||||
delete _gfx;
|
||||
delete _font;
|
||||
}
|
||||
|
||||
int PreAgiEngine::rnd(int max) {
|
||||
return (_rnd->getRandomNumber(max - 1) + 1);
|
||||
}
|
||||
|
||||
// Screen functions
|
||||
void PreAgiEngine::clearScreen(int attr, bool overrideDefault) {
|
||||
if (overrideDefault)
|
||||
_defaultColor = attr;
|
||||
|
||||
_gfx->clearDisplay((attr & 0xF0) / 0x10);
|
||||
}
|
||||
|
||||
void PreAgiEngine::clearGfxScreen(int attr) {
|
||||
_gfx->drawDisplayRect(0, 0, DISPLAY_DEFAULT_WIDTH - 1, IDI_MAX_ROW_PIC * 8 - 1, (attr & 0xF0) / 0x10);
|
||||
}
|
||||
|
||||
byte PreAgiEngine::getWhite() const {
|
||||
switch (_renderMode) {
|
||||
case Common::kRenderCGA:
|
||||
return 3;
|
||||
case Common::kRenderHercA:
|
||||
case Common::kRenderHercG:
|
||||
return 1;
|
||||
default:
|
||||
return 15;
|
||||
}
|
||||
}
|
||||
|
||||
// String functions
|
||||
|
||||
void PreAgiEngine::drawStr(int row, int col, int attr, const char *buffer) {
|
||||
if (attr == kColorDefault)
|
||||
attr = _defaultColor;
|
||||
|
||||
byte foreground = attr & 0x0f;
|
||||
byte background = attr >> 4;
|
||||
|
||||
// Simplistic CGA and Hercules mapping that handles all text
|
||||
// in Mickey and Winnie. Troll text is handled correctly in
|
||||
// graphics mode, but the original switched to CGA 16 color
|
||||
// text mode for some parts, and we are not doing that.
|
||||
switch (_renderMode) {
|
||||
case Common::kRenderCGA:
|
||||
// Map non-black text to white
|
||||
if (foreground != 0) {
|
||||
foreground = 3;
|
||||
}
|
||||
// Map white background to white
|
||||
if (background == 15) {
|
||||
background = 3;
|
||||
}
|
||||
break;
|
||||
case Common::kRenderHercA:
|
||||
case Common::kRenderHercG:
|
||||
// Map non-black text to amber/green
|
||||
if (foreground != 0) {
|
||||
foreground = 1;
|
||||
}
|
||||
// Map white background to amber/green,
|
||||
// all others to black
|
||||
if (background == 0x0f) {
|
||||
background = 1;
|
||||
} else {
|
||||
background = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const int stringLength = (int)strlen(buffer);
|
||||
for (int iChar = 0; iChar < stringLength; iChar++) {
|
||||
int code = buffer[iChar];
|
||||
|
||||
switch (code) {
|
||||
case '\n':
|
||||
case '\r': // winnie
|
||||
case 0x8D:
|
||||
if (++row == 200 / 8) return;
|
||||
col = 0;
|
||||
break;
|
||||
|
||||
case '|':
|
||||
// swap attribute nibbles
|
||||
break;
|
||||
|
||||
default:
|
||||
_gfx->drawCharacter(row, col, code, foreground, background, false);
|
||||
|
||||
if (++col == 320 / 8) {
|
||||
col = 0;
|
||||
if (++row == 200 / 8) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PreAgiEngine::clearTextArea() {
|
||||
int start = IDI_MAX_ROW_PIC;
|
||||
|
||||
if (getGameID() == GID_TROLL)
|
||||
start = 21;
|
||||
|
||||
for (int row = start; row < 200 / 8; row++) {
|
||||
clearRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
void PreAgiEngine::clearRow(int row) {
|
||||
drawStr(row, 0, IDA_DEFAULT, " "); // 40 spaces
|
||||
}
|
||||
|
||||
void PreAgiEngine::printStr(const char *szMsg) {
|
||||
clearTextArea();
|
||||
drawStr(21, 0, IDA_DEFAULT, szMsg);
|
||||
_system->updateScreen();
|
||||
}
|
||||
|
||||
void PreAgiEngine::XOR80(char *buffer) {
|
||||
for (size_t i = 0; i < strlen(buffer); i++)
|
||||
if (buffer[i] & 0x80)
|
||||
buffer[i] ^= 0x80;
|
||||
}
|
||||
|
||||
void PreAgiEngine::printStrXOR(char *szMsg) {
|
||||
XOR80(szMsg);
|
||||
printStr(szMsg);
|
||||
}
|
||||
|
||||
// Input functions
|
||||
|
||||
int PreAgiEngine::getSelection(SelectionTypes type) {
|
||||
Common::Event event;
|
||||
|
||||
while (!shouldQuit()) {
|
||||
while (_eventMan->pollEvent(event)) {
|
||||
switch (event.type) {
|
||||
case Common::EVENT_RETURN_TO_LAUNCHER:
|
||||
case Common::EVENT_QUIT:
|
||||
return 0;
|
||||
case Common::EVENT_RBUTTONUP:
|
||||
return 0;
|
||||
case Common::EVENT_LBUTTONUP:
|
||||
if (type == kSelYesNo || type == kSelAnyKey || type == kSelBackspace)
|
||||
return 1;
|
||||
break;
|
||||
case Common::EVENT_KEYDOWN:
|
||||
if (event.kbd.flags & Common::KBD_NON_STICKY) {
|
||||
break;
|
||||
}
|
||||
switch (event.kbd.keycode) {
|
||||
case Common::KEYCODE_y:
|
||||
if (type == kSelYesNo)
|
||||
return 1;
|
||||
break;
|
||||
case Common::KEYCODE_n:
|
||||
if (type == kSelYesNo)
|
||||
return 0;
|
||||
break;
|
||||
case Common::KEYCODE_ESCAPE:
|
||||
if (type == kSelNumber || type == kSelAnyKey)
|
||||
return 0;
|
||||
break;
|
||||
case Common::KEYCODE_1:
|
||||
case Common::KEYCODE_2:
|
||||
case Common::KEYCODE_3:
|
||||
case Common::KEYCODE_4:
|
||||
case Common::KEYCODE_5:
|
||||
case Common::KEYCODE_6:
|
||||
case Common::KEYCODE_7:
|
||||
case Common::KEYCODE_8:
|
||||
case Common::KEYCODE_9:
|
||||
if (type == kSelNumber)
|
||||
return event.kbd.keycode - Common::KEYCODE_1 + 1;
|
||||
break;
|
||||
case Common::KEYCODE_SPACE:
|
||||
if (type == kSelSpace)
|
||||
return 1;
|
||||
break;
|
||||
case Common::KEYCODE_BACKSPACE:
|
||||
if (type == kSelBackspace)
|
||||
return 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (type == kSelYesNo) {
|
||||
return 2;
|
||||
} else if (type == kSelNumber) {
|
||||
return 10;
|
||||
} else if (type == kSelAnyKey || type == kSelBackspace) {
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
_system->updateScreen();
|
||||
_system->delayMillis(10);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool PreAgiEngine::playSpeakerNote(int16 frequency, int32 length, WaitOptions options) {
|
||||
// play note, unless this is a pause
|
||||
if (frequency != 0) {
|
||||
_speaker->play(Audio::PCSpeaker::kWaveFormSquare, frequency, length);
|
||||
}
|
||||
|
||||
// wait for note length
|
||||
bool completed = wait(length, options);
|
||||
|
||||
// stop note if the wait was interrupted
|
||||
if (!completed) {
|
||||
if (frequency != 0) {
|
||||
_speaker->stop();
|
||||
}
|
||||
}
|
||||
|
||||
return completed;
|
||||
}
|
||||
|
||||
// A wait function that updates the screen, optionally allows events to be
|
||||
// processed, and optionally allows keyboard and mouse events to interrupt
|
||||
// the wait. Processing events keeps the program window responsive, but for
|
||||
// very short delays it may be better to not process events so that they
|
||||
// are buffered and not lost.
|
||||
bool PreAgiEngine::wait(uint32 delay, WaitOptions options) {
|
||||
Common::Event event;
|
||||
uint32 startTime = _system->getMillis();
|
||||
|
||||
bool processEvents = (options & kWaitProcessEvents) != 0;
|
||||
bool allowInterrupt = (options == kWaitAllowInterrupt);
|
||||
|
||||
while (!shouldQuit()) {
|
||||
// process events
|
||||
if (processEvents) {
|
||||
while (_eventMan->pollEvent(event)) {
|
||||
switch (event.type) {
|
||||
case Common::EVENT_KEYDOWN:
|
||||
// don't interrupt if a modifier is pressed
|
||||
if (event.kbd.flags & Common::KBD_NON_STICKY) {
|
||||
continue;
|
||||
}
|
||||
// fall through
|
||||
case Common::EVENT_LBUTTONUP:
|
||||
case Common::EVENT_RBUTTONUP:
|
||||
if (!allowInterrupt) {
|
||||
continue;
|
||||
}
|
||||
// fall through
|
||||
case Common::EVENT_RETURN_TO_LAUNCHER:
|
||||
case Common::EVENT_QUIT:
|
||||
return false; // interrupted by quit or input
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_system->getMillis() - startTime >= delay) {
|
||||
return true; // delay completed
|
||||
}
|
||||
|
||||
_system->updateScreen();
|
||||
_system->delayMillis(10);
|
||||
}
|
||||
|
||||
return false; // interrupted by quit
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
120
engines/agi/preagi/preagi.h
Normal file
120
engines/agi/preagi/preagi.h
Normal file
@@ -0,0 +1,120 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PREAGI_PREAGI_H
|
||||
#define AGI_PREAGI_PREAGI_H
|
||||
|
||||
#include "agi/agi.h"
|
||||
|
||||
namespace Audio {
|
||||
class PCSpeaker;
|
||||
}
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// default attributes
|
||||
#define IDA_DEFAULT 0x0F
|
||||
#define IDA_DEFAULT_REV 0xF0
|
||||
|
||||
#define IDI_SND_OSCILLATOR_FREQUENCY 1193180
|
||||
#define IDI_SND_TIMER_RESOLUTION 0.0182
|
||||
|
||||
#define kColorDefault 0x1337
|
||||
|
||||
#define IDI_MAX_ROW_PIC 20
|
||||
|
||||
enum SelectionTypes {
|
||||
kSelYesNo,
|
||||
kSelNumber,
|
||||
kSelSpace,
|
||||
kSelAnyKey,
|
||||
kSelBackspace
|
||||
};
|
||||
|
||||
// Options for controlling behavior during waits and sound playback
|
||||
enum WaitOptions {
|
||||
kWaitBlock = 0x00, // no event processing, cannot be interrupted
|
||||
kWaitProcessEvents = 0x01, // process events, stops on quit
|
||||
kWaitAllowInterrupt = 0x03 // process events, stops on input or quit
|
||||
};
|
||||
|
||||
class PreAgiEngine : public AgiBase {
|
||||
int _gameId;
|
||||
|
||||
protected:
|
||||
void initialize() override;
|
||||
|
||||
int getKeypress() override { return 0; }
|
||||
bool isKeypress() override { return false; }
|
||||
void clearKeyQueue() override {}
|
||||
|
||||
PreAgiEngine(OSystem *syst, const AGIGameDescription *gameDesc);
|
||||
~PreAgiEngine() override;
|
||||
int getGameId() const { return _gameId; }
|
||||
|
||||
void clearImageStack() override {}
|
||||
void recordImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3,
|
||||
int16 p4, int16 p5, int16 p6, int16 p7) override {}
|
||||
void replayImageStackCall(uint8 type, int16 p1, int16 p2, int16 p3,
|
||||
int16 p4, int16 p5, int16 p6, int16 p7) override {}
|
||||
void releaseImageStack() override {}
|
||||
int saveGame(const Common::String &fileName, const Common::String &saveName) { return -1; }
|
||||
int loadGame(const Common::String &fileName, bool checkId = true) { return -1; }
|
||||
|
||||
// Game
|
||||
Common::String getTargetName() const { return _targetName; }
|
||||
|
||||
// Screen
|
||||
void clearScreen(int attr, bool overrideDefault = true);
|
||||
void clearGfxScreen(int attr);
|
||||
void setDefaultTextColor(int attr) { _defaultColor = attr; }
|
||||
byte getWhite() const;
|
||||
|
||||
// Keyboard
|
||||
int getSelection(SelectionTypes type);
|
||||
|
||||
// Random number between 1 and max. Example: rnd(2) returns 1 or 2.
|
||||
int rnd(int max);
|
||||
|
||||
// Text
|
||||
void drawStr(int row, int col, int attr, const char *buffer);
|
||||
void clearTextArea();
|
||||
void clearRow(int row);
|
||||
static void XOR80(char *buffer);
|
||||
void printStr(const char *szMsg);
|
||||
void printStrXOR(char *szMsg);
|
||||
|
||||
// Saved Games
|
||||
Common::SaveFileManager *getSaveFileMan() { return _saveFileMan; }
|
||||
|
||||
bool playSpeakerNote(int16 frequency, int32 length, WaitOptions options);
|
||||
bool wait(uint32 delay, WaitOptions options = kWaitProcessEvents);
|
||||
|
||||
private:
|
||||
int _defaultColor;
|
||||
|
||||
Audio::PCSpeaker *_speaker;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
|
||||
#endif
|
||||
767
engines/agi/preagi/troll.cpp
Normal file
767
engines/agi/preagi/troll.cpp
Normal file
@@ -0,0 +1,767 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/preagi/preagi.h"
|
||||
#include "agi/preagi/picture_troll.h"
|
||||
#include "agi/preagi/troll.h"
|
||||
#include "agi/graphics.h"
|
||||
|
||||
#include "common/events.h"
|
||||
#include "common/textconsole.h"
|
||||
|
||||
#include "graphics/cursorman.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
TrollEngine::TrollEngine(OSystem *syst, const AGIGameDescription *gameDesc) : PreAgiEngine(syst, gameDesc) {
|
||||
_picture = nullptr;
|
||||
}
|
||||
|
||||
TrollEngine::~TrollEngine() {
|
||||
delete _picture;
|
||||
}
|
||||
|
||||
// User Interface
|
||||
|
||||
void TrollEngine::pressAnyKey(int col) {
|
||||
drawStr(24, col, kColorDefault, IDS_TRO_PRESSANYKEY);
|
||||
_system->updateScreen();
|
||||
getSelection(kSelAnyKey);
|
||||
}
|
||||
|
||||
void TrollEngine::drawMenu(const char *szMenu, int iSel) {
|
||||
clearTextArea();
|
||||
drawStr(21, 0, kColorDefault, szMenu);
|
||||
drawStr(22 + iSel, 0, kColorDefault, " *");
|
||||
_system->updateScreen();
|
||||
}
|
||||
|
||||
bool TrollEngine::getMenuSel(const char *szMenu, int *iSel, int nSel) {
|
||||
Common::Event event;
|
||||
int y;
|
||||
|
||||
drawMenu(szMenu, *iSel);
|
||||
|
||||
while (!shouldQuit()) {
|
||||
while (_system->getEventManager()->pollEvent(event)) {
|
||||
switch (event.type) {
|
||||
case Common::EVENT_RETURN_TO_LAUNCHER:
|
||||
case Common::EVENT_QUIT:
|
||||
return 0;
|
||||
case Common::EVENT_MOUSEMOVE:
|
||||
y = event.mouse.y / 8;
|
||||
|
||||
if (y >= 22)
|
||||
if (nSel > y - 22)
|
||||
*iSel = y - 22;
|
||||
|
||||
drawMenu(szMenu, *iSel);
|
||||
break;
|
||||
case Common::EVENT_LBUTTONUP:
|
||||
return true;
|
||||
case Common::EVENT_KEYDOWN:
|
||||
switch (event.kbd.keycode) {
|
||||
case Common::KEYCODE_t:
|
||||
case Common::KEYCODE_f:
|
||||
inventory();
|
||||
|
||||
return false;
|
||||
case Common::KEYCODE_DOWN:
|
||||
case Common::KEYCODE_SPACE:
|
||||
*iSel += 1;
|
||||
|
||||
if (*iSel == nSel)
|
||||
*iSel = IDI_TRO_SEL_OPTION_1;
|
||||
|
||||
drawMenu(szMenu, *iSel);
|
||||
break;
|
||||
case Common::KEYCODE_UP:
|
||||
*iSel -= 1;
|
||||
|
||||
if (*iSel == IDI_TRO_SEL_OPTION_1 - 1)
|
||||
*iSel = nSel - 1;
|
||||
|
||||
drawMenu(szMenu, *iSel);
|
||||
break;
|
||||
case Common::KEYCODE_RETURN:
|
||||
case Common::KEYCODE_KP_ENTER:
|
||||
return true;
|
||||
case Common::KEYCODE_s:
|
||||
if (event.kbd.hasFlags(Common::KBD_CTRL)) {
|
||||
if (_soundOn) {
|
||||
playTune(2, 1);
|
||||
_soundOn = !_soundOn;
|
||||
} else {
|
||||
_soundOn = !_soundOn;
|
||||
playTune(3, 1);
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
_system->updateScreen();
|
||||
_system->delayMillis(10);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Graphics
|
||||
|
||||
void TrollEngine::drawPic(int iPic, bool f3IsCont, bool clr, bool troll) {
|
||||
if (clr) {
|
||||
clearScreen(0x0f, false);
|
||||
}
|
||||
|
||||
// draw the frame picture
|
||||
_picture->setStopOnF3(false);
|
||||
_picture->setTrollMode(false);
|
||||
_picture->decodePictureFromBuffer(_gameData + IDO_TRO_FRAMEPIC, 4096, clr, IDI_TRO_PIC_WIDTH, IDI_TRO_PIC_HEIGHT);
|
||||
|
||||
// draw the picture
|
||||
_picture->setStopOnF3(!f3IsCont);
|
||||
_picture->setTrollMode(troll);
|
||||
_picture->decodePictureFromBuffer(_gameData + _pictureOffsets[iPic], 4096, false, IDI_TRO_PIC_WIDTH, IDI_TRO_PIC_HEIGHT);
|
||||
|
||||
_picture->showPicture(0, 0, IDI_TRO_PIC_WIDTH, IDI_TRO_PIC_HEIGHT);
|
||||
_system->updateScreen();
|
||||
}
|
||||
|
||||
// Game Logic
|
||||
|
||||
void TrollEngine::inventory() {
|
||||
clearScreen(0x07);
|
||||
drawStr(1, 12, kColorDefault, IDS_TRO_TREASURE_0);
|
||||
drawStr(2, 12, kColorDefault, IDS_TRO_TREASURE_1);
|
||||
|
||||
for (int i = 0; i < IDI_TRO_MAX_TREASURE - _treasuresLeft; i++) {
|
||||
int n = _inventory[i] - 1;
|
||||
drawStr(2 + i, 10, _items[n].bg << 4 | 0x0f, Common::String::format(" %2d ", i + 1).c_str());
|
||||
drawStr(2 + i, 14, _items[n].bg << 4 | _items[n].fg, _items[n].name);
|
||||
}
|
||||
|
||||
switch (_treasuresLeft) {
|
||||
case 1:
|
||||
drawStr(20, 10, kColorDefault, Common::String::format(IDS_TRO_TREASURE_5, _treasuresLeft).c_str());
|
||||
break;
|
||||
case 0:
|
||||
drawStr(20, 1, kColorDefault, IDS_TRO_TREASURE_6);
|
||||
break;
|
||||
case IDI_TRO_MAX_TREASURE:
|
||||
drawStr(3, 17, kColorDefault, IDS_TRO_TREASURE_2);
|
||||
break;
|
||||
default:
|
||||
drawStr(20, 10, kColorDefault, Common::String::format(IDS_TRO_TREASURE_4, _treasuresLeft).c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
pressAnyKey(6);
|
||||
}
|
||||
|
||||
void TrollEngine::waitAnyKeyIntro() {
|
||||
Common::Event event;
|
||||
int iMsg = 0;
|
||||
|
||||
while (!shouldQuit()) {
|
||||
while (_system->getEventManager()->pollEvent(event)) {
|
||||
switch (event.type) {
|
||||
case Common::EVENT_KEYDOWN:
|
||||
// don't interrupt if a modifier is pressed
|
||||
if (event.kbd.flags & Common::KBD_NON_STICKY) {
|
||||
continue;
|
||||
}
|
||||
// fall through
|
||||
case Common::EVENT_RETURN_TO_LAUNCHER:
|
||||
case Common::EVENT_QUIT:
|
||||
case Common::EVENT_LBUTTONUP:
|
||||
case Common::EVENT_RBUTTONUP:
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (iMsg) {
|
||||
case 200:
|
||||
iMsg = 0;
|
||||
// fall through
|
||||
case 0:
|
||||
drawStr(22, 3, kColorDefault, IDS_TRO_INTRO_2);
|
||||
break;
|
||||
case 100:
|
||||
drawStr(22, 3, kColorDefault, IDS_TRO_INTRO_3);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
iMsg++;
|
||||
|
||||
_system->updateScreen();
|
||||
_system->delayMillis(10);
|
||||
}
|
||||
}
|
||||
|
||||
void TrollEngine::credits() {
|
||||
clearScreen(0x07);
|
||||
|
||||
drawStr(1, 2, kColorDefault, IDS_TRO_CREDITS_0);
|
||||
|
||||
int color = 10;
|
||||
char str[2];
|
||||
|
||||
str[1] = 0;
|
||||
|
||||
for (uint i = 0; i < strlen(IDS_TRO_CREDITS_1); i++) {
|
||||
str[0] = IDS_TRO_CREDITS_1[i];
|
||||
drawStr(7, 19 + i, color++, str);
|
||||
if (color > 15)
|
||||
color = 9;
|
||||
}
|
||||
|
||||
drawStr(8, 19, kColorDefault, IDS_TRO_CREDITS_2);
|
||||
|
||||
drawStr(13, 11, 9, IDS_TRO_CREDITS_3);
|
||||
drawStr(15, 8, 10, IDS_TRO_CREDITS_4);
|
||||
drawStr(17, 7, 12, IDS_TRO_CREDITS_5);
|
||||
drawStr(19, 2, 14, IDS_TRO_CREDITS_6);
|
||||
|
||||
_system->updateScreen();
|
||||
|
||||
pressAnyKey();
|
||||
}
|
||||
|
||||
void TrollEngine::tutorial() {
|
||||
bool done = false;
|
||||
int iSel = 0;
|
||||
//char szTreasure[16] = {0};
|
||||
|
||||
while (!shouldQuit()) {
|
||||
clearScreen(0xFF);
|
||||
|
||||
printStr(IDS_TRO_TUTORIAL_0);
|
||||
getSelection(kSelSpace);
|
||||
|
||||
clearScreen(0x55);
|
||||
setDefaultTextColor(0x0F);
|
||||
|
||||
done = false;
|
||||
while (!done && !shouldQuit()) {
|
||||
getMenuSel(IDS_TRO_TUTORIAL_1, &iSel, IDI_TRO_MAX_OPTION);
|
||||
|
||||
switch (iSel) {
|
||||
case IDI_TRO_SEL_OPTION_1:
|
||||
clearScreen(0x22, false);
|
||||
_system->updateScreen();
|
||||
break;
|
||||
case IDI_TRO_SEL_OPTION_2:
|
||||
clearScreen(0x00, false);
|
||||
_system->updateScreen();
|
||||
break;
|
||||
case IDI_TRO_SEL_OPTION_3:
|
||||
done = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// do you need more practice ?
|
||||
clearScreen(0x4F);
|
||||
drawStr(7, 4, kColorDefault, IDS_TRO_TUTORIAL_5);
|
||||
drawStr(9, 4, kColorDefault, IDS_TRO_TUTORIAL_6);
|
||||
_system->updateScreen();
|
||||
|
||||
if (!getSelection(kSelYesNo))
|
||||
break;
|
||||
}
|
||||
|
||||
// show info texts
|
||||
clearScreen(0x5F);
|
||||
drawStr(4, 1, kColorDefault, IDS_TRO_TUTORIAL_7);
|
||||
drawStr(5, 1, kColorDefault, IDS_TRO_TUTORIAL_8);
|
||||
_system->updateScreen();
|
||||
pressAnyKey();
|
||||
|
||||
clearScreen(0x2F);
|
||||
drawStr(6, 1, kColorDefault, IDS_TRO_TUTORIAL_9);
|
||||
_system->updateScreen();
|
||||
pressAnyKey();
|
||||
|
||||
clearScreen(0x19);
|
||||
drawStr(7, 1, kColorDefault, IDS_TRO_TUTORIAL_10);
|
||||
drawStr(8, 1, kColorDefault, IDS_TRO_TUTORIAL_11);
|
||||
_system->updateScreen();
|
||||
pressAnyKey();
|
||||
|
||||
clearScreen(0x6E);
|
||||
drawStr(9, 1, kColorDefault, IDS_TRO_TUTORIAL_12);
|
||||
drawStr(10, 1, kColorDefault, IDS_TRO_TUTORIAL_13);
|
||||
_system->updateScreen();
|
||||
pressAnyKey();
|
||||
|
||||
clearScreen(0x4C);
|
||||
drawStr(11, 1, kColorDefault, IDS_TRO_TUTORIAL_14);
|
||||
drawStr(12, 1, kColorDefault, IDS_TRO_TUTORIAL_15);
|
||||
_system->updateScreen();
|
||||
pressAnyKey();
|
||||
|
||||
clearScreen(0x5D);
|
||||
drawStr(13, 1, kColorDefault, IDS_TRO_TUTORIAL_16);
|
||||
drawStr(14, 1, kColorDefault, IDS_TRO_TUTORIAL_17);
|
||||
drawStr(15, 1, kColorDefault, IDS_TRO_TUTORIAL_18);
|
||||
_system->updateScreen();
|
||||
pressAnyKey();
|
||||
|
||||
// show treasures
|
||||
clearScreen(0x2A);
|
||||
drawStr(2, 1, kColorDefault, IDS_TRO_TUTORIAL_19);
|
||||
for (int i = 0; i < IDI_TRO_MAX_TREASURE; i++)
|
||||
drawStr(19 - i, 11, kColorDefault, _items[i].name);
|
||||
|
||||
_system->updateScreen();
|
||||
|
||||
pressAnyKey();
|
||||
}
|
||||
|
||||
void TrollEngine::intro() {
|
||||
// sierra on-line presents
|
||||
clearScreen(0x2F);
|
||||
drawStr(9, 10, kColorDefault, IDS_TRO_INTRO_0);
|
||||
drawStr(14, 15, kColorDefault, IDS_TRO_INTRO_1);
|
||||
wait(3200);
|
||||
|
||||
CursorMan.showMouse(true);
|
||||
|
||||
// Draw logo
|
||||
setDefaultTextColor(0x0f);
|
||||
drawPic(45, false, true);
|
||||
_system->updateScreen();
|
||||
|
||||
// wait for keypress and alternate message
|
||||
waitAnyKeyIntro();
|
||||
|
||||
// have you played this game before?
|
||||
drawStr(22, 3, kColorDefault, IDS_TRO_INTRO_4);
|
||||
drawStr(23, 6, kColorDefault, IDS_TRO_INTRO_5);
|
||||
_system->updateScreen();
|
||||
|
||||
if (!getSelection(kSelYesNo))
|
||||
tutorial();
|
||||
|
||||
credits();
|
||||
}
|
||||
|
||||
void TrollEngine::gameOver() {
|
||||
// We do a check to see if the game should quit. Without this, the game show the picture, plays the
|
||||
// music, and then quits. So if the game is quitting, we shouldn't run the "game over" part.
|
||||
if (shouldQuit())
|
||||
return;
|
||||
|
||||
char szMoves[40];
|
||||
|
||||
clearTextArea();
|
||||
drawPic(42, true, true);
|
||||
|
||||
playTune(4, 25);
|
||||
|
||||
printUserMessage(16);
|
||||
|
||||
printUserMessage(33);
|
||||
|
||||
clearTextArea();
|
||||
|
||||
drawPic(46, true, true);
|
||||
|
||||
Common::sprintf_s(szMoves, IDS_TRO_GAMEOVER_0, _moves);
|
||||
drawStr(21, 1, kColorDefault, szMoves);
|
||||
drawStr(22, 1, kColorDefault, IDS_TRO_GAMEOVER_1);
|
||||
_system->updateScreen();
|
||||
|
||||
pressAnyKey();
|
||||
}
|
||||
|
||||
void TrollEngine::drawTroll() {
|
||||
for (int i = 0; i < IDI_TRO_NUM_NONTROLL; i++)
|
||||
if (_currentRoom == _nonTrollRooms[i]) {
|
||||
_isTrollAway = true;
|
||||
return;
|
||||
}
|
||||
|
||||
drawPic(43, false, false, true);
|
||||
}
|
||||
|
||||
int TrollEngine::drawRoom(char *menu) {
|
||||
bool contFlag = false;
|
||||
|
||||
if (_currentRoom == 1) {
|
||||
clearScreen(0x00, false);
|
||||
} else {
|
||||
|
||||
if (_currentRoom != 42) {
|
||||
if (_roomPicDeltas[_currentRoom]) {
|
||||
contFlag = true;
|
||||
}
|
||||
}
|
||||
|
||||
drawPic(_currentRoom, contFlag, true);
|
||||
_system->updateScreen();
|
||||
|
||||
if (_currentRoom == 42) {
|
||||
drawPic(44, false, false); // don't clear
|
||||
} else {
|
||||
if (!_isTrollAway) {
|
||||
drawTroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_system->updateScreen();
|
||||
|
||||
int n = 0;
|
||||
menu[0] = ' '; // leading space
|
||||
menu[1] = '\0';
|
||||
strncat(menu, (char *)_gameData + _locMessagesIdx[_currentRoom], 39);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (_roomDescs[_roomPicture - 1].options[i]) {
|
||||
strncat(menu, Common::String::format(" %d.", i + 1).c_str(), 4);
|
||||
|
||||
strncat(menu, (char *)_gameData + _options[_roomDescs[_roomPicture - 1].options[i] - 1], 35);
|
||||
menu[(i + 2) * 40 - 1] = ' ';
|
||||
menu[(i + 2) * 40] = '\0';
|
||||
|
||||
n = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
void TrollEngine::playTune(int tune, int len) {
|
||||
if (!_soundOn)
|
||||
return;
|
||||
|
||||
int ptr = _tunes[tune - 1];
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
int freq = READ_LE_UINT16(_gameData + ptr);
|
||||
ptr += 2;
|
||||
int duration = READ_LE_UINT16(_gameData + ptr);
|
||||
ptr += 2;
|
||||
|
||||
// Play note without processing events.
|
||||
// The sounds are so short in this game that we don't
|
||||
// need to process events while playing notes.
|
||||
playSpeakerNote(freq, duration, kWaitBlock);
|
||||
}
|
||||
}
|
||||
|
||||
void TrollEngine::pickupTreasure(int treasureId) {
|
||||
_inventory[IDI_TRO_MAX_TREASURE - _treasuresLeft] = treasureId;
|
||||
|
||||
if (_currentRoom != 24) {
|
||||
clearTextArea();
|
||||
drawPic(_currentRoom, false, true);
|
||||
_system->updateScreen();
|
||||
}
|
||||
|
||||
printUserMessage(treasureId + 16);
|
||||
|
||||
clearTextArea();
|
||||
|
||||
_treasuresLeft--;
|
||||
|
||||
switch (_treasuresLeft) {
|
||||
case 1:
|
||||
drawStr(22, 1, kColorDefault, IDS_TRO_TREASURE_7);
|
||||
break;
|
||||
case 0:
|
||||
drawStr(22, 1, kColorDefault, IDS_TRO_TREASURE_8);
|
||||
drawStr(23, 4, kColorDefault, IDS_TRO_TREASURE_9);
|
||||
|
||||
_roomStates[6] = 1;
|
||||
|
||||
_locMessagesIdx[6] = IDO_TRO_ALLTREASURES;
|
||||
break;
|
||||
default:
|
||||
drawStr(22, 1, kColorDefault, Common::String::format(IDS_TRO_TREASURE_3, _treasuresLeft).c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
pressAnyKey();
|
||||
}
|
||||
|
||||
void TrollEngine::printUserMessage(int msgId) {
|
||||
int i;
|
||||
|
||||
clearTextArea();
|
||||
|
||||
for (i = 0; i < _userMessages[msgId - 1].num; i++) {
|
||||
drawStr(21 + i, 1, kColorDefault, _userMessages[msgId - 1].msg[i]);
|
||||
}
|
||||
|
||||
if (msgId == 34) {
|
||||
for (i = 0; i < 2; i++)
|
||||
playTune(5, 11);
|
||||
}
|
||||
pressAnyKey();
|
||||
}
|
||||
|
||||
void TrollEngine::gameLoop() {
|
||||
bool done = false;
|
||||
char menu[160 + 5];
|
||||
int currentOption, numberOfOptions;
|
||||
int roomParam;
|
||||
int haveFlashlight;
|
||||
|
||||
_moves = 0;
|
||||
_roomPicture = 1;
|
||||
_treasuresLeft = IDI_TRO_MAX_TREASURE;
|
||||
haveFlashlight = false;
|
||||
_currentRoom = 0;
|
||||
_isTrollAway = true;
|
||||
_soundOn = true;
|
||||
|
||||
memset(_roomStates, 0, sizeof(_roomStates));
|
||||
|
||||
memset(_inventory, 0, sizeof(_inventory));
|
||||
|
||||
while (!done && !shouldQuit()) {
|
||||
currentOption = 0;
|
||||
|
||||
numberOfOptions = drawRoom(menu);
|
||||
|
||||
if (getMenuSel(menu, ¤tOption, numberOfOptions)) {
|
||||
_moves++;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
roomParam = _roomDescs[_roomPicture - 1].roomDescIndex[currentOption];
|
||||
|
||||
switch (_roomDescs[_roomPicture - 1].optionTypes[currentOption]) {
|
||||
case OT_FLASHLIGHT:
|
||||
if (!haveFlashlight) {
|
||||
printUserMessage(13);
|
||||
break;
|
||||
}
|
||||
// fall through
|
||||
case OT_GO:
|
||||
_currentRoom = roomParam;
|
||||
_roomPicture = _roomPicStartIdx[_currentRoom];
|
||||
_roomPicture += _roomStates[_currentRoom];
|
||||
|
||||
if (_currentRoom < 6 || _treasuresLeft == 0) {
|
||||
_isTrollAway = true;
|
||||
} else { // make odd 1:3
|
||||
_isTrollAway = (rnd(3) != 2);
|
||||
}
|
||||
break;
|
||||
case OT_GET:
|
||||
if (!_isTrollAway) {
|
||||
printUserMessage(34);
|
||||
} else {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
playTune(1, 3);
|
||||
// delayMillis()
|
||||
}
|
||||
|
||||
_roomStates[_currentRoom] = 1;
|
||||
_roomPicDeltas[_currentRoom] = 0;
|
||||
|
||||
_roomPicture++;
|
||||
|
||||
if (_roomConnects[roomParam - 1] != 0xff) {
|
||||
_roomStates[_roomConnects[roomParam - 1]] = 1;
|
||||
}
|
||||
|
||||
if (roomParam == 1)
|
||||
haveFlashlight = true;
|
||||
|
||||
_locMessagesIdx[_currentRoom] = IDO_TRO_LOCMESSAGES +
|
||||
(roomParam + 42) * 39;
|
||||
|
||||
pickupTreasure(roomParam);
|
||||
}
|
||||
break;
|
||||
case OT_DO:
|
||||
if (roomParam != 16) {
|
||||
printUserMessage(roomParam);
|
||||
break;
|
||||
}
|
||||
|
||||
done = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TrollEngine::fillOffsets() {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < IDI_TRO_PICNUM; i++)
|
||||
_pictureOffsets[i] = READ_LE_UINT16(_gameData + IDO_TRO_PIC_START + i * 2);
|
||||
|
||||
for (i = 0; i < IDI_TRO_NUM_OPTIONS; i++)
|
||||
_options[i] = READ_LE_UINT16(_gameData + IDO_TRO_OPTIONS + i * 2);
|
||||
|
||||
for (i = 0; i < IDI_TRO_NUM_NUMROOMS; i++) {
|
||||
_roomPicStartIdx[i] = _gameData[IDO_TRO_PICSTARTIDX + i];
|
||||
_roomPicDeltas[i] = _gameData[IDO_TRO_ROOMPICDELTAS + i];
|
||||
_roomConnects[i] = _gameData[IDO_TRO_ROOMCONNECTS + i];
|
||||
}
|
||||
|
||||
for (i = 0; i < IDI_TRO_NUM_LOCDESCS; i++)
|
||||
_locMessagesIdx[i] = IDO_TRO_LOCMESSAGES + i * 39;
|
||||
|
||||
int start = READ_LE_UINT16(_gameData + IDO_TRO_ROOMDESCS);
|
||||
int ptr;
|
||||
int j;
|
||||
|
||||
for (i = 0; i < IDI_TRO_NUM_ROOMDESCS; i++, start += 2) {
|
||||
ptr = READ_LE_UINT16(_gameData + start);
|
||||
|
||||
for (j = 0; j < 3; j++)
|
||||
_roomDescs[i].options[j] = _gameData[ptr++];
|
||||
|
||||
for (j = 0; j < 3; j++) {
|
||||
switch (_gameData[ptr++]) {
|
||||
case 0:
|
||||
_roomDescs[i].optionTypes[j] = OT_GO;
|
||||
break;
|
||||
case 1:
|
||||
_roomDescs[i].optionTypes[j] = OT_GET;
|
||||
break;
|
||||
case 2:
|
||||
_roomDescs[i].optionTypes[j] = OT_DO;
|
||||
break;
|
||||
case 3:
|
||||
_roomDescs[i].optionTypes[j] = OT_FLASHLIGHT;
|
||||
break;
|
||||
default:
|
||||
error("Bad data @ (%x) %d", ptr - 1, i);
|
||||
}
|
||||
}
|
||||
|
||||
for (j = 0; j < 3; j++)
|
||||
_roomDescs[i].roomDescIndex[j] = _gameData[ptr++];
|
||||
}
|
||||
|
||||
start = IDO_TRO_USERMESSAGES;
|
||||
|
||||
for (i = 0; i < IDI_TRO_NUM_USERMSGS; i++, start += 2) {
|
||||
ptr = READ_LE_UINT16(_gameData + start);
|
||||
|
||||
_userMessages[i].num = _gameData[ptr++];
|
||||
|
||||
for (j = 0; j < _userMessages[i].num; j++, ptr += 39) {
|
||||
memcpy(_userMessages[i].msg[j], _gameData + ptr, 39);
|
||||
_userMessages[i].msg[j][39] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
start = IDO_TRO_ITEMS;
|
||||
|
||||
for (i = 0; i < IDI_TRO_MAX_TREASURE; i++, start += 2) {
|
||||
ptr = READ_LE_UINT16(_gameData + start);
|
||||
_items[i].bg = _gameData[ptr++];
|
||||
_items[i].fg = _gameData[ptr++];
|
||||
memcpy(_items[i].name, _gameData + ptr, 15);
|
||||
_items[i].name[15] = 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < IDI_TRO_NUM_NONTROLL; i++)
|
||||
_nonTrollRooms[i] = _gameData[IDO_TRO_NONTROLLROOMS + i];
|
||||
|
||||
_tunes[0] = 0x3BFD;
|
||||
_tunes[1] = 0x3C09;
|
||||
_tunes[2] = 0x3C0D;
|
||||
_tunes[3] = 0x3C11;
|
||||
_tunes[4] = 0x3C79;
|
||||
_tunes[5] = 0x3CA5;
|
||||
}
|
||||
|
||||
// Init
|
||||
|
||||
void TrollEngine::init() {
|
||||
_picture = new PictureMgr_Troll(this, _gfx);
|
||||
//SetScreenPar(320, 200, (char *)ibm_fontdata);
|
||||
|
||||
const int gaps[] = { 0x3A40, 0x4600, 0x4800, 0x5800, 0x5a00, 0x6a00,
|
||||
0x6c00, 0x7400, 0x7600, 0x7c00, 0x7e00, 0x8e00,
|
||||
0x9000, 0xa000, 0xa200, 0xb200, 0xb400, 0xc400,
|
||||
0xc600, 0xd600, 0xd800, 0xe800, 0xea00, 0xfa00,
|
||||
0xfc00, 0x10c00, 0x10e00, 0x11e00, 0x12000, 0x13000
|
||||
};
|
||||
|
||||
Common::File infile;
|
||||
if (!infile.open(IDA_TRO_BINNAME))
|
||||
return;
|
||||
|
||||
_gameData = (byte *)malloc(0xD9C0);
|
||||
|
||||
bool flip = true;
|
||||
byte *ptr = _gameData;
|
||||
int diff;
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(gaps) - 1; i++) {
|
||||
diff = gaps[i + 1] - gaps[i];
|
||||
|
||||
if (flip) {
|
||||
infile.seek(gaps[i]);
|
||||
infile.read(ptr, diff);
|
||||
ptr += diff;
|
||||
} else {
|
||||
}
|
||||
flip = !flip;
|
||||
}
|
||||
|
||||
// One sector is off
|
||||
infile.seek(0x18470);
|
||||
infile.read(_gameData + 15632, 592);
|
||||
|
||||
infile.close();
|
||||
|
||||
fillOffsets();
|
||||
}
|
||||
|
||||
Common::Error TrollEngine::go() {
|
||||
init();
|
||||
|
||||
while (!shouldQuit()) {
|
||||
intro();
|
||||
gameLoop();
|
||||
gameOver();
|
||||
}
|
||||
|
||||
return Common::kNoError;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
228
engines/agi/preagi/troll.h
Normal file
228
engines/agi/preagi/troll.h
Normal file
@@ -0,0 +1,228 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PREAGI_TROLL_H
|
||||
#define AGI_PREAGI_TROLL_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// strings
|
||||
|
||||
#define IDS_TRO_DISK "ERROR ERROR !"
|
||||
#define IDS_TRO_PATH_PIC "%s"
|
||||
|
||||
#define IDS_TRO_PRESSANYKEY "PRESS ANY KEY TO CONTINUE:"
|
||||
|
||||
#define IDS_TRO_INTRO_0 "SIERRA ON-LINE INC."
|
||||
#define IDS_TRO_INTRO_1 "Presents :"
|
||||
#define IDS_TRO_INTRO_2 "Copyright 1984 Sierra On-Line Inc."
|
||||
#define IDS_TRO_INTRO_3 " Press any key to continue. "
|
||||
#define IDS_TRO_INTRO_4 "HAVE YOU PLAYED THIS GAME BEFORE ?"
|
||||
#define IDS_TRO_INTRO_5 "PRESS <Y> OR <N>"
|
||||
|
||||
#define IDS_TRO_TUTORIAL_0 " First press the <space bar>.\n 1. Turn the screen GREEN.\n 2. Turn the screen BLACK.\n *3. SEE a SURPRISE, and then more."
|
||||
#define IDS_TRO_TUTORIAL_1 " Press <return> to make your choice.\n 1. Turn the screen GREEN.\n 2. Turn the screen BLACK.\n 3. SEE a SURPRISE, and then more."
|
||||
//#define IDS_TRO_TUTORIAL_0 "First press the <space bar>."
|
||||
//#define IDS_TRO_TUTORIAL_1 "1. Turn the screen GREEN."
|
||||
//#define IDS_TRO_TUTORIAL_2 "2. Turn the screen BLACK."
|
||||
//#define IDS_TRO_TUTORIAL_3 "3. SEE a SURPRISE, and then more."
|
||||
//#define IDS_TRO_TUTORIAL_4 "Press <return> to make your choice."
|
||||
#define IDS_TRO_TUTORIAL_5 "Would you like more practice ?"
|
||||
#define IDS_TRO_TUTORIAL_6 "Press <Y> for yes, <N> for no."
|
||||
#define IDS_TRO_TUTORIAL_7 "The evil TROLL has hidden all the"
|
||||
#define IDS_TRO_TUTORIAL_8 "Treasures of MARK, the Dwarf King."
|
||||
#define IDS_TRO_TUTORIAL_9 "Help KING MARK find his Treasures."
|
||||
#define IDS_TRO_TUTORIAL_10 "You can't take a Treasure if the TROLL"
|
||||
#define IDS_TRO_TUTORIAL_11 "is in the same picture as the Treasure."
|
||||
#define IDS_TRO_TUTORIAL_12 "To make the TROLL go away you have to"
|
||||
#define IDS_TRO_TUTORIAL_13 "make the picture change."
|
||||
#define IDS_TRO_TUTORIAL_14 "During the game see the Treasures you"
|
||||
#define IDS_TRO_TUTORIAL_15 "have already found by pressing <F>."
|
||||
#define IDS_TRO_TUTORIAL_16 "During the game you can turn the sound"
|
||||
#define IDS_TRO_TUTORIAL_17 "on or off by pressing the <S> key "
|
||||
#define IDS_TRO_TUTORIAL_18 "while holding down the <Ctrl> key."
|
||||
#define IDS_TRO_TUTORIAL_19 "The TROLL has hidden these Treasures:"
|
||||
|
||||
#define IDS_TRO_CREDITS_0 "Prepare to enter the world of . . ."
|
||||
#define IDS_TRO_CREDITS_1 "TROLL'S TALE (tm)"
|
||||
#define IDS_TRO_CREDITS_2 "------------"
|
||||
#define IDS_TRO_CREDITS_3 "Written by MIKE MACCHESNEY"
|
||||
#define IDS_TRO_CREDITS_4 "Conversion by PETER OLIPHANT"
|
||||
#define IDS_TRO_CREDITS_5 "Graphic Art by DOUG MACNEILL"
|
||||
#define IDS_TRO_CREDITS_6 "Original Version by AL LOWE"
|
||||
|
||||
#define IDS_TRO_TREASURE_0 "TREASURES FOUND"
|
||||
#define IDS_TRO_TREASURE_1 "---------------"
|
||||
#define IDS_TRO_TREASURE_2 "NONE"
|
||||
#define IDS_TRO_TREASURE_3 "THERE ARE STILL %d TREASURES TO FIND"
|
||||
#define IDS_TRO_TREASURE_4 "%d TREASURES TO FIND"
|
||||
#define IDS_TRO_TREASURE_5 "%d TREASURE TO FIND"
|
||||
#define IDS_TRO_TREASURE_6 "YOU HAVE FOUND ALL OF THE TREASURES!!"
|
||||
#define IDS_TRO_TREASURE_7 "THERE'S ONLY ONE MORE TREASURE TO FIND."
|
||||
#define IDS_TRO_TREASURE_8 "GREAT!! YOU HAVE FOUND EVERY TREASURE."
|
||||
#define IDS_TRO_TREASURE_9 "TAKE THE TREASURES TO THE GUARD."
|
||||
|
||||
#define IDS_TRO_GAMEOVER_0 "You took %d moves to complete TROLL'S"
|
||||
#define IDS_TRO_GAMEOVER_1 "TALE. Do you think you can do better?"
|
||||
|
||||
// picture
|
||||
|
||||
#define IDI_TRO_PICNUM 47
|
||||
|
||||
#define IDI_TRO_PIC_WIDTH 160
|
||||
#define IDI_TRO_PIC_HEIGHT 168
|
||||
#define IDI_TRO_PIC_X0 0
|
||||
#define IDI_TRO_PIC_Y0 0
|
||||
#define IDI_TRO_PIC_FLAGS IDF_AGI_PIC_V15
|
||||
|
||||
// max values
|
||||
|
||||
#define IDI_TRO_MAX_TREASURE 16
|
||||
#define IDI_TRO_MAX_OPTION 3
|
||||
|
||||
#define IDI_TRO_SEL_OPTION_1 0
|
||||
#define IDI_TRO_SEL_OPTION_2 1
|
||||
#define IDI_TRO_SEL_OPTION_3 2
|
||||
|
||||
#define IDI_TRO_MAX_ROW_PIC 21
|
||||
|
||||
#define IDI_TRO_NUM_ROOMDESCS 65
|
||||
#define IDI_TRO_NUM_OPTIONS 129
|
||||
#define IDI_TRO_NUM_NUMROOMS 43
|
||||
|
||||
#define IDI_TRO_NUM_USERMSGS 34
|
||||
|
||||
#define IDI_TRO_NUM_LOCDESCS 59
|
||||
|
||||
#define IDI_TRO_NUM_NONTROLL 9
|
||||
|
||||
// offsets
|
||||
|
||||
#define IDA_TRO_BINNAME "troll.img"
|
||||
|
||||
#define IDO_TRO_DATA_START 0x3A40
|
||||
#define IDO_TRO_PIC_START 0x3EF5
|
||||
#define IDO_TRO_LOCMESSAGES 0x1F7C
|
||||
#define IDO_TRO_USERMESSAGES 0x34A4
|
||||
#define IDO_TRO_ROOMDESCS 0x0082
|
||||
#define IDO_TRO_OPTIONS 0x0364
|
||||
#define IDO_TRO_PICSTARTIDX 0x02CD
|
||||
#define IDO_TRO_ROOMPICDELTAS 0x030C
|
||||
#define IDO_TRO_ALLTREASURES 0x3B24
|
||||
#define IDO_TRO_ITEMS 0x34E8
|
||||
#define IDO_TRO_FRAMEPIC 0x3EC2
|
||||
#define IDO_TRO_ROOMCONNECTS 0x02FA
|
||||
#define IDO_TRO_NONTROLLROOMS 0x3CF9
|
||||
|
||||
enum OptionType {
|
||||
OT_GO,
|
||||
OT_GET,
|
||||
OT_DO,
|
||||
OT_FLASHLIGHT
|
||||
};
|
||||
|
||||
struct RoomDesc {
|
||||
int options[3];
|
||||
OptionType optionTypes[3];
|
||||
int roomDescIndex[3];
|
||||
};
|
||||
|
||||
struct UserMsg {
|
||||
int num;
|
||||
char msg[3][40];
|
||||
};
|
||||
|
||||
struct Item {
|
||||
byte bg;
|
||||
byte fg;
|
||||
char name[16];
|
||||
};
|
||||
|
||||
class PictureMgr_Troll;
|
||||
|
||||
class TrollEngine : public PreAgiEngine {
|
||||
public:
|
||||
TrollEngine(OSystem *syst, const AGIGameDescription *gameDesc);
|
||||
~TrollEngine() override;
|
||||
|
||||
Common::Error go() override;
|
||||
|
||||
private:
|
||||
PictureMgr_Troll *_picture;
|
||||
|
||||
int _roomPicture;
|
||||
int _treasuresLeft;
|
||||
int _currentRoom;
|
||||
int _moves;
|
||||
|
||||
bool _isTrollAway;
|
||||
|
||||
int _inventory[IDI_TRO_MAX_TREASURE];
|
||||
|
||||
bool _soundOn;
|
||||
|
||||
byte *_gameData;
|
||||
|
||||
void init();
|
||||
void intro();
|
||||
void drawPic(int iPic, bool f3IsCont, bool clear, bool troll = false);
|
||||
void drawTroll();
|
||||
void gameLoop();
|
||||
void gameOver();
|
||||
void tutorial();
|
||||
void credits();
|
||||
|
||||
void inventory();
|
||||
void pickupTreasure(int treasureId);
|
||||
|
||||
int drawRoom(char *menu);
|
||||
void printUserMessage(int msgId);
|
||||
|
||||
void pressAnyKey(int col = 4);
|
||||
void waitAnyKeyIntro();
|
||||
|
||||
void playTune(int tune, int len);
|
||||
|
||||
bool getMenuSel(const char *, int *, int);
|
||||
|
||||
void drawMenu(const char *szMenu, int iSel);
|
||||
|
||||
void fillOffsets();
|
||||
|
||||
// These are come from game data
|
||||
int _pictureOffsets[IDI_TRO_PICNUM];
|
||||
int _roomPicStartIdx[IDI_TRO_NUM_NUMROOMS];
|
||||
int _roomPicDeltas[IDI_TRO_NUM_NUMROOMS];
|
||||
int _roomStates[IDI_TRO_NUM_NUMROOMS];
|
||||
UserMsg _userMessages[IDI_TRO_NUM_USERMSGS];
|
||||
int _locMessagesIdx[IDI_TRO_NUM_LOCDESCS];
|
||||
RoomDesc _roomDescs[IDI_TRO_NUM_ROOMDESCS];
|
||||
int _options[IDI_TRO_NUM_OPTIONS];
|
||||
Item _items[IDI_TRO_MAX_TREASURE];
|
||||
int _roomConnects[IDI_TRO_NUM_OPTIONS];
|
||||
int _nonTrollRooms[IDI_TRO_NUM_NONTROLL];
|
||||
|
||||
int _tunes[6];
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif
|
||||
1552
engines/agi/preagi/winnie.cpp
Normal file
1552
engines/agi/preagi/winnie.cpp
Normal file
File diff suppressed because it is too large
Load Diff
375
engines/agi/preagi/winnie.h
Normal file
375
engines/agi/preagi/winnie.h
Normal file
@@ -0,0 +1,375 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_PREAGI_WINNIE_H
|
||||
#define AGI_PREAGI_WINNIE_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define WTP_SAVEGAME_VERSION 1
|
||||
#define IDI_XOR_KEY 0x80
|
||||
|
||||
// strings
|
||||
#define IDS_WTP_ROOM_DOS "rooms/rm.%02d"
|
||||
#define IDS_WTP_ROOM_AMIGA "rooms/room.%d"
|
||||
#define IDS_WTP_ROOM_C64 "room%02d"
|
||||
#define IDS_WTP_ROOM_APPLE "room%d.obj"
|
||||
#define IDS_WTP_ROOM_COCO "room%02d"
|
||||
#define IDS_WTP_OBJ_DOS "obj.%02d"
|
||||
#define IDS_WTP_OBJ_AMIGA "objects/object.%d"
|
||||
#define IDS_WTP_OBJ_C64 "object%02d"
|
||||
#define IDS_WTP_OBJ_APPLE "object%d.obj"
|
||||
#define IDS_WTP_OBJ_COCO "obj%02d"
|
||||
#define IDS_WTP_SND_DOS "snd.%02d"
|
||||
#define IDS_WTP_SND_AMIGA "Sounds"
|
||||
#define IDS_WTP_SND_C64 "sound.obj"
|
||||
#define IDS_WTP_SND_APPLE "sound.obj"
|
||||
|
||||
#define IDS_WTP_FILE_LOGO "logo"
|
||||
#define IDS_WTP_FILE_TITLE "title"
|
||||
#define IDS_WTP_FILE_SAVEGAME "savegame"
|
||||
#define IDS_WTP_FILE_RND "rnd"
|
||||
|
||||
#define IDS_WTP_DISK_ERROR "There is a problem with your disk drive.Please make sure your Winnie-the-Pooh disk is in the drive correctly."
|
||||
|
||||
#define IDS_WTP_INTRO_0 " PRESENT"
|
||||
#define IDS_WTP_INTRO_1 " TM designates trademark of\n Sierra On-Line, Inc.\n (c) 1985 Walt Disney Productions"
|
||||
|
||||
#define IDS_WTP_HELP_0 "The <SPACE BAR> moves the pointer. Press <RETURN> when it is by the choice you want. Press the <Backspace> key to see what you just finished reading."
|
||||
#define IDS_WTP_HELP_1 "Press <C> to see what you are carrying. <Ctrl-S> turns the sound off and on. <ESC> takes you to the playroom (in caseyou get lost or want to save the game)."
|
||||
|
||||
#define IDS_WTP_GAME_OVER_0 "Congratulations!! You did it! You returned everything that was lost. Now,Christopher Robin invites you to a Hero party."
|
||||
#define IDS_WTP_GAME_OVER_1 "The good news is: YOU are the Hero!! The bad news is: you have to find the party by yourself. Good luck!"
|
||||
|
||||
#define IDS_WTP_OWL_0 "\"For example, that object you are carrying now is interesting. I know I've seen it before. Hmmm. Let me think about this . . .\""
|
||||
#define IDS_WTP_OWL_1 "\"You know, this object here beside me isfamiliar. I'm sure I could give you some sort of clue about it. Let me see. . .\""
|
||||
|
||||
#define IDS_WTP_WIND_0 "Oh, no! The Blustery Wind begins to howl. It has returned, and mixed up all the objects in the Wood."
|
||||
#define IDS_WTP_WIND_1 "But don't worry. Everyone still has theobjects you returned to them.\n\n (Today must be Winds-day!)"
|
||||
#define IDS_WTP_TIGGER "\"Hallooooo, there!!!! It's ME, Tigger! Let's BOUNCE!\""
|
||||
#define IDS_WTP_MIST "Oh, look out! The mysterious mist is coming in. It gets so thick that you can't see through it. Just keep walkingand it will soon clear up."
|
||||
|
||||
#define IDS_WTP_SONG_0 "Winnie-the-Pooh, Winnie-the-Pooh, Tubby little cubby all stuffed with fluff, He's Winnie-the-Pooh, Winnie-the-Pooh, Willy, nilly, silly, old bear."
|
||||
#define IDS_WTP_SONG_1 "Deep in the Hundred Acre Wood, Where Christopher Robin plays, You will find the enchanted neighborhoodof Christopher's childhood days."
|
||||
#define IDS_WTP_SONG_2 "A donkey named Eeyore is his friend, and Kanga and little Roo. There's Rabbit and Piglet and there's Owl But most of all Winnie-the-Pooh!"
|
||||
|
||||
#define IDS_WTP_NSEW "North South East West"
|
||||
#define IDS_WTP_TAKE "Take"
|
||||
#define IDS_WTP_DROP "Drop"
|
||||
#define IDS_WTP_CANT_GO "\nSorry, but you can't go that way."
|
||||
#define IDS_WTP_CANT_TAKE "You can't take it. You can only carry one object at a time."
|
||||
#define IDS_WTP_CANT_DROP "You can't drop it. Another object is already here."
|
||||
#define IDS_WTP_WRONG_PLACE "\nOk, but it doesn't belong here."
|
||||
#define IDS_WTP_OK "\nOk."
|
||||
|
||||
#define IDS_WTP_INVENTORY_0 "You are carrying nothing."
|
||||
#define IDS_WTP_INVENTORY_1 "Number of objects still missing: %d"
|
||||
|
||||
// COMMODORE 64 version strings
|
||||
|
||||
#define IDS_WTP_FILE_SAVEGAME_C64 "saved game"
|
||||
#define IDS_WTP_DISK_ERROR_C64 "There is a problem with your disk drive.Please make sure your disk is in the drive correctly."
|
||||
#define IDS_WTP_HELP_0_C64 "The <SPACE BAR> moves the pointer. Press <RETURN> when it is by the choice you want. <F1> brings back what you have already read."
|
||||
#define IDS_WTP_HELP_1_C64 "<F3> takes you back to the playroom (if you get lost, or want to save the game).<F5> turns the sound off and on. <F7> shows what you're carrying."
|
||||
#define IDS_WTP_WRONG_PLACE_C64 "\nOk, but this is not the right place."
|
||||
|
||||
// Amiga strings
|
||||
|
||||
// original strings from the executable and edited versions for our four-line interface
|
||||
#define IDS_WTP_AMIGA_HELP_0 "Use the mouse to select your choice from\nthe bottom of the screen or the menu.\n\nTo use the keyboard instead of the\nmouse, press the NUMBER or first LETTER\nof what you want."
|
||||
#define IDS_WTP_AMIGA_HELP_1 "From the keyboard you can also press:\n 'l' to look at the scene you're in.\n 'o' to look at an object in the room.\n 'c' to see what you're carrying and\n how you're doing.\n CTRL-S to turn the sound on or off."
|
||||
#define IDS_WTP_AMIGA_HELP_EDITED_0 "Use the mouse to select your choice fromthe bottom of the screen or the menu.\nTo use the keyboard instead of the\nmouse, press the NUMBER or first LETTER."
|
||||
#define IDS_WTP_AMIGA_HELP_EDITED_1 "From the keyboard you can also press:\n <C> to see what you're carrying and\n how you're doing.\n <CTRL-S> to turn the sound on or off."
|
||||
|
||||
// maximum values
|
||||
|
||||
#define IDI_WTP_MAX_OBJ_MISSING 10
|
||||
|
||||
#define IDI_WTP_MAX_ROOM 62
|
||||
#define IDI_WTP_MAX_OBJ 40
|
||||
#define IDI_WTP_MAX_SND 14
|
||||
#define IDI_WTP_MAX_PIC 2
|
||||
|
||||
#define IDI_WTP_MAX_ROOM_NORMAL 57
|
||||
#define IDI_WTP_MAX_ROOM_TELEPORT 30
|
||||
#define IDI_WTP_MAX_ROOM_OBJ 42
|
||||
#define IDI_WTP_MAX_BLOCK 4
|
||||
#define IDI_WTP_MAX_STR 6
|
||||
#define IDI_WTP_MAX_OBJ_STR 4
|
||||
#define IDI_WTP_MAX_OBJ_STR_END 2
|
||||
#define IDI_WTP_MAX_FLAG 40
|
||||
#define IDI_WTP_MAX_OPTION 3
|
||||
#define IDI_WTP_MAX_DIR 4
|
||||
#define IDI_WTP_MAX_MOVES_UNTIL_WIND 150
|
||||
|
||||
#define IDI_WTP_TIMER_INTERVAL (10 * 60 * 1000) // 10 minutes on DOS
|
||||
|
||||
// positions
|
||||
|
||||
#define IDI_WTP_ROW_MENU 21
|
||||
#define IDI_WTP_ROW_OPTION_1 21
|
||||
#define IDI_WTP_ROW_OPTION_2 22
|
||||
#define IDI_WTP_ROW_OPTION_3 23
|
||||
#define IDI_WTP_ROW_OPTION_4 24
|
||||
|
||||
#define IDI_WTP_COL_MENU 0
|
||||
#define IDI_WTP_COL_OPTION 1
|
||||
#define IDI_WTP_COL_NSEW 1
|
||||
#define IDI_WTP_COL_NORTH 1
|
||||
#define IDI_WTP_COL_SOUTH 8
|
||||
#define IDI_WTP_COL_EAST 15
|
||||
#define IDI_WTP_COL_WEST 21
|
||||
#define IDI_WTP_COL_TAKE 29
|
||||
#define IDI_WTP_COL_DROP 35
|
||||
#define IDI_WTP_COL_PRESENT 17
|
||||
|
||||
// data file offset modifiers
|
||||
|
||||
#define IDI_WTP_OFS_ROOM 0x5400
|
||||
#define IDI_WTP_OFS_OBJ 0x0800
|
||||
|
||||
// picture
|
||||
|
||||
#define IDI_WTP_PIC_WIDTH 140
|
||||
#define IDI_WTP_PIC_HEIGHT 159
|
||||
#define IDI_WTP_PIC_X0 10
|
||||
#define IDI_WTP_PIC_Y0 0
|
||||
#define IDI_WTP_PIC_FLAGS IDF_AGI_PIC_V2
|
||||
|
||||
// selections
|
||||
|
||||
enum {
|
||||
IDI_WTP_SEL_HOME = -2,
|
||||
IDI_WTP_SEL_BACK,
|
||||
IDI_WTP_SEL_OPT_1,
|
||||
IDI_WTP_SEL_OPT_2,
|
||||
IDI_WTP_SEL_OPT_3,
|
||||
IDI_WTP_SEL_NORTH,
|
||||
IDI_WTP_SEL_SOUTH,
|
||||
IDI_WTP_SEL_EAST,
|
||||
IDI_WTP_SEL_WEST,
|
||||
IDI_WTP_SEL_TAKE,
|
||||
IDI_WTP_SEL_DROP,
|
||||
IDI_WTP_SEL_REAL_OPT_1,
|
||||
IDI_WTP_SEL_REAL_OPT_2,
|
||||
IDI_WTP_SEL_REAL_OPT_3,
|
||||
IDI_WTP_SEL_TIMER_EVENT
|
||||
};
|
||||
|
||||
#define IDI_WTP_SEL_LAST IDI_WTP_SEL_REAL_OPT_3
|
||||
|
||||
// rooms
|
||||
|
||||
enum {
|
||||
IDI_WTP_ROOM_NONE = -1,
|
||||
IDI_WTP_ROOM_NORTH,
|
||||
IDI_WTP_ROOM_SOUTH,
|
||||
IDI_WTP_ROOM_EAST,
|
||||
IDI_WTP_ROOM_WEST
|
||||
};
|
||||
|
||||
#define IDI_WTP_ROOM_HIDE 0
|
||||
|
||||
#define IDI_WTP_ROOM_PICNIC 2
|
||||
#define IDI_WTP_ROOM_HOME 28
|
||||
#define IDI_WTP_ROOM_PARTY 58
|
||||
#define IDI_WTP_ROOM_MIST 59
|
||||
#define IDI_WTP_ROOM_TIGGER 61
|
||||
|
||||
// sound
|
||||
|
||||
enum ENUM_WTP_SOUND {
|
||||
IDI_WTP_SND_POOH_0 = 1,
|
||||
IDI_WTP_SND_TIGGER,
|
||||
IDI_WTP_SND_TAKE,
|
||||
IDI_WTP_SND_DROP,
|
||||
IDI_WTP_SND_DROP_OK,
|
||||
IDI_WTP_SND_FANFARE,
|
||||
IDI_WTP_SND_POOH_1,
|
||||
IDI_WTP_SND_KEYHELP,
|
||||
IDI_WTP_SND_POOH_2,
|
||||
IDI_WTP_SND_WIND_0,
|
||||
IDI_WTP_SND_WIND_1
|
||||
};
|
||||
|
||||
// script opcodes
|
||||
|
||||
#define IDO_WTP_GOTO_ROOM 0x06
|
||||
#define IDO_WTP_PRINT_MSG 0x08
|
||||
#define IDO_WTP_PRINT_STR 0x0A
|
||||
#define IDO_WTP_DROP_OBJ 0x0C
|
||||
#define IDO_WTP_FLAG_CLEAR 0x0E
|
||||
#define IDO_WTP_FLAG_SET 0x10
|
||||
#define IDO_WTP_GAME_OVER 0x12
|
||||
#define IDO_WTP_WALK_MIST 0x14
|
||||
#define IDO_WTP_PLAY_SOUND 0x16
|
||||
#define IDO_WTP_SAVE_GAME 0x18
|
||||
#define IDO_WTP_LOAD_GAME 0x1A
|
||||
#define IDO_WTP_OWL_HELP 0x1C
|
||||
#define IDO_WTP_GOTO_RND 0x1E
|
||||
|
||||
#define IDO_WTP_OPTION_0 0x15
|
||||
#define IDO_WTP_OPTION_1 0x16
|
||||
#define IDO_WTP_OPTION_2 0x17
|
||||
|
||||
enum {
|
||||
IDI_WTP_OBJ_DESC = 0,
|
||||
IDI_WTP_OBJ_TAKE,
|
||||
IDI_WTP_OBJ_DROP,
|
||||
IDI_WTP_OBJ_HELP
|
||||
};
|
||||
|
||||
enum {
|
||||
IDI_WTP_PAR_OK = 0,
|
||||
IDI_WTP_PAR_GOTO,
|
||||
IDI_WTP_PAR_BACK,
|
||||
IDI_WTP_PAR_RELOAD
|
||||
};
|
||||
|
||||
// room file option block
|
||||
|
||||
struct WTP_ROOM_BLOCK {
|
||||
uint16 ofsOpt[IDI_WTP_MAX_BLOCK];
|
||||
};
|
||||
|
||||
// room file header
|
||||
|
||||
struct WTP_ROOM_HDR {
|
||||
uint8 roomNumber;
|
||||
uint8 objId;
|
||||
uint16 ofsPic;
|
||||
uint16 fileLen;
|
||||
uint16 reserved0;
|
||||
int8 roomNew[IDI_WTP_MAX_DIR];
|
||||
uint8 objX;
|
||||
uint8 objY;
|
||||
uint16 reserved1;
|
||||
uint16 ofsDesc[IDI_WTP_MAX_BLOCK];
|
||||
uint16 ofsBlock[IDI_WTP_MAX_BLOCK];
|
||||
uint16 ofsStr[IDI_WTP_MAX_STR];
|
||||
uint32 reserved2;
|
||||
WTP_ROOM_BLOCK opt[IDI_WTP_MAX_BLOCK];
|
||||
};
|
||||
|
||||
// object file header
|
||||
|
||||
struct WTP_OBJ_HDR {
|
||||
uint16 fileLen;
|
||||
uint16 objId;
|
||||
uint16 ofsEndStr[IDI_WTP_MAX_OBJ_STR_END];
|
||||
uint16 ofsStr[IDI_WTP_MAX_OBJ_STR];
|
||||
uint16 ofsPic;
|
||||
};
|
||||
|
||||
// savegame
|
||||
|
||||
struct WTP_SAVE_GAME {
|
||||
uint8 fSound;
|
||||
uint8 nMoves;
|
||||
uint8 nObjMiss;
|
||||
uint8 nObjRet;
|
||||
uint8 iObjHave;
|
||||
uint8 fGame[IDI_WTP_MAX_FLAG];
|
||||
uint8 iUsedObj[IDI_WTP_MAX_OBJ_MISSING];
|
||||
uint8 iObjRoom[IDI_WTP_MAX_ROOM_OBJ];
|
||||
};
|
||||
|
||||
class PreAgiEngine;
|
||||
class PictureMgr_Mickey_Winnie;
|
||||
|
||||
class WinnieEngine : public PreAgiEngine {
|
||||
public:
|
||||
WinnieEngine(OSystem *syst, const AGIGameDescription *gameDesc);
|
||||
~WinnieEngine() override;
|
||||
|
||||
void init();
|
||||
Common::Error go() override;
|
||||
|
||||
void debugCurRoom();
|
||||
|
||||
private:
|
||||
PictureMgr_Mickey_Winnie *_picture;
|
||||
|
||||
WTP_SAVE_GAME _gameStateWinnie;
|
||||
int _room;
|
||||
int _mist;
|
||||
bool _doWind;
|
||||
bool _tiggerOrMist;
|
||||
bool _timerEnabled;
|
||||
uint32 _timerStart;
|
||||
|
||||
int _roomOffset;
|
||||
int _objOffset;
|
||||
bool _isBigEndian;
|
||||
Common::Rect hotspotNorth, hotspotSouth, hotspotEast, hotspotWest;
|
||||
|
||||
void randomize();
|
||||
void intro();
|
||||
void drawPic(const char *);
|
||||
void gameLoop();
|
||||
|
||||
void parseRoomHeader(WTP_ROOM_HDR *roomHdr, byte *buffer, int len);
|
||||
void parseObjHeader(WTP_OBJ_HDR *objHdr, byte *buffer, int len);
|
||||
uint32 readRoom(int, uint8 *, WTP_ROOM_HDR &);
|
||||
|
||||
void drawRoomPic();
|
||||
int parser(int, int, uint8 *);
|
||||
int getObjInRoom(int);
|
||||
void getMenuSel(char *, int *, int[]);
|
||||
void keyHelp();
|
||||
void clrMenuSel(int *, int[]);
|
||||
void incMenuSel(int *, int[]);
|
||||
void decMenuSel(int *, int[]);
|
||||
void drawMenu(char *, int, int[]);
|
||||
void printRoomStr(int, int);
|
||||
void inventory();
|
||||
void printObjStr(int, int);
|
||||
uint32 readObj(int, uint8 *);
|
||||
void takeObj(int);
|
||||
bool dropObj(int);
|
||||
bool isRightObj(int, int, int *);
|
||||
void drawObjPic(int, int, int);
|
||||
void getMenuMouseSel(int *, int[], int, int);
|
||||
void setWinnieFlag(int);
|
||||
void clearWinnieFlag(int);
|
||||
void gameOver();
|
||||
void saveGame();
|
||||
void loadGame();
|
||||
void dropObjRnd();
|
||||
void setTakeDrop(int[]);
|
||||
bool makeSel(int *, int[]);
|
||||
|
||||
void wind();
|
||||
void mist();
|
||||
void tigger();
|
||||
void startTimer();
|
||||
void stopTimer();
|
||||
|
||||
void showOwlHelp();
|
||||
void showAmigaHelp();
|
||||
bool playSound(ENUM_WTP_SOUND);
|
||||
|
||||
void printStrWinnie(char *szMsg);
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif
|
||||
1095
engines/agi/saveload.cpp
Normal file
1095
engines/agi/saveload.cpp
Normal file
File diff suppressed because it is too large
Load Diff
230
engines/agi/sound.cpp
Normal file
230
engines/agi/sound.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
|
||||
#include "agi/sound_2gs.h"
|
||||
#include "agi/sound_a2.h"
|
||||
#include "agi/sound_coco3.h"
|
||||
#include "agi/sound_midi.h"
|
||||
#include "agi/sound_sarien.h"
|
||||
#include "agi/sound_pcjr.h"
|
||||
|
||||
#include "common/textconsole.h"
|
||||
#include "audio/mixer.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
SoundGen::SoundGen(AgiBase *vm, Audio::Mixer *pMixer) : _vm(vm), _mixer(pMixer) {
|
||||
_sampleRate = pMixer->getOutputRate();
|
||||
_soundHandle = new Audio::SoundHandle();
|
||||
}
|
||||
|
||||
SoundGen::~SoundGen() {
|
||||
delete _soundHandle;
|
||||
}
|
||||
|
||||
//
|
||||
// TODO: add support for variable sampling rate in the output device
|
||||
//
|
||||
|
||||
AgiSound *AgiSound::createFromRawResource(uint8 *data, uint32 len, int resnum, int soundemu, bool isAgiV1) {
|
||||
if (data == nullptr || len < 2) // Check for too small resource or no resource at all
|
||||
return nullptr;
|
||||
|
||||
// Handle platform-specific formats that can't be detected by contents.
|
||||
// These formats have no headers or predictable first bytes.
|
||||
if (soundemu == SOUND_EMU_APPLE2) {
|
||||
return new AgiSound(resnum, data, len, AGI_SOUND_APPLE2);
|
||||
}
|
||||
if (soundemu == SOUND_EMU_COCO3) {
|
||||
return new AgiSound(resnum, data, len, AGI_SOUND_COCO3);
|
||||
}
|
||||
|
||||
// Handle AGIv1; this format has no header or predictable first bytes.
|
||||
// Must occur after platform check; Apple II always uses its format.
|
||||
if (isAgiV1) {
|
||||
return new PCjrSound(resnum, data, len, AGI_SOUND_4CHN);
|
||||
}
|
||||
|
||||
uint16 type = READ_LE_UINT16(data);
|
||||
switch (type) { // Create a sound object based on the type
|
||||
case AGI_SOUND_SAMPLE:
|
||||
return new IIgsSample(resnum, data, len, type);
|
||||
case AGI_SOUND_MIDI:
|
||||
return new IIgsMidi(resnum, data, len, type);
|
||||
case AGI_SOUND_4CHN:
|
||||
if (soundemu == SOUND_EMU_MIDI) {
|
||||
return new AgiSound(resnum, data, len, type);
|
||||
} else {
|
||||
return new PCjrSound(resnum, data, len, type);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
warning("Sound resource (%d) has unknown type (0x%04x). Not using the sound", resnum, type);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PCjrSound::PCjrSound(byte resourceNr, byte *data, uint32 length, uint16 type) :
|
||||
AgiSound(resourceNr, data, length, type) {
|
||||
|
||||
bool isValid = (_type == AGI_SOUND_4CHN) && (_data != nullptr) && (_length >= 2);
|
||||
if (!isValid) // Check for errors
|
||||
warning("Error creating PCjr 4-channel sound from resource %d (Type %d, length %d)", _resourceNr, _type, _length);
|
||||
}
|
||||
|
||||
const uint8 *PCjrSound::getVoicePointer(uint voiceNum) {
|
||||
assert(voiceNum < 4);
|
||||
uint16 voiceStartOffset = READ_LE_UINT16(_data + voiceNum * 2);
|
||||
|
||||
return _data + voiceStartOffset;
|
||||
}
|
||||
|
||||
void SoundMgr::unloadSound(int resnum) {
|
||||
if (_vm->_game.dirSound[resnum].flags & RES_LOADED) {
|
||||
if (_vm->_game.sounds[resnum]->isPlaying()) {
|
||||
_vm->_game.sounds[resnum]->stop();
|
||||
}
|
||||
|
||||
// Release the sound resource's data
|
||||
delete _vm->_game.sounds[resnum];
|
||||
_vm->_game.sounds[resnum] = nullptr;
|
||||
_vm->_game.dirSound[resnum].flags &= ~RES_LOADED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start playing a sound resource. The logic here is that when the sound is
|
||||
* finished we set the given flag to be true. This way the condition can be
|
||||
* detected by the game. On the other hand, if the game wishes to start
|
||||
* playing a new sound before the current one is finished, we also let it
|
||||
* do that.
|
||||
* @param resnum the sound resource number
|
||||
* @param flag the flag that is wished to be set true when finished
|
||||
*/
|
||||
void SoundMgr::startSound(int resnum, int flag) {
|
||||
AgiSound *sound = _vm->_game.sounds[resnum];
|
||||
debugC(3, kDebugLevelSound, "startSound(resnum = %d, flag = %d, type = %d)", resnum, flag, sound ? sound->type() : 0);
|
||||
if (sound == nullptr) {
|
||||
warning("startSound: sound %d does not exist", resnum);
|
||||
return;
|
||||
}
|
||||
|
||||
stopSound();
|
||||
|
||||
// This check handles an Apple IIgs sample with an invalid header
|
||||
if (!sound->isValid()) {
|
||||
warning("startSound: sound %d is invalid", resnum);
|
||||
return;
|
||||
}
|
||||
|
||||
sound->play();
|
||||
_playingSound = resnum;
|
||||
_soundGen->play(resnum);
|
||||
|
||||
// Reset the flag
|
||||
_endflag = flag;
|
||||
|
||||
_vm->setFlagOrVar(_endflag, false);
|
||||
}
|
||||
|
||||
void SoundMgr::stopSound() {
|
||||
debugC(3, kDebugLevelSound, "stopSound() --> %d", _playingSound);
|
||||
|
||||
if (_playingSound != -1) {
|
||||
if (_vm->_game.sounds[_playingSound]) // sanity checking
|
||||
_vm->_game.sounds[_playingSound]->stop();
|
||||
_soundGen->stop();
|
||||
_playingSound = -1;
|
||||
}
|
||||
|
||||
// This is needed all the time, some games wait until music got played and when a sound/music got stopped early
|
||||
// it would otherwise block the game (for example Death Angel jingle in back door poker room in Police Quest 1, room 71)
|
||||
if (_endflag != -1) {
|
||||
_vm->setFlagOrVar(_endflag, true);
|
||||
}
|
||||
|
||||
_endflag = -1;
|
||||
}
|
||||
|
||||
// FIXME: This is called from SoundGen classes on unsynchronized background threads.
|
||||
void SoundMgr::soundIsFinished() {
|
||||
if (_endflag != -1)
|
||||
_vm->setFlagOrVar(_endflag, true);
|
||||
|
||||
if (_playingSound != -1)
|
||||
_vm->_game.sounds[_playingSound]->stop();
|
||||
_playingSound = -1;
|
||||
_endflag = -1;
|
||||
}
|
||||
|
||||
SoundMgr::SoundMgr(AgiBase *agi, Audio::Mixer *pMixer) {
|
||||
_vm = agi;
|
||||
_endflag = -1;
|
||||
_playingSound = -1;
|
||||
|
||||
// FIXME: AGIv1 sounds are only supported by SoundGenPCJr, so we must not
|
||||
// use SoundGenSarien for those games or it will crash because it expects
|
||||
// the later AGI sound format. This means we cannot currently play these
|
||||
// sounds in PC Speaker mode (SOUND_EMU_PC) or accept SOUND_EMU_NONE,
|
||||
// because SoundGenSarien supports these but SoundGenPCJr does not.
|
||||
if (agi->getVersion() <= 0x2001 && agi->getPlatform() == Common::kPlatformDOS) {
|
||||
if (_vm->_soundemu != SOUND_EMU_PCJR) {
|
||||
warning("Unsupported sound emulation %d for AGIv1 sounds, using PCjr", _vm->_soundemu);
|
||||
_vm->_soundemu = SOUND_EMU_PCJR;
|
||||
}
|
||||
}
|
||||
|
||||
switch (_vm->_soundemu) {
|
||||
default:
|
||||
case SOUND_EMU_NONE:
|
||||
case SOUND_EMU_AMIGA:
|
||||
case SOUND_EMU_MAC:
|
||||
case SOUND_EMU_PC:
|
||||
_soundGen = new SoundGenSarien(_vm, pMixer);
|
||||
break;
|
||||
case SOUND_EMU_PCJR:
|
||||
_soundGen = new SoundGenPCJr(_vm, pMixer);
|
||||
break;
|
||||
case SOUND_EMU_APPLE2:
|
||||
_soundGen = new SoundGenA2(_vm, pMixer);
|
||||
break;
|
||||
case SOUND_EMU_APPLE2GS:
|
||||
_soundGen = new SoundGen2GS(_vm, pMixer);
|
||||
break;
|
||||
case SOUND_EMU_COCO3:
|
||||
_soundGen = new SoundGenCoCo3(_vm, pMixer);
|
||||
break;
|
||||
case SOUND_EMU_MIDI:
|
||||
_soundGen = new SoundGenMIDI(_vm, pMixer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SoundMgr::~SoundMgr() {
|
||||
stopSound();
|
||||
|
||||
delete _soundGen;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
139
engines/agi/sound.h
Normal file
139
engines/agi/sound.h
Normal file
@@ -0,0 +1,139 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SOUND_H
|
||||
#define AGI_SOUND_H
|
||||
|
||||
namespace Audio {
|
||||
class Mixer;
|
||||
class SoundHandle;
|
||||
}
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define SOUND_EMU_NONE 0
|
||||
#define SOUND_EMU_PC 1
|
||||
#define SOUND_EMU_PCJR 2
|
||||
#define SOUND_EMU_MAC 3
|
||||
#define SOUND_EMU_AMIGA 4
|
||||
#define SOUND_EMU_APPLE2 5
|
||||
#define SOUND_EMU_APPLE2GS 6
|
||||
#define SOUND_EMU_COCO3 7
|
||||
#define SOUND_EMU_MIDI 8
|
||||
|
||||
/**
|
||||
* AGI sound resource types.
|
||||
* These values are the first 16-bit LE words of each resource's header,
|
||||
* except for AGIv1, Apple II, and CoCo3, which do not have headers.
|
||||
*/
|
||||
enum AgiSoundEmuType {
|
||||
AGI_SOUND_SAMPLE = 0x0001,
|
||||
AGI_SOUND_MIDI = 0x0002,
|
||||
AGI_SOUND_4CHN = 0x0008,
|
||||
AGI_SOUND_APPLE2 = 0xffff,
|
||||
AGI_SOUND_COCO3 = 0xfffe
|
||||
};
|
||||
|
||||
class SoundMgr;
|
||||
|
||||
class SoundGen {
|
||||
public:
|
||||
SoundGen(AgiBase *vm, Audio::Mixer *pMixer);
|
||||
virtual ~SoundGen();
|
||||
|
||||
virtual void play(int resnum) = 0;
|
||||
virtual void stop() = 0;
|
||||
|
||||
AgiBase *_vm;
|
||||
|
||||
Audio::Mixer *_mixer;
|
||||
Audio::SoundHandle *_soundHandle;
|
||||
|
||||
uint32 _sampleRate;
|
||||
};
|
||||
|
||||
/**
|
||||
* AGI sound resource structure.
|
||||
*/
|
||||
class AgiSound {
|
||||
public:
|
||||
AgiSound(byte resourceNr, byte *data, uint32 length, uint16 type) :
|
||||
_resourceNr(resourceNr),
|
||||
_data(data),
|
||||
_length(length),
|
||||
_type(type),
|
||||
_isPlaying(false) {}
|
||||
|
||||
virtual ~AgiSound() { free(_data); }
|
||||
|
||||
virtual void play() { _isPlaying = true; }
|
||||
virtual void stop() { _isPlaying = false; }
|
||||
virtual bool isPlaying() { return _isPlaying; }
|
||||
byte *getData() { return _data; }
|
||||
uint32 getLength() { return _length; }
|
||||
virtual uint16 type() { return _type; }
|
||||
virtual bool isValid() { return true; }
|
||||
|
||||
/**
|
||||
* A named constructor for creating different types of AgiSound objects
|
||||
* from a raw sound resource.
|
||||
*/
|
||||
static AgiSound *createFromRawResource(uint8 *data, uint32 len, int resnum, int soundemu, bool isAgiV1);
|
||||
|
||||
protected:
|
||||
byte _resourceNr;
|
||||
byte *_data;
|
||||
uint32 _length;
|
||||
uint16 _type;
|
||||
bool _isPlaying;
|
||||
};
|
||||
|
||||
class PCjrSound : public AgiSound {
|
||||
public:
|
||||
PCjrSound(byte resourceNr, byte *data, uint32 length, uint16 type);
|
||||
const uint8 *getVoicePointer(uint voiceNum);
|
||||
};
|
||||
|
||||
class SoundMgr {
|
||||
|
||||
public:
|
||||
SoundMgr(AgiBase *agi, Audio::Mixer *pMixer);
|
||||
~SoundMgr();
|
||||
|
||||
void unloadSound(int);
|
||||
void startSound(int, int);
|
||||
void stopSound();
|
||||
|
||||
void soundIsFinished();
|
||||
bool isPlaying() const { return _playingSound != -1; }
|
||||
|
||||
private:
|
||||
int _endflag;
|
||||
AgiBase *_vm;
|
||||
|
||||
SoundGen *_soundGen;
|
||||
|
||||
int _playingSound;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SOUND_H */
|
||||
836
engines/agi/sound_2gs.cpp
Normal file
836
engines/agi/sound_2gs.cpp
Normal file
@@ -0,0 +1,836 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/config-manager.h"
|
||||
#include "common/fs.h"
|
||||
#include "common/archive.h"
|
||||
#include "common/md5.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/str-array.h"
|
||||
#include "common/textconsole.h"
|
||||
#include "audio/mixer.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/sound_2gs.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
SoundGen2GS::SoundGen2GS(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer) {
|
||||
// Allocate memory for the wavetable
|
||||
_wavetable = new int8[SIERRASTANDARD_SIZE];
|
||||
|
||||
// Apple IIGS AGI MIDI player advances 60 ticks per second. Strategy
|
||||
// here is to first generate audio for a 1/60th of a second and then
|
||||
// advance the MIDI player by one tick. Thus, make the output buffer
|
||||
// to be a 1/60th of a second in length.
|
||||
_outSize = _sampleRate / 60;
|
||||
_out = new int16[2 * _outSize]; // stereo
|
||||
|
||||
// Initialize player variables
|
||||
_nextGen = 0;
|
||||
_ticks = 0;
|
||||
|
||||
// Not playing anything yet
|
||||
_playingSound = -1;
|
||||
_playing = false;
|
||||
|
||||
// Load instruments
|
||||
_disableMidi = !loadInstruments();
|
||||
|
||||
_mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
|
||||
}
|
||||
|
||||
SoundGen2GS::~SoundGen2GS() {
|
||||
_mixer->stopHandle(*_soundHandle);
|
||||
delete[] _wavetable;
|
||||
delete[] _out;
|
||||
}
|
||||
|
||||
int SoundGen2GS::readBuffer(int16 *buffer, const int numSamples) {
|
||||
static uint data_available = 0;
|
||||
static uint data_offset = 0;
|
||||
uint n = numSamples << 1;
|
||||
uint8 *p = (uint8 *)buffer;
|
||||
|
||||
while (n > data_available) {
|
||||
memcpy(p, (uint8 *)_out + data_offset, data_available);
|
||||
p += data_available;
|
||||
n -= data_available;
|
||||
|
||||
advancePlayer();
|
||||
|
||||
data_available = generateOutput() << 1;
|
||||
data_offset = 0;
|
||||
}
|
||||
|
||||
memcpy(p, (uint8 *)_out + data_offset, n);
|
||||
data_offset += n;
|
||||
data_available -= n;
|
||||
|
||||
return numSamples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the playing of a sound resource.
|
||||
* @param resnum Resource number
|
||||
*/
|
||||
void SoundGen2GS::play(int resnum) {
|
||||
AgiSoundEmuType type;
|
||||
|
||||
type = (AgiSoundEmuType)_vm->_game.sounds[resnum]->type();
|
||||
assert(type == AGI_SOUND_SAMPLE || type == AGI_SOUND_MIDI);
|
||||
|
||||
if (_vm->_soundemu != SOUND_EMU_APPLE2GS) {
|
||||
warning("Trying to play sample or MIDI resource but not using Apple IIGS sound emulation mode");
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: all sorts of things in here are not thread-safe
|
||||
haltGenerators();
|
||||
|
||||
switch (type) {
|
||||
case AGI_SOUND_SAMPLE: {
|
||||
IIgsSample *sampleRes = (IIgsSample *) _vm->_game.sounds[resnum];
|
||||
const IIgsSampleHeader &header = sampleRes->getHeader();
|
||||
_channels[kSfxMidiChannel].setInstrument(&header.instrument);
|
||||
_channels[kSfxMidiChannel].setVolume(header.volume);
|
||||
midiNoteOn(kSfxMidiChannel, header.pitch, 127);
|
||||
break;
|
||||
}
|
||||
case AGI_SOUND_MIDI:
|
||||
((IIgsMidi *) _vm->_game.sounds[resnum])->rewind();
|
||||
_ticks = 0;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
_playingSound = resnum;
|
||||
}
|
||||
|
||||
void SoundGen2GS::stop() {
|
||||
haltGenerators();
|
||||
_playingSound = -1;
|
||||
_playing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill output buffer by advancing the generators for a 1/60th of a second.
|
||||
* @return Number of generated samples
|
||||
*/
|
||||
uint SoundGen2GS::generateOutput() {
|
||||
memset(_out, 0, _outSize * 2 * 2);
|
||||
|
||||
if (!_playing || _playingSound == -1)
|
||||
return _outSize * 2;
|
||||
|
||||
int16 *p = _out;
|
||||
int n = _outSize;
|
||||
while (n--) {
|
||||
int outLeft = 0;
|
||||
int outRight = 0;
|
||||
for (int k = 0; k < MAX_GENERATORS; k++) {
|
||||
IIgsGenerator *g = &_generators[k];
|
||||
if (!g->curInstrument)
|
||||
continue;
|
||||
const IIgsInstrumentHeader *curInstrument = g->curInstrument;
|
||||
|
||||
// Advance envelope
|
||||
int vol = fracToInt(g->a);
|
||||
if (g->a <= curInstrument->env[g->seg].bp) {
|
||||
g->a += curInstrument->env[g->seg].inc * ENVELOPE_COEF;
|
||||
if (g->a > curInstrument->env[g->seg].bp) {
|
||||
g->a = curInstrument->env[g->seg].bp;
|
||||
g->seg++;
|
||||
}
|
||||
} else {
|
||||
g->a -= curInstrument->env[g->seg].inc * ENVELOPE_COEF;
|
||||
if (g->a < curInstrument->env[g->seg].bp) {
|
||||
g->a = curInstrument->env[g->seg].bp;
|
||||
g->seg++;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Advance vibrato here. The Apple IIGS uses a LFO with
|
||||
// triangle wave to modulate the frequency of both oscillators.
|
||||
// In Apple IIGS the vibrato and the envelope are updated at the
|
||||
// same time, so the vibrato speed depends on ENVELOPE_COEF.
|
||||
|
||||
// Advance oscillators
|
||||
int s0 = 0;
|
||||
int s1 = 0;
|
||||
if (!g->osc[0].halt) {
|
||||
s0 = g->osc[0].base[fracToInt(g->osc[0].p)];
|
||||
g->osc[0].p += g->osc[0].pd;
|
||||
if ((uint)fracToInt(g->osc[0].p) >= g->osc[0].size) {
|
||||
g->osc[0].p -= intToFrac(g->osc[0].size);
|
||||
if (!g->osc[0].loop)
|
||||
g->osc[0].halt = true;
|
||||
if (g->osc[0].swap) {
|
||||
g->osc[0].halt = true;
|
||||
g->osc[1].halt = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!g->osc[1].halt) {
|
||||
s1 = g->osc[1].base[fracToInt(g->osc[1].p)];
|
||||
g->osc[1].p += g->osc[1].pd;
|
||||
if ((uint)fracToInt(g->osc[1].p) >= g->osc[1].size) {
|
||||
g->osc[1].p -= intToFrac(g->osc[1].size);
|
||||
if (!g->osc[1].loop)
|
||||
g->osc[1].halt = true;
|
||||
if (g->osc[1].swap) {
|
||||
g->osc[0].halt = false;
|
||||
g->osc[1].halt = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Take envelope and MIDI volume information into account.
|
||||
// Also amplify.
|
||||
s0 *= vol * g->velocity / 127 * 80 / 256;
|
||||
s1 *= vol * g->velocity / 127 * 80 / 256;
|
||||
|
||||
// Select output channel.
|
||||
if (g->osc[0].rightChannel)
|
||||
outRight += s0;
|
||||
else
|
||||
outLeft += s0;
|
||||
|
||||
if (g->osc[1].rightChannel)
|
||||
outRight += s1;
|
||||
else
|
||||
outLeft += s1;
|
||||
}
|
||||
|
||||
if (outLeft > 32768)
|
||||
outLeft = 32768;
|
||||
if (outLeft < -32767)
|
||||
outLeft = -32767;
|
||||
if (outRight > 32768)
|
||||
outRight = 32768;
|
||||
if (outRight < -32767)
|
||||
outRight = -32767;
|
||||
|
||||
*p++ = outLeft;
|
||||
*p++ = outRight;
|
||||
}
|
||||
|
||||
return _outSize * 2;
|
||||
}
|
||||
|
||||
void SoundGen2GS::advancePlayer() {
|
||||
if (_playingSound == -1)
|
||||
return;
|
||||
|
||||
if (_vm->_game.sounds[_playingSound]->type() == AGI_SOUND_MIDI) {
|
||||
advanceMidiPlayer();
|
||||
} else if (_vm->_game.sounds[_playingSound]->type() == AGI_SOUND_SAMPLE) {
|
||||
_playing = activeGenerators() > 0;
|
||||
}
|
||||
|
||||
if (!_playing) {
|
||||
_vm->_sound->soundIsFinished();
|
||||
_playingSound = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void SoundGen2GS::advanceMidiPlayer() {
|
||||
if (_disableMidi)
|
||||
return;
|
||||
|
||||
const uint8 *p;
|
||||
uint8 parm1, parm2;
|
||||
static uint8 cmd, chn;
|
||||
|
||||
if (_playingSound == -1 || _vm->_game.sounds[_playingSound] == nullptr) {
|
||||
warning("Error playing Apple IIGS MIDI sound resource");
|
||||
_playing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
IIgsMidi *midiObj = (IIgsMidi *) _vm->_game.sounds[_playingSound];
|
||||
|
||||
_ticks++;
|
||||
_playing = true;
|
||||
p = midiObj->getPtr();
|
||||
|
||||
while (true) {
|
||||
// Check for end of MIDI sequence marker (Can also be here before delta-time)
|
||||
if (*p == MIDI_STOP_SEQUENCE) {
|
||||
debugC(3, kDebugLevelSound, "End of MIDI sequence (Before reading delta-time)");
|
||||
_playing = false;
|
||||
midiObj->rewind();
|
||||
return;
|
||||
}
|
||||
if (*p == MIDI_TIMER_SYNC) {
|
||||
debugC(3, kDebugLevelSound, "Timer sync");
|
||||
p++; // Jump over the timer sync byte as it's not needed
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for delta time
|
||||
uint8 delta = *p;
|
||||
if (midiObj->_ticks + delta > _ticks)
|
||||
break;
|
||||
midiObj->_ticks += delta;
|
||||
p++;
|
||||
|
||||
// Check for end of MIDI sequence marker (This time it after reading delta-time)
|
||||
if (*p == MIDI_STOP_SEQUENCE) {
|
||||
debugC(3, kDebugLevelSound, "End of MIDI sequence (After reading delta-time)");
|
||||
_playing = false;
|
||||
midiObj->rewind();
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate byte into command and channel if it's a command byte.
|
||||
// Otherwise use running status (i.e. previously set command and channel).
|
||||
if (*p & 0x80) {
|
||||
cmd = *p++;
|
||||
chn = cmd & 0x0f;
|
||||
cmd >>= 4;
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case MIDI_NOTE_OFF:
|
||||
parm1 = *p++;
|
||||
parm2 = *p++;
|
||||
debugC(3, kDebugLevelSound, "channel %X: note off (key = %d, velocity = %d)", chn, parm1, parm2);
|
||||
midiNoteOff(chn, parm1, parm2);
|
||||
break;
|
||||
case MIDI_NOTE_ON:
|
||||
parm1 = *p++;
|
||||
parm2 = *p++;
|
||||
debugC(3, kDebugLevelSound, "channel %X: note on (key = %d, velocity = %d)", chn, parm1, parm2);
|
||||
midiNoteOn(chn, parm1, parm2);
|
||||
break;
|
||||
case MIDI_CONTROLLER:
|
||||
parm1 = *p++;
|
||||
parm2 = *p++;
|
||||
debugC(3, kDebugLevelSound, "channel %X: controller %02X = %02X", chn, parm1, parm2);
|
||||
// The tested Apple IIGS AGI MIDI resources only used
|
||||
// controllers 0 (Bank select?), 7 (Volume) and 64 (Sustain On/Off).
|
||||
// Controller 0's parameter was in range 94-127,
|
||||
// controller 7's parameter was in range 0-127 and
|
||||
// controller 64's parameter was always 0 (i.e. sustain off).
|
||||
switch (parm1) {
|
||||
case 7:
|
||||
_channels[chn].setVolume(parm2);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case MIDI_PROGRAM_CHANGE:
|
||||
parm1 = *p++;
|
||||
debugC(3, kDebugLevelSound, "channel %X: program change %02X", chn, parm1);
|
||||
_channels[chn].setInstrument(getInstrument(parm1));
|
||||
break;
|
||||
case MIDI_PITCH_WHEEL:
|
||||
parm1 = *p++;
|
||||
parm2 = *p++;
|
||||
debugC(3, kDebugLevelSound, "channel %X: pitch wheel (unimplemented) %02X, %02X", chn, parm1, parm2);
|
||||
break;
|
||||
|
||||
default:
|
||||
debugC(3, kDebugLevelSound, "channel %X: unimplemented command %02X", chn, cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
midiObj->setPtr(p);
|
||||
}
|
||||
|
||||
void SoundGen2GS::midiNoteOff(int channel, int note, int velocity) {
|
||||
// Release keys within the given MIDI channel
|
||||
for (int i = 0; i < MAX_GENERATORS; i++) {
|
||||
if (_generators[i].channel == channel && _generators[i].key == note) {
|
||||
if (_generators[i].curInstrument) {
|
||||
_generators[i].seg = _generators[i].curInstrument->seg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SoundGen2GS::midiNoteOn(int channel, int note, int velocity) {
|
||||
if (!_channels[channel].getInstrument()) {
|
||||
debugC(3, kDebugLevelSound, "midiNoteOn(): no instrument specified for channel %d", channel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate a generator for the note.
|
||||
IIgsGenerator *generator = allocateGenerator();
|
||||
generator->curInstrument = _channels[channel].getInstrument();
|
||||
const IIgsInstrumentHeader *curInstrument = generator->curInstrument;
|
||||
|
||||
// Pass information from the MIDI channel to the generator. Take
|
||||
// velocity into account, although simplistically.
|
||||
velocity *= 5 / 3;
|
||||
if (velocity > 127)
|
||||
velocity = 127;
|
||||
|
||||
generator->key = note;
|
||||
generator->velocity = velocity * _channels[channel].getVolume() / 127;
|
||||
generator->channel = channel;
|
||||
|
||||
// Instruments can define different samples to be used based on
|
||||
// what the key is. Find the correct samples for our key.
|
||||
int wa = 0;
|
||||
int wb = 0;
|
||||
while (wa < curInstrument->waveCount[0] - 1 && note > curInstrument->wave[0][wa].key)
|
||||
wa++;
|
||||
while (wb < curInstrument->waveCount[1] - 1 && note > curInstrument->wave[1][wb].key)
|
||||
wb++;
|
||||
|
||||
// Prepare the generator.
|
||||
generator->osc[0].base = curInstrument->wavetableBase + curInstrument->wave[0][wa].offset;
|
||||
generator->osc[0].size = curInstrument->wave[0][wa].size;
|
||||
generator->osc[0].pd = doubleToFrac(midiKeyToFreq(note, (double)curInstrument->wave[0][wa].tune / 256.0) / (double)_sampleRate);
|
||||
generator->osc[0].p = 0;
|
||||
generator->osc[0].halt = curInstrument->wave[0][wa].halt;
|
||||
generator->osc[0].loop = curInstrument->wave[0][wa].loop;
|
||||
generator->osc[0].swap = curInstrument->wave[0][wa].swap;
|
||||
generator->osc[0].rightChannel = curInstrument->wave[0][wa].rightChannel;
|
||||
|
||||
generator->osc[1].base = curInstrument->wavetableBase + curInstrument->wave[1][wb].offset;
|
||||
generator->osc[1].size = curInstrument->wave[1][wb].size;
|
||||
generator->osc[1].pd = doubleToFrac(midiKeyToFreq(note, (double)curInstrument->wave[1][wb].tune / 256.0) / (double)_sampleRate);
|
||||
generator->osc[1].p = 0;
|
||||
generator->osc[1].halt = curInstrument->wave[1][wb].halt;
|
||||
generator->osc[1].loop = curInstrument->wave[1][wb].loop;
|
||||
generator->osc[1].swap = curInstrument->wave[1][wb].swap;
|
||||
generator->osc[1].rightChannel = curInstrument->wave[1][wb].rightChannel;
|
||||
|
||||
generator->seg = 0;
|
||||
generator->a = 0;
|
||||
|
||||
// Print debug messages for instruments with swap mode or vibrato enabled
|
||||
if (generator->osc[0].swap || generator->osc[1].swap)
|
||||
debugC(2, kDebugLevelSound, "Detected swap mode in a playing instrument. This is rare and is not tested well...");
|
||||
if (curInstrument->vibDepth > 0)
|
||||
debugC(2, kDebugLevelSound, "Detected vibrato in a playing instrument. Vibrato is not implemented, playing without...");
|
||||
}
|
||||
|
||||
double SoundGen2GS::midiKeyToFreq(int key, double finetune) {
|
||||
return 440.0 * pow(2.0, (15.0 + (double)key + finetune) / 12.0);
|
||||
}
|
||||
|
||||
void SoundGen2GS::haltGenerators() {
|
||||
for (int i = 0; i < MAX_GENERATORS; i++) {
|
||||
// Reset instrument pointer especially for samples, because samples are deleted on unload/room changes
|
||||
// and not resetting them here would cause those invalidated samples get accessed during generateOutput()
|
||||
_generators[i].curInstrument = nullptr;
|
||||
_generators[i].osc[0].halt = true;
|
||||
_generators[i].osc[1].halt = true;
|
||||
}
|
||||
}
|
||||
|
||||
uint SoundGen2GS::activeGenerators() {
|
||||
int n = 0;
|
||||
for (int i = 0; i < MAX_GENERATORS; i++)
|
||||
if (!_generators[i].osc[0].halt || !_generators[i].osc[1].halt)
|
||||
n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
void SoundGen2GS::setProgramChangeMapping(const IIgsMidiProgramMapping *mapping) {
|
||||
_progToInst = mapping;
|
||||
}
|
||||
|
||||
IIgsMidi::IIgsMidi(byte resourceNr, byte *data, uint32 length, uint16 type) :
|
||||
AgiSound(resourceNr, data, length, type) {
|
||||
|
||||
_ptr = _data + 2; // Set current position to just after the header
|
||||
_ticks = 0;
|
||||
bool isValid = (_type == AGI_SOUND_MIDI) && (_data != nullptr) && (_length >= 2);
|
||||
|
||||
if (!isValid) // Check for errors
|
||||
warning("Error creating Apple IIGS midi sound from resource %d (Type %d, length %d)", _resourceNr, _type, _length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert sample from 8-bit unsigned to 8-bit signed format.
|
||||
* @param source Source stream containing the 8-bit unsigned sample data.
|
||||
* @param dest Destination buffer for the 8-bit signed sample data.
|
||||
* @param length Length of the sample data to be converted.
|
||||
*/
|
||||
static bool convertWave(Common::SeekableReadStream &source, int8 *dest, uint length) {
|
||||
// Convert the wave from 8-bit unsigned to 8-bit signed format
|
||||
for (uint i = 0; i < length; i++)
|
||||
dest[i] = (int8)((int)source.readByte() - ZERO_OFFSET);
|
||||
return !(source.eos() || source.err());
|
||||
}
|
||||
|
||||
IIgsSample::IIgsSample(byte resourceNr, byte *data, uint32 length, uint16 type) :
|
||||
_isValid(false), AgiSound(resourceNr, data, length, type) {
|
||||
|
||||
Common::MemoryReadStream stream(_data, _length, DisposeAfterUse::NO);
|
||||
|
||||
_sample = nullptr;
|
||||
|
||||
// Check that the header was read ok and that it's of the correct type
|
||||
if (_header.read(stream) && _header.type == AGI_SOUND_SAMPLE) { // An Apple IIGS AGI sample resource
|
||||
uint32 sampleStartPos = stream.pos();
|
||||
uint32 tailLen = stream.size() - sampleStartPos;
|
||||
|
||||
if (tailLen < _header.sampleSize) { // Check if there's no room for the sample data in the stream
|
||||
// Apple IIGS Manhunter I: Sound resource 16 has only 16074 bytes
|
||||
// of sample data although header says it should have 16384 bytes.
|
||||
warning("Apple IIGS sample (%d) expected %d bytes, got %d bytes only",
|
||||
resourceNr, _header.sampleSize, tailLen);
|
||||
|
||||
_header.sampleSize = (uint16) tailLen; // Use the part that's left
|
||||
}
|
||||
|
||||
if (_header.pitch > 0x7F) { // Check if the pitch is invalid
|
||||
warning("Apple IIGS sample (%d) has too high pitch (0x%02x)", resourceNr, _header.pitch);
|
||||
|
||||
_header.pitch &= 0x7F; // Apple IIGS AGI probably did it this way too
|
||||
}
|
||||
|
||||
// Convert sample data from 8-bit unsigned to 8-bit signed format
|
||||
stream.seek(sampleStartPos);
|
||||
_sample = new int8[_header.sampleSize];
|
||||
|
||||
if (_sample != nullptr) {
|
||||
_isValid = convertWave(stream, _sample, _header.sampleSize);
|
||||
|
||||
if (_isValid) {
|
||||
// Finalize header info using sample data
|
||||
_header.finalize(_sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isValid) // Check for errors
|
||||
warning("Error creating Apple IIGS sample from resource %d (Type %d, length %d)", resourceNr, _header.type, _length);
|
||||
}
|
||||
|
||||
|
||||
bool IIgsInstrumentHeader::read(Common::SeekableReadStream &stream, bool ignoreAddr) {
|
||||
for (int i = 0; i < ENVELOPE_SEGMENT_COUNT; i++) {
|
||||
env[i].bp = intToFrac(stream.readByte());
|
||||
env[i].inc = intToFrac(stream.readUint16LE()) >> 8;
|
||||
}
|
||||
seg = stream.readByte();
|
||||
/*priority =*/ stream.readByte(); // Not needed. 32 in all tested data.
|
||||
bend = stream.readByte();
|
||||
vibDepth = stream.readByte();
|
||||
vibSpeed = stream.readByte();
|
||||
stream.readByte(); // Not needed? 0 in all tested data.
|
||||
|
||||
waveCount[0] = stream.readByte();
|
||||
waveCount[1] = stream.readByte();
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int k = 0; k < waveCount[i]; k++) {
|
||||
wave[i][k].key = stream.readByte();
|
||||
wave[i][k].offset = stream.readByte() << 8;
|
||||
wave[i][k].size = 0x100 << (stream.readByte() & 7);
|
||||
uint8 b = stream.readByte();
|
||||
wave[i][k].tune = stream.readUint16LE();
|
||||
|
||||
// For sample resources we ignore the address.
|
||||
if (ignoreAddr)
|
||||
wave[i][k].offset = 0;
|
||||
|
||||
// Parse the generator mode byte to separate fields.
|
||||
wave[i][k].halt = b & 0x1; // Bit 0 = HALT
|
||||
wave[i][k].loop = !(b & 0x2); // Bit 1 =!LOOP
|
||||
wave[i][k].swap = (b & 0x6) == 0x6; // Bit 1&2 = SWAP
|
||||
// channels seem to be reversed, verified with emulator + captured apple IIgs music
|
||||
if (b & 0x10) {
|
||||
wave[i][k].rightChannel = true; // Bit 4 set = right channel
|
||||
} else {
|
||||
wave[i][k].rightChannel = false; // Bit 4 not set = left channel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !(stream.eos() || stream.err());
|
||||
}
|
||||
|
||||
bool IIgsInstrumentHeader::finalize(int8 *wavetable, uint32 wavetableSize) {
|
||||
wavetableBase = wavetable;
|
||||
|
||||
// Go through all offsets and sizes and make sure, they point to within wavetable
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int k = 0; k < waveCount[i]; k++) {
|
||||
uint32 waveOffset = wave[i][k].offset;
|
||||
uint32 waveSize = wave[i][k].size;
|
||||
|
||||
if (waveOffset >= wavetableSize) {
|
||||
error("Apple IIgs sound: sample data points outside of wavetable");
|
||||
}
|
||||
|
||||
if ((waveOffset + waveSize) > wavetableSize) {
|
||||
// fix up size, it's actually saved in a way in the header, that it can't be correct
|
||||
// if we don't fix it here, we would do invalid memory access, which results in potential crashes
|
||||
wave[i][k].size = wavetableSize - waveOffset;
|
||||
}
|
||||
|
||||
// Detect true sample size
|
||||
int8 *sample = wavetableBase + wave[i][k].offset;
|
||||
uint32 trueSize;
|
||||
for (trueSize = 0; trueSize < wave[i][k].size; trueSize++) {
|
||||
if (sample[trueSize] == -ZERO_OFFSET)
|
||||
break;
|
||||
}
|
||||
wave[i][k].size = trueSize;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IIgsSampleHeader::read(Common::SeekableReadStream &stream) {
|
||||
type = stream.readUint16LE();
|
||||
pitch = stream.readByte();
|
||||
unknownByte_Ofs3 = stream.readByte();
|
||||
volume = stream.readByte();
|
||||
unknownByte_Ofs5 = stream.readByte();
|
||||
instrumentSize = stream.readUint16LE();
|
||||
sampleSize = stream.readUint16LE();
|
||||
// Read the instrument header *ignoring* its wave address info
|
||||
return instrument.read(stream, true);
|
||||
}
|
||||
|
||||
bool IIgsSampleHeader::finalize(int8 *sampleData) {
|
||||
return instrument.finalize(sampleData, sampleSize);
|
||||
}
|
||||
|
||||
//###
|
||||
//### LOADER METHODS
|
||||
//###
|
||||
|
||||
bool SoundGen2GS::loadInstruments() {
|
||||
// Get info on the particular Apple IIGS AGI game's executable
|
||||
const IIgsExeInfo *exeInfo = getIIgsExeInfo((enum AgiGameID)_vm->getGameID());
|
||||
if (exeInfo == nullptr) {
|
||||
warning("Unsupported Apple IIGS game, not loading instruments");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the executable file and the wavetable file
|
||||
Common::ArchiveMemberList exeNames, waveNames;
|
||||
SearchMan.listMatchingMembers(exeNames, "*.SYS16");
|
||||
SearchMan.listMatchingMembers(exeNames, "*.SYS");
|
||||
SearchMan.listMatchingMembers(waveNames, "SIERRASTANDARD");
|
||||
SearchMan.listMatchingMembers(waveNames, "SIERRAST");
|
||||
|
||||
if (exeNames.empty()) {
|
||||
warning("Couldn't find Apple IIGS game executable (*.SYS16 or *.SYS), not loading instruments");
|
||||
return false;
|
||||
}
|
||||
if (waveNames.empty()) {
|
||||
warning("Couldn't find Apple IIGS wave file (SIERRASTANDARD or SIERRAST), not loading instruments");
|
||||
return false;
|
||||
}
|
||||
|
||||
Common::Path exeName = exeNames.front()->getPathInArchive();
|
||||
Common::Path waveName = waveNames.front()->getPathInArchive();
|
||||
|
||||
// Set the MIDI program change to instrument number mapping and
|
||||
// load the instrument headers and their sample data.
|
||||
setProgramChangeMapping(exeInfo->instSet->progToInst);
|
||||
return loadWaveFile(waveName, *exeInfo) && loadInstrumentHeaders(exeName, *exeInfo);
|
||||
}
|
||||
|
||||
/** Older Apple IIGS AGI MIDI program change to instrument number mapping. */
|
||||
static const IIgsMidiProgramMapping progToInstMappingV1 = {
|
||||
{
|
||||
19, 20, 22, 23, 21, 24, 5, 5, 5, 5,
|
||||
6, 7, 10, 9, 11, 9, 15, 8, 5, 5,
|
||||
17, 16, 18, 12, 14, 5, 5, 5, 5, 5,
|
||||
0, 1, 2, 9, 3, 4, 15, 2, 2, 2,
|
||||
25, 13, 13, 25
|
||||
},
|
||||
5
|
||||
};
|
||||
|
||||
/** Newer Apple IIGS AGI MIDI program change to instrument number mapping.
|
||||
FIXME: Some instrument choices sound wrong. */
|
||||
static const IIgsMidiProgramMapping progToInstMappingV2 = {
|
||||
{
|
||||
21, 22, 24, 25, 23, 26, 6, 6, 6, 6,
|
||||
7, 9, 12, 8, 13, 11, 17, 10, 6, 6,
|
||||
19, 18, 20, 14, 16, 6, 6, 6, 6, 6,
|
||||
0, 1, 2, 4, 3, 5, 17, 2, 2, 2,
|
||||
27, 15, 15, 27
|
||||
},
|
||||
6
|
||||
};
|
||||
|
||||
// Older Apple IIGS AGI instrument set. Used only by Space Quest I (AGI v1.002).
|
||||
//
|
||||
// Instrument 0 uses vibrato.
|
||||
// Instrument 1 uses vibrato.
|
||||
// Instrument 3 uses vibrato.
|
||||
// Instrument 5 has swap mode enabled for the first oscillator.
|
||||
// Instruemnt 9 uses vibrato.
|
||||
// Instrument 10 uses vibrato.
|
||||
// Instrument 12 uses vibrato.
|
||||
// Instrument 15 uses vibrato.
|
||||
// Instrument 16 uses vibrato.
|
||||
// Instrument 18 uses vibrato.
|
||||
static const IIgsInstrumentSetInfo instSetV1 = {
|
||||
1192, 26, "7ee16bbc135171ffd6b9120cc7ff1af2", "edd3bf8905d9c238e02832b732fb2e18", &progToInstMappingV1
|
||||
};
|
||||
|
||||
// Newer Apple IIGS AGI instrument set (AGI v1.003+). Used by all others than Space Quest I.
|
||||
//
|
||||
// Instrument 0 uses vibrato.
|
||||
// Instrument 1 uses vibrato.
|
||||
// Instrument 3 uses vibrato.
|
||||
// Instrument 6 has swap mode enabled for the first oscillator.
|
||||
// Instrument 11 uses vibrato.
|
||||
// Instrument 12 uses vibrato.
|
||||
// Instrument 14 uses vibrato.
|
||||
// Instrument 17 uses vibrato.
|
||||
// Instrument 18 uses vibrato.
|
||||
// Instrument 20 uses vibrato.
|
||||
//
|
||||
// In KQ1 intro and in LSL intro one (and the same, or at least similar)
|
||||
// instrument is using vibrato. In PQ intro there is also one instrument
|
||||
// using vibrato.
|
||||
static const IIgsInstrumentSetInfo instSetV2 = {
|
||||
1292, 28, "b7d428955bb90721996de1cbca25e768", "c05fb0b0e11deefab58bc68fbd2a3d07", &progToInstMappingV2
|
||||
};
|
||||
|
||||
/** Information about different Apple IIGS AGI executables. */
|
||||
static const IIgsExeInfo IIgsExeInfos[] = {
|
||||
{GID_SQ1, "SQ", 0x1002, 138496, 0x80AD, &instSetV1},
|
||||
{GID_LSL1, "LL", 0x1003, 141003, 0x844E, &instSetV2},
|
||||
{GID_AGIDEMO, "DEMO", 0x1005, 141884, 0x8469, &instSetV2},
|
||||
{GID_KQ1, "KQ", 0x1006, 141894, 0x8469, &instSetV2},
|
||||
{GID_PQ1, "PQ", 0x1007, 141882, 0x8469, &instSetV2},
|
||||
{GID_MIXEDUP, "MG", 0x1013, 142552, 0x84B7, &instSetV2},
|
||||
{GID_KQ2, "KQ2", 0x1013, 143775, 0x84B7, &instSetV2},
|
||||
{GID_KQ3, "KQ3", 0x1014, 144312, 0x84B7, &instSetV2},
|
||||
{GID_SQ2, "SQ2", 0x1014, 107882, 0x6563, &instSetV2},
|
||||
{GID_MH1, "MH", 0x2004, 147678, 0x8979, &instSetV2},
|
||||
{GID_KQ4, "KQ4", 0x2006, 147652, 0x8979, &instSetV2},
|
||||
{GID_BC, "BC", 0x3001, 148192, 0x8979, &instSetV2},
|
||||
{GID_GOLDRUSH, "GR", 0x3003, 148268, 0x8979, &instSetV2}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds information about an Apple IIGS AGI executable based on the game ID.
|
||||
* @return A non-null IIgsExeInfo pointer if successful, otherwise NULL.
|
||||
*/
|
||||
const IIgsExeInfo *SoundGen2GS::getIIgsExeInfo(enum AgiGameID gameid) const {
|
||||
for (int i = 0; i < ARRAYSIZE(IIgsExeInfos); i++)
|
||||
if (IIgsExeInfos[i].gameid == gameid)
|
||||
return &IIgsExeInfos[i];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SoundGen2GS::loadInstrumentHeaders(const Common::Path &exePath, const IIgsExeInfo &exeInfo) {
|
||||
Common::File file;
|
||||
|
||||
// Open the executable file and check that it has correct size
|
||||
file.open(exePath);
|
||||
if (file.size() != (int32)exeInfo.exeSize) {
|
||||
debugC(3, kDebugLevelSound, "Apple IIGS executable (%s) has wrong size (Is %d, should be %d)",
|
||||
exePath.toString(Common::Path::kNativeSeparator).c_str(), (int)file.size(), exeInfo.exeSize);
|
||||
}
|
||||
|
||||
// Read the whole executable file into memory
|
||||
// CHECKME: Why do we read the file into memory first? It does not seem to be
|
||||
// kept outside of this function. Is the processing of the data too slow
|
||||
// otherwise?
|
||||
Common::ScopedPtr<Common::SeekableReadStream> data(file.readStream(file.size()));
|
||||
file.close();
|
||||
|
||||
// Check that we got enough data to be able to parse the instruments
|
||||
if (!data || data->size() < (int32)(exeInfo.instSetStart + exeInfo.instSet->byteCount)) {
|
||||
warning("Error loading instruments from Apple IIGS executable (%s)", exePath.toString(Common::Path::kNativeSeparator).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check instrument set's length (The info's saved in the executable)
|
||||
data->seek(exeInfo.instSetStart - 4);
|
||||
uint16 instSetByteCount = data->readUint16LE();
|
||||
if (instSetByteCount != exeInfo.instSet->byteCount) {
|
||||
debugC(3, kDebugLevelSound, "Wrong instrument set size (Is %d, should be %d) in Apple IIGS executable (%s)",
|
||||
instSetByteCount, exeInfo.instSet->byteCount, exePath.toString(Common::Path::kNativeSeparator).c_str());
|
||||
}
|
||||
|
||||
// Check instrument set's md5sum
|
||||
data->seek(exeInfo.instSetStart);
|
||||
Common::String md5str = Common::computeStreamMD5AsString(*data, exeInfo.instSet->byteCount);
|
||||
if (md5str != exeInfo.instSet->md5) {
|
||||
warning("Unknown Apple IIGS instrument set (md5: %s) in %s, trying to use it nonetheless",
|
||||
md5str.c_str(), exePath.toString(Common::Path::kNativeSeparator).c_str());
|
||||
}
|
||||
|
||||
// Read in the instrument set one instrument at a time
|
||||
data->seek(exeInfo.instSetStart);
|
||||
|
||||
_instruments.clear();
|
||||
_instruments.reserve(exeInfo.instSet->instCount);
|
||||
|
||||
IIgsInstrumentHeader instrument;
|
||||
for (uint i = 0; i < exeInfo.instSet->instCount; i++) {
|
||||
if (!instrument.read(*data)) {
|
||||
warning("Error loading Apple IIGS instrument (%d. of %d) from %s, not loading more instruments",
|
||||
i + 1, exeInfo.instSet->instCount, exePath.toString(Common::Path::kNativeSeparator).c_str());
|
||||
break;
|
||||
}
|
||||
instrument.finalize(_wavetable, SIERRASTANDARD_SIZE);
|
||||
_instruments.push_back(instrument);
|
||||
}
|
||||
|
||||
// Loading was successful only if all instruments were loaded successfully
|
||||
return (_instruments.size() == exeInfo.instSet->instCount);
|
||||
}
|
||||
|
||||
bool SoundGen2GS::loadWaveFile(const Common::Path &wavePath, const IIgsExeInfo &exeInfo) {
|
||||
Common::File file;
|
||||
|
||||
// Open the wave file and read it into memory
|
||||
// CHECKME: Why do we read the file into memory first? It does not seem to be
|
||||
// kept outside of this function. Is the processing of the data too slow
|
||||
// otherwise?
|
||||
file.open(wavePath);
|
||||
Common::ScopedPtr<Common::SeekableReadStream> uint8Wave(file.readStream(file.size()));
|
||||
file.close();
|
||||
|
||||
// Check that we got the whole wave file
|
||||
if (!uint8Wave || (uint8Wave->size() != SIERRASTANDARD_SIZE)) {
|
||||
warning("Error loading Apple IIGS wave file (%s), not loading instruments", wavePath.toString(Common::Path::kNativeSeparator).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check wave file's md5sum
|
||||
Common::String md5str = Common::computeStreamMD5AsString(*uint8Wave, SIERRASTANDARD_SIZE);
|
||||
if (md5str != exeInfo.instSet->waveFileMd5) {
|
||||
warning("Unknown Apple IIGS wave file (md5: %s, game: %s).\n" \
|
||||
"Please report the information on the previous line to the ScummVM team.\n" \
|
||||
"Using the wave file as it is - music may sound weird", md5str.c_str(), exeInfo.exePrefix);
|
||||
}
|
||||
|
||||
// Convert the wave file to 8-bit signed and save the result
|
||||
uint8Wave->seek(0);
|
||||
return convertWave(*uint8Wave, _wavetable, SIERRASTANDARD_SIZE);
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
272
engines/agi/sound_2gs.h
Normal file
272
engines/agi/sound_2gs.h
Normal file
@@ -0,0 +1,272 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SOUND_2GS_H
|
||||
#define AGI_SOUND_2GS_H
|
||||
|
||||
#include "common/frac.h"
|
||||
#include "audio/audiostream.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// Sample data in SIERRASTANDARD files is in unsigned 8-bit format. A zero
|
||||
// occurring in the sample data causes the ES5503 wavetable sound chip in
|
||||
// Apple IIGS to halt the corresponding oscillator immediately. We preprocess
|
||||
// the sample data by converting it to signed values and the instruments by
|
||||
// detecting prematurely stopping samples beforehand.
|
||||
//
|
||||
// Note: None of the tested SIERRASTANDARD files have zeroes in them. So in
|
||||
// practice there is no need to check for them. However, they still do exist
|
||||
// in the sample resources.
|
||||
#define ZERO_OFFSET 0x80
|
||||
|
||||
// Apple IIGS envelope update frequency defaults to 100Hz. It can be changed,
|
||||
// so there might be differences per game, for example.
|
||||
#define ENVELOPE_COEF 100 / _sampleRate
|
||||
|
||||
// MIDI player commands
|
||||
#define MIDI_NOTE_OFF 0x8
|
||||
#define MIDI_NOTE_ON 0x9
|
||||
#define MIDI_CONTROLLER 0xB
|
||||
#define MIDI_PROGRAM_CHANGE 0xC
|
||||
#define MIDI_PITCH_WHEEL 0xE
|
||||
|
||||
#define MIDI_STOP_SEQUENCE 0xFC
|
||||
#define MIDI_TIMER_SYNC 0xF8
|
||||
|
||||
// Size of the SIERRASTANDARD file (i.e. the wave file i.e. the sample data used by the instruments).
|
||||
#define SIERRASTANDARD_SIZE 65536
|
||||
|
||||
// Maximum number of instruments in an Apple IIGS instrument set.
|
||||
// Chosen empirically based on Apple IIGS AGI game data, increase if needed.
|
||||
#define MAX_INSTRUMENTS 28
|
||||
|
||||
// The MIDI player allocates one generator for each note it starts to play.
|
||||
// Here the maximum number of generators is defined. Feel free to increase
|
||||
// this if it does not seem to be enough.
|
||||
#define MAX_GENERATORS 16
|
||||
|
||||
#define ENVELOPE_SEGMENT_COUNT 8
|
||||
#define MAX_OSCILLATOR_WAVES 127 // Maximum is one for every MIDI key
|
||||
|
||||
struct IIgsInstrumentHeader {
|
||||
struct {
|
||||
frac_t bp; ///< Envelope segment breakpoint
|
||||
frac_t inc; ///< Envelope segment velocity
|
||||
} env[ENVELOPE_SEGMENT_COUNT];
|
||||
uint8 seg; ///< Envelope release segment
|
||||
uint8 bend; ///< Maximum range for pitch bend
|
||||
uint8 vibDepth; ///< Vibrato depth
|
||||
uint8 vibSpeed; ///< Vibrato speed
|
||||
uint8 waveCount[2]; ///< Wave count for both generators
|
||||
struct {
|
||||
uint8 key; ///< Highest MIDI key to use this wave
|
||||
uint32 offset; ///< Offset of wave data, relative to base
|
||||
uint32 size; ///< Wave size
|
||||
bool halt; ///< Oscillator halted?
|
||||
bool loop; ///< Loop mode?
|
||||
bool swap; ///< Swap mode?
|
||||
bool rightChannel; ///< Output channel (left / right)
|
||||
int16 tune; ///< Fine tune in semitones (8.8 fixed point)
|
||||
} wave[2][MAX_OSCILLATOR_WAVES];
|
||||
|
||||
int8 *wavetableBase; ///< Base of wave data
|
||||
|
||||
/**
|
||||
* Read an Apple IIGS instrument header from the given stream.
|
||||
* @param stream The source stream from which to read the data.
|
||||
* @param ignoreAddr Should we ignore wave infos' wave address variable's value?
|
||||
* @return True if successful, false otherwise.
|
||||
*/
|
||||
bool read(Common::SeekableReadStream &stream, bool ignoreAddr = false);
|
||||
bool finalize(int8 *wavetable, uint32 wavetableSize);
|
||||
};
|
||||
|
||||
struct IIgsSampleHeader {
|
||||
uint16 type;
|
||||
uint8 pitch; ///< Logarithmic, base is 2**(1/12), unknown multiplier (Possibly in range 1040-1080)
|
||||
uint8 unknownByte_Ofs3; // 0x7F in Gold Rush's sound resource 60, 0 in all others.
|
||||
uint8 volume; ///< Current guess: Logarithmic in 6 dB steps
|
||||
uint8 unknownByte_Ofs5; ///< 0 in all tested samples.
|
||||
uint16 instrumentSize; ///< 44 in all tested samples. A guess.
|
||||
uint16 sampleSize; ///< Accurate in all tested samples excluding Manhunter I's sound resource 16.
|
||||
IIgsInstrumentHeader instrument;
|
||||
|
||||
/**
|
||||
* Read an Apple IIGS AGI sample header from the given stream.
|
||||
* @param stream The source stream from which to read the data.
|
||||
* @return True if successful, false otherwise.
|
||||
*/
|
||||
bool read(Common::SeekableReadStream &stream);
|
||||
bool finalize(int8 *sampleData);
|
||||
};
|
||||
|
||||
class IIgsGenerator {
|
||||
public:
|
||||
IIgsGenerator() : curInstrument(nullptr), key(-1), channel(-1) {
|
||||
memset(&osc, 0, sizeof(osc));
|
||||
seg = 0;
|
||||
a = 0;
|
||||
velocity = 0;
|
||||
}
|
||||
|
||||
const IIgsInstrumentHeader *curInstrument; ///< Currently used instrument
|
||||
int key; ///< MIDI key
|
||||
int velocity; ///< MIDI velocity (& channel volume)
|
||||
int channel; ///< MIDI channel
|
||||
struct {
|
||||
int8 *base; ///< Sample base pointer
|
||||
uint size; ///< Sample size
|
||||
frac_t p; ///< Sample pointer
|
||||
frac_t pd; ///< Sample pointer delta
|
||||
bool halt; ///< Is oscillator halted?
|
||||
bool loop; ///< Is looping enabled?
|
||||
bool swap; ///< Is swapping enabled?
|
||||
bool rightChannel; ///< Output channel (left / right)
|
||||
} osc[2];
|
||||
int seg; ///< Current envelope segment
|
||||
frac_t a; ///< Current envelope amplitude
|
||||
};
|
||||
|
||||
class IIgsMidi : public AgiSound {
|
||||
public:
|
||||
IIgsMidi(byte resourceNr, byte *data, uint32 length, uint16 type);
|
||||
virtual const uint8 *getPtr() { return _ptr; }
|
||||
virtual void setPtr(const uint8 *ptr) { _ptr = ptr; }
|
||||
virtual void rewind() { _ptr = _data + 2; _ticks = 0; }
|
||||
protected:
|
||||
const uint8 *_ptr; ///< Pointer to the current position in the MIDI data
|
||||
public:
|
||||
uint _ticks; ///< MIDI song position in ticks (1/60ths of a second)
|
||||
};
|
||||
|
||||
class IIgsSample : public AgiSound {
|
||||
public:
|
||||
IIgsSample(byte resourceNr, byte *data, uint32 length, uint16 type);
|
||||
~IIgsSample() override { delete[] _sample; }
|
||||
uint16 type() override { return _header.type; }
|
||||
const IIgsSampleHeader &getHeader() const { return _header; }
|
||||
bool isValid() override { return _isValid; }
|
||||
protected:
|
||||
IIgsSampleHeader _header; ///< Apple IIGS AGI sample header
|
||||
int8 *_sample; ///< Sample data (8-bit signed format)
|
||||
bool _isValid;
|
||||
};
|
||||
|
||||
/** Apple IIGS MIDI program change to instrument number mapping. */
|
||||
struct IIgsMidiProgramMapping {
|
||||
byte midiProgToInst[44]; ///< Lookup table for the MIDI program number to instrument number mapping
|
||||
byte undefinedInst; ///< The undefined instrument number
|
||||
|
||||
// Maps the MIDI program number to an instrument number
|
||||
byte map(uint midiProg) const {
|
||||
return midiProg < ARRAYSIZE(midiProgToInst) ? midiProgToInst[midiProg] : undefinedInst;
|
||||
}
|
||||
};
|
||||
|
||||
/** Apple IIGS AGI instrument set information. */
|
||||
struct IIgsInstrumentSetInfo {
|
||||
uint byteCount; ///< Length of the whole instrument set in bytes
|
||||
uint instCount; ///< Amount of instrument in the set
|
||||
const char *md5; ///< MD5 hex digest of the whole instrument set
|
||||
const char *waveFileMd5; ///< MD5 hex digest of the wave file (i.e. the sample data used by the instruments)
|
||||
const IIgsMidiProgramMapping *progToInst; ///< Program change to instrument number mapping
|
||||
};
|
||||
|
||||
/** Apple IIGS AGI executable file information. */
|
||||
struct IIgsExeInfo {
|
||||
enum AgiGameID gameid; ///< Game ID
|
||||
const char *exePrefix; ///< Prefix of the Apple IIGS AGI executable (e.g. "SQ", "PQ", "KQ4" etc)
|
||||
uint agiVer; ///< Apple IIGS AGI version number, not strictly needed
|
||||
uint exeSize; ///< Size of the Apple IIGS AGI executable file in bytes
|
||||
uint instSetStart; ///< Starting offset of the instrument set inside the executable file
|
||||
const IIgsInstrumentSetInfo *instSet; ///< Information about the used instrument set
|
||||
};
|
||||
|
||||
class IIgsMidiChannel {
|
||||
public:
|
||||
IIgsMidiChannel() : _instrument(NULL), _volume(127) {}
|
||||
void setInstrument(const IIgsInstrumentHeader *instrument) { _instrument = instrument; }
|
||||
const IIgsInstrumentHeader *getInstrument() { return _instrument; }
|
||||
void setVolume(int volume) { _volume = volume; }
|
||||
int getVolume() { return _volume; }
|
||||
private:
|
||||
const IIgsInstrumentHeader *_instrument; ///< Instrument used on this MIDI channel
|
||||
int _volume; ///< MIDI controller number 7 (Volume)
|
||||
};
|
||||
|
||||
class SoundGen2GS : public SoundGen, public Audio::AudioStream {
|
||||
public:
|
||||
SoundGen2GS(AgiBase *vm, Audio::Mixer *pMixer);
|
||||
~SoundGen2GS() override;
|
||||
|
||||
void play(int resnum) override;
|
||||
void stop() override;
|
||||
|
||||
int readBuffer(int16 *buffer, const int numSamples) override;
|
||||
|
||||
bool isStereo() const override { return true; }
|
||||
bool endOfData() const override { return false; }
|
||||
int getRate() const override { return _sampleRate; }
|
||||
|
||||
private:
|
||||
// Loader methods
|
||||
bool loadInstruments();
|
||||
bool loadInstrumentHeaders(const Common::Path &exePath, const IIgsExeInfo &exeInfo);
|
||||
bool loadWaveFile(const Common::Path &wavePath, const IIgsExeInfo &exeInfo);
|
||||
|
||||
const IIgsExeInfo *getIIgsExeInfo(enum AgiGameID gameid) const;
|
||||
void setProgramChangeMapping(const IIgsMidiProgramMapping *mapping);
|
||||
|
||||
// Player methods
|
||||
void advancePlayer(); ///< Advance the player
|
||||
void advanceMidiPlayer(); ///< Advance MIDI player
|
||||
uint generateOutput(); ///< Fill the output buffer
|
||||
|
||||
void haltGenerators(); ///< Halt all generators
|
||||
uint activeGenerators(); ///< How many generators are active?
|
||||
|
||||
void midiNoteOff(int channel, int note, int velocity);
|
||||
void midiNoteOn(int channel, int note, int velocity);
|
||||
double midiKeyToFreq(int key, double finetune);
|
||||
IIgsInstrumentHeader *getInstrument(uint8 program) { return &_instruments[_progToInst->map(program)]; }
|
||||
IIgsGenerator *allocateGenerator() { IIgsGenerator *g = &_generators[_nextGen++]; _nextGen %= 16; return g; }
|
||||
|
||||
bool _disableMidi; ///< Disable MIDI if loading instruments fail
|
||||
int _playingSound; ///< Resource number for the currently playing sound
|
||||
bool _playing; ///< True when the resource is still playing
|
||||
|
||||
IIgsGenerator _generators[MAX_GENERATORS]; ///< IIGS sound generators that are used to play single notes
|
||||
uint _nextGen; ///< Next generator available for allocation
|
||||
IIgsMidiChannel _channels[16]; ///< MIDI channels
|
||||
Common::Array<IIgsInstrumentHeader> _instruments; ///< Instrument data
|
||||
const IIgsMidiProgramMapping *_progToInst; ///< MIDI program number to instrument mapping
|
||||
int8 *_wavetable; ///< Sample data used by the instruments
|
||||
uint _ticks; ///< MIDI ticks (60Hz)
|
||||
int16 *_out; ///< Output buffer
|
||||
uint _outSize; ///< Output buffer size
|
||||
|
||||
static const int kSfxMidiChannel = 15; ///< MIDI channel used for playing sample resources
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SOUND_2GS_H */
|
||||
216
engines/agi/sound_a2.cpp
Normal file
216
engines/agi/sound_a2.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "audio/mixer.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/sound_a2.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// SoundGenA2 plays Apple II sounds.
|
||||
//
|
||||
// Apple II AGI sounds are a series of monotonic notes. They sound similar to
|
||||
// PC speaker versions, but they use a different resource format, and sound
|
||||
// playback is a blocking operation.
|
||||
//
|
||||
// The sound resource's values are based on the number of 6502 CPU cycles
|
||||
// consumed by AGI's play-note routine and the speed of the CPU. Playback was
|
||||
// driven by the engine's own inner loops instead of a timer, so games are
|
||||
// blocked until a sound is completed or interrupted by a key press.
|
||||
//
|
||||
// TODO: Migrate to Audio::PCSpeaker
|
||||
|
||||
static void calculateNote(uint16 clickCount, uint16 delayCount, float &freq, uint32 &duration_usec);
|
||||
static uint32 calculateDelayCycles(uint16 delayCount);
|
||||
static uint32 calculateTotalCycles(uint32 delayCycles, uint16 delayCount, uint16 clickCount);
|
||||
|
||||
SoundGenA2::SoundGenA2(AgiBase *vm, Audio::Mixer *pMixer) :
|
||||
_isPlaying(false),
|
||||
SoundGen(vm, pMixer) {
|
||||
|
||||
_mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
|
||||
}
|
||||
|
||||
SoundGenA2::~SoundGenA2() {
|
||||
_mixer->stopHandle(*_soundHandle);
|
||||
}
|
||||
|
||||
void SoundGenA2::play(int resnum) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
if (_vm->_game.sounds[resnum] == nullptr ||
|
||||
_vm->_game.sounds[resnum]->type() != AGI_SOUND_APPLE2) {
|
||||
error("Apple II sound %d not loaded", resnum);
|
||||
}
|
||||
|
||||
_speaker.stop();
|
||||
|
||||
// parse and enqueue all notes
|
||||
AgiSound *sound = _vm->_game.sounds[resnum];
|
||||
byte *data = sound->getData();
|
||||
uint32 dataLength = sound->getLength();
|
||||
for (uint32 i = 0; i + 4 < dataLength; i += 4) {
|
||||
uint16 clickCount = READ_LE_UINT16(&data[i]);
|
||||
uint16 delayCount = READ_LE_UINT16(&data[i + 2]);
|
||||
if (clickCount == 0xffff) {
|
||||
break;
|
||||
}
|
||||
|
||||
float freq;
|
||||
uint32 duration_usec;
|
||||
calculateNote(clickCount, delayCount, freq, duration_usec);
|
||||
|
||||
if (delayCount != 0) {
|
||||
_speaker.playQueue(Audio::PCSpeaker::kWaveFormSquare, freq, duration_usec);
|
||||
} else {
|
||||
_speaker.playQueue(Audio::PCSpeaker::kWaveFormSilence, 0, duration_usec);
|
||||
}
|
||||
}
|
||||
|
||||
_isPlaying = true;
|
||||
}
|
||||
|
||||
void SoundGenA2::stop() {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
_speaker.stop();
|
||||
_isPlaying = false;
|
||||
}
|
||||
|
||||
int SoundGenA2::readBuffer(int16 *buffer, const int numSamples) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
// if not playing then there are no samples
|
||||
if (!_isPlaying) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// fill the buffer with PCSpeaker samples
|
||||
int result = _speaker.readBuffer(buffer, numSamples);
|
||||
|
||||
// if PCSpeaker is no longer playing then sound is finished
|
||||
if (!_speaker.isPlaying()) {
|
||||
_isPlaying = false;
|
||||
_vm->_sound->soundIsFinished();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Apple II note calculations
|
||||
//
|
||||
// Each note is four bytes. Each byte controls how many iterations a loop makes
|
||||
// in AGI's play-note routine. If the last two bytes are zero then the "click"
|
||||
// instruction (LDA $C030) is skipped.
|
||||
//
|
||||
// The four bytes are conceptually two 16-bit little-endian values: the number
|
||||
// of clicks to perform and the delay before each click.
|
||||
//
|
||||
// Calculating a note's frequency and duration requires calculating the number
|
||||
// of CPU cycles spent delaying before each click and the number of CPU cycles
|
||||
// spent in the play-note routine, and then applying the CPU speed.
|
||||
//
|
||||
// play-note routine from Black Cauldron:
|
||||
//
|
||||
// 6583:A5 12 LDA VALTYP+1 0012 ; a = delayCount[0]
|
||||
// 6585:05 13 ORA GARFLG 0013 ; a |= delayCount[1]
|
||||
// 6587:85 14 STA SUBFLG 0014 ; playNote = delayCount != 0
|
||||
// ----------- begin counting cycles -----------
|
||||
// 6589:A6 13 LDX GARFLG 0013 ; x = delayCount[1]
|
||||
// 658B:A4 12 LDY VALTYP+1 0012 ; y = delayCount[0]
|
||||
// 658D:88 DEY ; x--
|
||||
// 658E:D0 FD BNE $658D 658D
|
||||
// 6590:CA DEX ; y--
|
||||
// 6591:10 FA BPL $658D 658D
|
||||
// 6593:A5 14 LDA SUBFLG 0014 ; a = playNote
|
||||
// 6595:F0 03 BEQ $659A 659A ; skip click if !playNote
|
||||
// 6597:AD 30 C0 LDA SPKR C030 ; *click*
|
||||
// 659A:C6 10 DEC DIMFLG 0010 ; clickCount[0]--
|
||||
// 659C:D0 EB BNE $6589 6589
|
||||
// 659E:C6 11 DEC VALTYP 0011 ; clickCount[1]--
|
||||
// 65A0:10 E7 BPL $6589 6589
|
||||
// ----------- end counting cycles -------------
|
||||
// 65A2:60 RTS
|
||||
|
||||
static void calculateNote(uint16 clickCount, uint16 delayCount, float &freq, uint32 &duration_usec) {
|
||||
// calculate CPU cycles
|
||||
uint32 delayCycles = calculateDelayCycles(delayCount);
|
||||
uint32 totalCycles = calculateTotalCycles(delayCycles, delayCount, clickCount);
|
||||
|
||||
// frequency is half the time spent delaying before a click,
|
||||
// because each click only toggles the speaker's state.
|
||||
// the average 6502 CPU cycle at 1.023 MHz is 0.98 microseconds.
|
||||
freq = 0.5f / (delayCycles * 0.00000098f);
|
||||
duration_usec = (uint32)(totalCycles * 0.98f);
|
||||
}
|
||||
|
||||
static uint32 calculateDelayCycles(uint16 delayCount) {
|
||||
bool playNote = (delayCount != 0);
|
||||
uint32 delayHighByte = delayCount >> 8;
|
||||
|
||||
uint32 cycles = 0;
|
||||
cycles += 3; // LDX
|
||||
cycles += 3; // LDY
|
||||
if (playNote) {
|
||||
cycles += (2 * delayCount); // DEY
|
||||
int bneNoBranchCount = (delayCount / 256) + 1;
|
||||
cycles += (3 * (delayCount - bneNoBranchCount)) + // BNE
|
||||
(2 * bneNoBranchCount);
|
||||
} else {
|
||||
cycles += ((2 + 3) * 256) - 1; // DEY, BNE - 1 for last 2-cycle BNE
|
||||
}
|
||||
cycles += 2 * (delayHighByte + 1); // DEX
|
||||
cycles += (3 * delayHighByte) + 2; // BPL (3 cycles, 2 cycles on last iteration)
|
||||
cycles += 3; // LDA playNote
|
||||
cycles += playNote ? 2 : 3; // BEQ (playNote)
|
||||
|
||||
return cycles;
|
||||
}
|
||||
|
||||
static uint32 calculateTotalCycles(uint32 delayCycles, uint16 delayCount, uint16 clickCount) {
|
||||
bool playNote = (delayCount != 0);
|
||||
uint32 clickHighByte = clickCount >> 8;
|
||||
|
||||
// click count should never be zero, but if it were, then the low byte
|
||||
// would wrap around and produce 256 clicks while the high byte would
|
||||
// be correctly interpreted as zero.
|
||||
if (clickCount == 0) {
|
||||
clickCount = 256;
|
||||
}
|
||||
|
||||
uint32 cycles = 0;
|
||||
cycles += delayCycles * clickCount; // every click incurs delayCycles
|
||||
if (playNote) {
|
||||
cycles += (4 * clickCount); // every click incurs LDA SPKR (the click!)
|
||||
}
|
||||
|
||||
cycles += 5 * clickCount; // DEC
|
||||
int bneNoBranchCount = (clickCount / 256) + 1;
|
||||
cycles += (3 * (clickCount - bneNoBranchCount)) + // BNE
|
||||
(2 * bneNoBranchCount);
|
||||
cycles += 5 * (clickHighByte + 1); // DEC
|
||||
cycles += (3 * clickHighByte) + 2; // BPL (3 cycles, 2 cycles on last iteration)
|
||||
|
||||
return cycles;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
60
engines/agi/sound_a2.h
Normal file
60
engines/agi/sound_a2.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SOUND_A2_H
|
||||
#define AGI_SOUND_A2_H
|
||||
|
||||
#include "audio/audiostream.h"
|
||||
#include "audio/softsynth/pcspk.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class SoundGenA2 : public SoundGen, public Audio::AudioStream {
|
||||
public:
|
||||
SoundGenA2(AgiBase *vm, Audio::Mixer *pMixer);
|
||||
~SoundGenA2() override;
|
||||
|
||||
void play(int resnum) override;
|
||||
void stop() override;
|
||||
|
||||
int readBuffer(int16 *buffer, const int numSamples) override;
|
||||
|
||||
bool isStereo() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool endOfData() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
int getRate() const override {
|
||||
return _speaker.getRate();
|
||||
}
|
||||
|
||||
private:
|
||||
Common::Mutex _mutex;
|
||||
bool _isPlaying;
|
||||
Audio::PCSpeakerStream _speaker;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SOUND_A2_H */
|
||||
142
engines/agi/sound_coco3.cpp
Normal file
142
engines/agi/sound_coco3.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "audio/mixer.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/sound_coco3.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// SoundGenCoCo3 plays Tandy Color Computer sounds.
|
||||
//
|
||||
// CoCo3 AGI sounds are a series of monotonic notes. They sound similar to
|
||||
// PC speaker versions, but they use a different resource format. As with
|
||||
// the Apple II, sound playback is a blocking operation.
|
||||
//
|
||||
// The sound resource is a stream of four-byte notes. The frequency is a table
|
||||
// index. The volume is a boolean. The duration changed between interpreters;
|
||||
// originally the units were 1/10 of a second, then 1/60.
|
||||
//
|
||||
// Thanks to Guillaume Major for documenting the sound format in their
|
||||
// conversion program, cc3snd.c.
|
||||
//
|
||||
// TODO: Migrate to Audio::PCSpeaker
|
||||
|
||||
static const uint16 cocoFrequencies[] = {
|
||||
130, 138, 146, 155, 164, 174, 184, 195, 207, 220, 233, 246,
|
||||
261, 277, 293, 311, 329, 349, 369, 391, 415, 440, 466, 493,
|
||||
523, 554, 587, 622, 659, 698, 739, 783, 830, 880, 932, 987,
|
||||
1046, 1108, 1174, 1244, 1318, 1396, 1479, 1567, 1661, 1760, 1864, 1975,
|
||||
2093, 2217, 2349, 2489, 2637, 2793, 2959, 3135, 3322, 3520, 3729, 3951
|
||||
};
|
||||
|
||||
SoundGenCoCo3::SoundGenCoCo3(AgiBase *vm, Audio::Mixer *pMixer) :
|
||||
_isPlaying(false),
|
||||
SoundGen(vm, pMixer) {
|
||||
|
||||
_mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
|
||||
}
|
||||
|
||||
SoundGenCoCo3::~SoundGenCoCo3() {
|
||||
_mixer->stopHandle(*_soundHandle);
|
||||
}
|
||||
|
||||
void SoundGenCoCo3::play(int resnum) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
if (_vm->_game.sounds[resnum] == nullptr ||
|
||||
_vm->_game.sounds[resnum]->type() != AGI_SOUND_COCO3) {
|
||||
error("CoCo3 sound %d not loaded", resnum);
|
||||
}
|
||||
|
||||
_speaker.stop();
|
||||
|
||||
// KQ3 (Int. 2.023) stored the duration in 1/10 of a second.
|
||||
// LSL1 (Int, 2.072) stored the duration in 1/60 of a second.
|
||||
// Fan ports have been made using both interpreters, but our
|
||||
// detection table doesn't capture this. For now, treat KQ3
|
||||
// as the early interpreter and all others as the later one.
|
||||
// TODO: create detection heuristic
|
||||
bool isEarlySound = (_vm->getGameID() == GID_KQ3);
|
||||
|
||||
// parse and enqueue all notes
|
||||
AgiSound *sound = _vm->_game.sounds[resnum];
|
||||
byte *data = sound->getData();
|
||||
uint32 dataLength = sound->getLength();
|
||||
for (uint32 i = 0; i + 4 < dataLength; i += 4) {
|
||||
// the third byte is apparently unused, and always zero
|
||||
byte freqIndex = data[i];
|
||||
byte volume = data[i + 1];
|
||||
byte duration = data[i + 3];
|
||||
if (freqIndex == 0xff) {
|
||||
break;
|
||||
}
|
||||
|
||||
// get duration in ticks (1/60 of a second)
|
||||
uint32 ticks = duration;
|
||||
if (isEarlySound) {
|
||||
ticks *= 6;
|
||||
}
|
||||
|
||||
// convert ticks to microseconds for PCSpeaker
|
||||
uint32 duration_usec = ticks * (1000000.0f / 60.0f);
|
||||
|
||||
// play if volume is non-zero (it's always 0x3f or zero)
|
||||
if (volume != 0 && freqIndex < ARRAYSIZE(cocoFrequencies)) {
|
||||
int freq = cocoFrequencies[freqIndex];
|
||||
_speaker.playQueue(Audio::PCSpeaker::kWaveFormSquare, freq, duration_usec);
|
||||
} else {
|
||||
_speaker.playQueue(Audio::PCSpeaker::kWaveFormSilence, 0, duration_usec);
|
||||
}
|
||||
}
|
||||
|
||||
_isPlaying = true;
|
||||
}
|
||||
|
||||
void SoundGenCoCo3::stop() {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
_speaker.stop();
|
||||
_isPlaying = false;
|
||||
}
|
||||
|
||||
int SoundGenCoCo3::readBuffer(int16 *buffer, const int numSamples) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
// if not playing then there are no samples
|
||||
if (!_isPlaying) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// fill the buffer with PCSpeaker samples
|
||||
int result = _speaker.readBuffer(buffer, numSamples);
|
||||
|
||||
// if PCSpeaker is no longer playing then sound is finished
|
||||
if (!_speaker.isPlaying()) {
|
||||
_isPlaying = false;
|
||||
_vm->_sound->soundIsFinished();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
60
engines/agi/sound_coco3.h
Normal file
60
engines/agi/sound_coco3.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SOUND_COCO3_H
|
||||
#define AGI_SOUND_COCO3_H
|
||||
|
||||
#include "audio/audiostream.h"
|
||||
#include "audio/softsynth/pcspk.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class SoundGenCoCo3 : public SoundGen, public Audio::AudioStream {
|
||||
public:
|
||||
SoundGenCoCo3(AgiBase *vm, Audio::Mixer *pMixer);
|
||||
~SoundGenCoCo3() override;
|
||||
|
||||
void play(int resnum) override;
|
||||
void stop() override;
|
||||
|
||||
int readBuffer(int16 *buffer, const int numSamples) override;
|
||||
|
||||
bool isStereo() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool endOfData() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
int getRate() const override {
|
||||
return _speaker.getRate();
|
||||
}
|
||||
|
||||
private:
|
||||
Common::Mutex _mutex;
|
||||
bool _isPlaying;
|
||||
Audio::PCSpeakerStream _speaker;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SOUND_COCO3_H */
|
||||
237
engines/agi/sound_midi.cpp
Normal file
237
engines/agi/sound_midi.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Code is based on:
|
||||
//
|
||||
// A very simple program, that converts an AGI-song into a MIDI-song.
|
||||
// Feel free to use it for anything.
|
||||
//
|
||||
// The default instrument is "piano" for all the channels, what gives
|
||||
// good results on most games. But I found, that some songs are interesting
|
||||
// with other instruments. If you want to experiment, modify the "instr"
|
||||
// array.
|
||||
//
|
||||
// Timing is not perfect, yet. It plays correct, when I use the
|
||||
// Gravis-Midiplayer, but the songs are too fast when I use playmidi on
|
||||
// Linux.
|
||||
//
|
||||
// Original program developed by Jens. Christian Restemeier
|
||||
//
|
||||
|
||||
// MIDI and digital music class
|
||||
|
||||
#include "audio/audiostream.h"
|
||||
#include "audio/mididrv.h"
|
||||
#include "audio/midiparser.h"
|
||||
#include "common/config-manager.h"
|
||||
#include "common/file.h"
|
||||
#include "common/memstream.h"
|
||||
#include "common/stream.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
|
||||
#include "agi/sound.h"
|
||||
#include "agi/sound_midi.h"
|
||||
|
||||
#define SPEED_FACTOR 6
|
||||
|
||||
namespace Agi {
|
||||
|
||||
static uint32 convertSND2MIDI(byte *snddata, byte **data);
|
||||
|
||||
SoundGenMIDI::SoundGenMIDI(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer), _isGM(false) {
|
||||
MidiPlayer::createDriver(MDT_MIDI | MDT_ADLIB);
|
||||
|
||||
int ret = _driver->open();
|
||||
if (ret == 0) {
|
||||
if (_nativeMT32)
|
||||
_driver->sendMT32Reset();
|
||||
else
|
||||
_driver->sendGMReset();
|
||||
|
||||
// FIXME: We need to cast "this" here due to the effects of
|
||||
// multiple inheritance. This hack can go away once this
|
||||
// setTimerCallback() has been moved inside Audio::MidiPlayer code.
|
||||
_driver->setTimerCallback(static_cast<Audio::MidiPlayer *>(this), &timerCallback);
|
||||
}
|
||||
}
|
||||
|
||||
void SoundGenMIDI::send(uint32 b) {
|
||||
if ((b & 0xF0) == 0xC0 && !_isGM && !_nativeMT32) {
|
||||
b = (b & 0xFFFF00FF) | MidiDriver::_mt32ToGm[(b >> 8) & 0xFF] << 8;
|
||||
}
|
||||
|
||||
Audio::MidiPlayer::send(b);
|
||||
}
|
||||
|
||||
void SoundGenMIDI::sendToChannel(byte channel, uint32 b) {
|
||||
if (!_channelsTable[channel]) {
|
||||
_channelsTable[channel] = (channel == 9) ? _driver->getPercussionChannel() : _driver->allocateChannel();
|
||||
// If a new channel is allocated during the playback, make sure
|
||||
// its volume is correctly initialized.
|
||||
if (_channelsTable[channel])
|
||||
_channelsTable[channel]->volume(_channelsVolume[channel] * _masterVolume / 255);
|
||||
}
|
||||
|
||||
if (_channelsTable[channel]) {
|
||||
if (_vm->getFlag(VM_FLAG_SOUND_ON))
|
||||
_channelsTable[channel]->send(b);
|
||||
else
|
||||
_channelsTable[channel]->send(0x7bb0 + channel); // all notes off
|
||||
}
|
||||
}
|
||||
|
||||
void SoundGenMIDI::endOfTrack() {
|
||||
stop();
|
||||
_vm->_sound->soundIsFinished();
|
||||
}
|
||||
|
||||
void SoundGenMIDI::play(int resnum) {
|
||||
stop();
|
||||
|
||||
_isGM = true;
|
||||
|
||||
AgiSound *track = _vm->_game.sounds[resnum];
|
||||
|
||||
// Convert AGI Sound data to MIDI
|
||||
int midiMusicSize = convertSND2MIDI(track->getData(), &_midiData);
|
||||
|
||||
MidiParser *parser = MidiParser::createParser_SMF();
|
||||
if (parser->loadMusic(_midiData, midiMusicSize)) {
|
||||
parser->setTrack(0);
|
||||
parser->setMidiDriver(this);
|
||||
parser->setTimerRate(_driver->getBaseTempo());
|
||||
parser->property(MidiParser::mpCenterPitchWheelOnUnload, 1);
|
||||
|
||||
_parser = parser;
|
||||
|
||||
syncVolume();
|
||||
|
||||
_isPlaying = true;
|
||||
} else {
|
||||
delete parser;
|
||||
}
|
||||
}
|
||||
|
||||
/* channel / instrument setup: */
|
||||
|
||||
/* most songs are good with this: */
|
||||
static const unsigned char instr[] = {0, 0, 0};
|
||||
|
||||
/* cool for sq2:
|
||||
static const unsigned char instr[] = {50, 51, 19};
|
||||
*/
|
||||
|
||||
static void writeDelta(Common::MemoryWriteStreamDynamic *st, int32 delta) {
|
||||
int32 i;
|
||||
|
||||
i = delta >> 21;
|
||||
if (i > 0)
|
||||
st->writeByte((i & 127) | 128);
|
||||
i = delta >> 14;
|
||||
if (i > 0)
|
||||
st->writeByte((i & 127) | 128);
|
||||
i = delta >> 7;
|
||||
if (i > 0)
|
||||
st->writeByte((i & 127) | 128);
|
||||
st->writeByte(delta & 127);
|
||||
}
|
||||
|
||||
static uint32 convertSND2MIDI(byte *snddata, byte **data) {
|
||||
int32 lp, ep;
|
||||
int n;
|
||||
double ll;
|
||||
|
||||
Common::MemoryWriteStreamDynamic st(DisposeAfterUse::NO);
|
||||
|
||||
ll = log10(pow(2.0, 1.0 / 12.0));
|
||||
|
||||
/* Header */
|
||||
st.write("MThd", 4);
|
||||
st.writeUint32BE(6);
|
||||
st.writeUint16BE(1); /* mode */
|
||||
st.writeUint16BE(3); /* number of tracks */
|
||||
st.writeUint16BE(192); /* ticks / quarter */
|
||||
|
||||
for (n = 0; n < 3; n++) {
|
||||
uint16 start, end, pos;
|
||||
|
||||
st.write("MTrk", 4);
|
||||
lp = st.pos();
|
||||
st.writeUint32BE(0); /* chunklength */
|
||||
writeDelta(&st, 0); /* set instrument */
|
||||
st.writeByte(0xc0 + n);
|
||||
st.writeByte(instr[n]);
|
||||
start = snddata[n * 2 + 0] | (snddata[n * 2 + 1] << 8);
|
||||
end = ((snddata[n * 2 + 2] | (snddata[n * 2 + 3] << 8))) - 5;
|
||||
|
||||
for (pos = start; pos < end; pos += 5) {
|
||||
uint16 freq, dur;
|
||||
dur = (snddata[pos + 0] | (snddata[pos + 1] << 8)) * SPEED_FACTOR;
|
||||
freq = ((snddata[pos + 2] & 0x3F) << 4) + (snddata[pos + 3] & 0x0F);
|
||||
if (snddata[pos + 2] > 0) {
|
||||
double fr;
|
||||
int note;
|
||||
/* I don't know, what frequency equals midi note 0 ... */
|
||||
/* This moves the song 4 octaves down: */
|
||||
fr = (log10(111860.0 / (double)freq) / ll) - 48;
|
||||
note = (int)floor(fr + 0.5);
|
||||
if (note < 0) note = 0;
|
||||
if (note > 127) note = 127;
|
||||
/* note on */
|
||||
writeDelta(&st, 0);
|
||||
st.writeByte(144 + n);
|
||||
st.writeByte(note);
|
||||
st.writeByte(100);
|
||||
/* note off */
|
||||
writeDelta(&st, dur);
|
||||
st.writeByte(128 + n);
|
||||
st.writeByte(note);
|
||||
st.writeByte(0);
|
||||
} else {
|
||||
/* note on */
|
||||
writeDelta(&st, 0);
|
||||
st.writeByte(144 + n);
|
||||
st.writeByte(0);
|
||||
st.writeByte(0);
|
||||
/* note off */
|
||||
writeDelta(&st, dur);
|
||||
st.writeByte(128 + n);
|
||||
st.writeByte(0);
|
||||
st.writeByte(0);
|
||||
}
|
||||
}
|
||||
writeDelta(&st, 0);
|
||||
st.writeByte(0xff);
|
||||
st.writeByte(0x2f);
|
||||
st.writeByte(0x0);
|
||||
ep = st.pos();
|
||||
st.seek(lp, SEEK_SET);
|
||||
st.writeUint32BE((ep - lp) - 4);
|
||||
st.seek(ep, SEEK_SET);
|
||||
}
|
||||
|
||||
*data = st.getData();
|
||||
|
||||
return st.pos();
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
55
engines/agi/sound_midi.h
Normal file
55
engines/agi/sound_midi.h
Normal file
@@ -0,0 +1,55 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Music class
|
||||
|
||||
#ifndef AGI_SOUND_MIDI_H
|
||||
#define AGI_SOUND_MIDI_H
|
||||
|
||||
#include "agi/sound.h"
|
||||
|
||||
#include "audio/midiplayer.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
class SoundGenMIDI : public SoundGen, public Audio::MidiPlayer {
|
||||
public:
|
||||
SoundGenMIDI(AgiBase *vm, Audio::Mixer *pMixer);
|
||||
|
||||
void play(int resnum) override;
|
||||
// We must overload stop() here to implement the pure virtual
|
||||
// stop() method of the SoundGen class.
|
||||
void stop() override { Audio::MidiPlayer::stop(); }
|
||||
|
||||
// MidiDriver_BASE interface implementation
|
||||
void send(uint32 b) override;
|
||||
|
||||
// Overload Audio::MidiPlayer method
|
||||
void sendToChannel(byte channel, uint32 b) override;
|
||||
void endOfTrack() override;
|
||||
|
||||
private:
|
||||
bool _isGM;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif
|
||||
589
engines/agi/sound_pcjr.cpp
Normal file
589
engines/agi/sound_pcjr.cpp
Normal file
@@ -0,0 +1,589 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/* Heavily based on code from NAGI
|
||||
*
|
||||
* COPYRIGHT AND PERMISSION NOTICE
|
||||
*
|
||||
* Copyright (c) 2001, 2001, 2002 Nick Sonneveld
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, and/or sell copies of the Software, and to permit persons
|
||||
* to whom the Software is furnished to do so, provided that the above
|
||||
* copyright notice(s) and this permission notice appear in all copies of
|
||||
* the Software and that both the above copyright notice(s) and this
|
||||
* permission notice appear in supporting documentation.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
* HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL
|
||||
* INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING
|
||||
* FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
|
||||
* WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*
|
||||
* Except as contained in this notice, the name of a copyright holder
|
||||
* shall not be used in advertising or otherwise to promote the sale, use
|
||||
* or other dealings in this Software without prior written authorization
|
||||
*
|
||||
*/
|
||||
|
||||
#include "audio/mixer.h"
|
||||
#include "agi/agi.h"
|
||||
#include "agi/sound.h"
|
||||
#include "agi/sound_pcjr.h"
|
||||
#include "common/config-manager.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// "fade out" or possibly "dissolve"
|
||||
// v2.9xx
|
||||
const int8 dissolveDataV2[] = {
|
||||
-2, -3, -2, -1,
|
||||
0x00, 0x00,
|
||||
0x01, 0x01, 0x01, 0x01,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x04, 0x04, 0x04, 0x04,
|
||||
0x05, 0x05, 0x05, 0x05,
|
||||
0x06, 0x06, 0x06, 0x06, 0x06,
|
||||
0x07, 0x07, 0x07, 0x07,
|
||||
0x08, 0x08, 0x08, 0x08,
|
||||
0x09, 0x09, 0x09, 0x09,
|
||||
0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B,
|
||||
0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
|
||||
0x0D,
|
||||
-100
|
||||
};
|
||||
|
||||
// v3
|
||||
const int8 dissolveDataV3[] = {
|
||||
-2, -3, -2, -1,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x06, 0x06, 0x06, 0x06, 0x06,
|
||||
0x07, 0x07, 0x07, 0x07,
|
||||
0x08, 0x08, 0x08, 0x08,
|
||||
0x09, 0x09, 0x09, 0x09,
|
||||
0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0B, 0x0B, 0x0B, 0x0B, 0x0B, 0x0B,
|
||||
0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
|
||||
0x0D,
|
||||
-100
|
||||
};
|
||||
|
||||
SoundGenPCJr::SoundGenPCJr(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer) {
|
||||
_chanAllocated = 10240; // preallocate something which will most likely fit
|
||||
_chanData = (int16 *)malloc(_chanAllocated << 1);
|
||||
|
||||
// Pick dissolve method
|
||||
//
|
||||
// 0 = no dissolve.. just play for as long as it's meant to be played
|
||||
// this was used in older v2.4 and under games i THINK
|
||||
// 1 = not used
|
||||
// 2 = v2.9+ games used a shorter dissolve
|
||||
// 3 (default) = v3 games used this dissolve pattern.. slightly longer
|
||||
if (_vm->getVersion() >= 0x3000)
|
||||
_dissolveMethod = 3;
|
||||
else if (_vm->getVersion() >= 0x2900)
|
||||
_dissolveMethod = 2;
|
||||
else
|
||||
_dissolveMethod = 0;
|
||||
|
||||
if (ConfMan.getBool("pcjr_16bitnoise"))
|
||||
_periodicNoiseMask = 0x10000; // non-standard, SEGA-like 16-bit periodic noise mask
|
||||
else
|
||||
_periodicNoiseMask = 0x08000; // default 15-bit periodic noise mask
|
||||
|
||||
memset(_channel, 0, sizeof(_channel));
|
||||
memset(_tchannel, 0, sizeof(_tchannel));
|
||||
|
||||
_v1data = nullptr;
|
||||
_v1size = 0;
|
||||
_v1duration = 0;
|
||||
|
||||
_reg = 0;
|
||||
|
||||
_mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
|
||||
}
|
||||
|
||||
SoundGenPCJr::~SoundGenPCJr() {
|
||||
free(_chanData);
|
||||
|
||||
_mixer->stopHandle(*_soundHandle);
|
||||
}
|
||||
|
||||
void SoundGenPCJr::play(int resnum) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
PCjrSound *pcjrSound = (PCjrSound *)_vm->_game.sounds[resnum];
|
||||
|
||||
for (int i = 0; i < CHAN_MAX; i++) {
|
||||
_channel[i].data = pcjrSound->getVoicePointer(i % 4);
|
||||
_channel[i].duration = 0;
|
||||
_channel[i].avail = 0xffff;
|
||||
_channel[i].dissolveCount = 0xFFFF;
|
||||
_channel[i].attenuation = 0;
|
||||
_channel[i].attenuationCopy = 0;
|
||||
_channel[i].genType = kGenSilence;
|
||||
_channel[i].freqCount = 0;
|
||||
|
||||
_tchannel[i].avail = 1;
|
||||
_tchannel[i].noteCount = 0;
|
||||
_tchannel[i].freqCount = 250;
|
||||
_tchannel[i].freqCountPrev = -1;
|
||||
_tchannel[i].atten = 0xF; // silence
|
||||
_tchannel[i].genType = kGenTone;
|
||||
_tchannel[i].genTypePrev = -1;
|
||||
_tchannel[i].count = 0;
|
||||
_tchannel[i].scale = 0;
|
||||
_tchannel[i].sign = 0;
|
||||
_tchannel[i].noiseState = 0;
|
||||
_tchannel[i].feedback = 0;
|
||||
}
|
||||
|
||||
_v1data = pcjrSound->getData() + 1;
|
||||
_v1size = pcjrSound->getLength() - 1;
|
||||
_v1duration = 0;
|
||||
|
||||
_reg = 0;
|
||||
}
|
||||
|
||||
void SoundGenPCJr::stop() {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
for (int i = 0; i < CHAN_MAX; i++) {
|
||||
_channel[i].avail = 0;
|
||||
_tchannel[i].avail = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int SoundGenPCJr::volumeCalc(SndGenChan *chan) {
|
||||
int8 attenuation, dissolveValue;
|
||||
|
||||
const int8 *dissolveData;
|
||||
|
||||
switch (_dissolveMethod) {
|
||||
case 2:
|
||||
dissolveData = dissolveDataV2;
|
||||
break;
|
||||
case 3:
|
||||
default:
|
||||
dissolveData = dissolveDataV3;
|
||||
break;
|
||||
}
|
||||
|
||||
assert(chan);
|
||||
|
||||
attenuation = chan->attenuation;
|
||||
if (attenuation != 0x0F) { // != silence
|
||||
if (chan->dissolveCount != 0xFFFF) {
|
||||
dissolveValue = dissolveData[chan->dissolveCount];
|
||||
if (dissolveValue == -100) { // if at end of list
|
||||
chan->dissolveCount = 0xFFFF;
|
||||
chan->attenuation = chan->attenuationCopy;
|
||||
attenuation = chan->attenuation;
|
||||
} else {
|
||||
chan->dissolveCount++;
|
||||
|
||||
attenuation += dissolveValue;
|
||||
if (attenuation < 0)
|
||||
attenuation = 0;
|
||||
if (attenuation > 0x0F)
|
||||
attenuation = 0x0F;
|
||||
|
||||
chan->attenuationCopy = attenuation;
|
||||
|
||||
attenuation &= 0x0F;
|
||||
}
|
||||
}
|
||||
//if (computer_type == 2) && (attenuation < 8)
|
||||
if (attenuation < 8)
|
||||
attenuation += 2;
|
||||
}
|
||||
|
||||
return attenuation;
|
||||
}
|
||||
|
||||
int SoundGenPCJr::getNextNote(int ch) {
|
||||
if (_vm->getVersion() > 0x2001)
|
||||
return getNextNote_v2(ch);
|
||||
else
|
||||
return getNextNote_v1(ch);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// read the next channel data.. fill it in *tone
|
||||
// if tone isn't touched.. it should be inited so it just plays silence
|
||||
// return 0 if it's passing more data
|
||||
// return -1 if it's passing nothing (end of data)
|
||||
int SoundGenPCJr::getNextNote_v2(int ch) {
|
||||
ToneChan *tpcm;
|
||||
SndGenChan *chan;
|
||||
const byte *data;
|
||||
|
||||
assert(ch < CHAN_MAX);
|
||||
|
||||
if (!_vm->getFlag(VM_FLAG_SOUND_ON))
|
||||
return -1;
|
||||
|
||||
tpcm = &_tchannel[ch];
|
||||
chan = &_channel[ch];
|
||||
if (!chan->avail)
|
||||
return -1;
|
||||
|
||||
while (chan->duration <= 0) {
|
||||
data = chan->data;
|
||||
|
||||
// read the duration of the note
|
||||
chan->duration = READ_LE_UINT16(data); // duration
|
||||
|
||||
// if it's 0 then it's not going to be played
|
||||
// if it's 0xFFFF then the channel data has finished.
|
||||
if ((chan->duration == 0) || (chan->duration == 0xFFFF)) {
|
||||
tpcm->genTypePrev = -1;
|
||||
tpcm->freqCountPrev = -1;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
tpcm->genTypePrev = tpcm->genType;
|
||||
tpcm->freqCountPrev = tpcm->freqCount;
|
||||
|
||||
// only tone channels dissolve
|
||||
if ((ch != 3) && (_dissolveMethod != 0)) // != noise??
|
||||
chan->dissolveCount = 0;
|
||||
|
||||
// attenuation (volume)
|
||||
writeData(data[4]);
|
||||
|
||||
// frequency
|
||||
writeData(data[3]);
|
||||
writeData(data[2]);
|
||||
|
||||
// data now points to the next data seg-a-ment
|
||||
chan->data += 5;
|
||||
}
|
||||
|
||||
if (chan->duration == 0xFFFF) {
|
||||
// kill channel
|
||||
chan->avail = 0;
|
||||
chan->attenuation = 0x0F; // silent
|
||||
chan->attenuationCopy = 0x0F; // dunno really
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
chan->duration--;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SoundGenPCJr::getNextNote_v1(int ch) {
|
||||
byte *data = _v1data;
|
||||
uint32 len = _v1size;
|
||||
|
||||
if (!_vm->getFlag(VM_FLAG_SOUND_ON))
|
||||
return -1;
|
||||
|
||||
if (len <= 0 || data == nullptr) {
|
||||
_channel[ch].avail = 0;
|
||||
_channel[ch].attenuation = 0x0F;
|
||||
_channel[ch].attenuationCopy = 0x0F;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// In the V1 player the default duration for a row is 3 ticks
|
||||
if (_v1duration > 0) {
|
||||
_v1duration--;
|
||||
return 0;
|
||||
}
|
||||
_v1duration = 3 * CHAN_MAX;
|
||||
|
||||
// Otherwise fetch a row of data for all channels
|
||||
while (*data) {
|
||||
writeData(*data);
|
||||
data++;
|
||||
len--;
|
||||
}
|
||||
data++;
|
||||
len--;
|
||||
|
||||
_v1data = data;
|
||||
_v1size = len;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SoundGenPCJr::writeData(uint8 val) {
|
||||
debugC(5, kDebugLevelSound, "writeData(%.2X)", val);
|
||||
|
||||
if ((val & 0x90) == 0x90) {
|
||||
_reg = (val >> 5) & 0x3;
|
||||
_channel[_reg].attenuation = val & 0xF;
|
||||
} else if ((val & 0xF0) == 0xE0) {
|
||||
_channel[3].genType = (val & 0x4) ? kGenWhite : kGenPeriod;
|
||||
int noiseFreq = val & 0x03;
|
||||
switch (noiseFreq) {
|
||||
case 0:
|
||||
_channel[3].freqCount = 32;
|
||||
break;
|
||||
case 1:
|
||||
_channel[3].freqCount = 64;
|
||||
break;
|
||||
case 2:
|
||||
_channel[3].freqCount = 128;
|
||||
break;
|
||||
case 3:
|
||||
_channel[3].freqCount = _channel[2].freqCount * 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (val & 0x80) {
|
||||
_reg = (val >> 5) & 0x3;
|
||||
_channel[_reg].freqCount = val & 0xF;
|
||||
_channel[_reg].genType = kGenTone;
|
||||
} else {
|
||||
_channel[_reg].freqCount |= (val & 0x3F) << 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Formulas for noise generator
|
||||
// bit0 = output
|
||||
|
||||
// noise feedback for white noise mode
|
||||
#define FB_WNOISE 0x12000 // bit15.d(16bits) = bit0(out) ^ bit2
|
||||
//#define FB_WNOISE 0x14000 // bit15.d(16bits) = bit0(out) ^ bit1
|
||||
//#define FB_WNOISE 0x28000 // bit16.d(17bits) = bit0(out) ^ bit2 (same to AY-3-8910)
|
||||
//#define FB_WNOISE 0x50000 // bit17.d(18bits) = bit0(out) ^ bit2
|
||||
|
||||
// noise feedback for periodic noise mode
|
||||
// it is correct maybe (it was in the Megadrive sound manual)
|
||||
//#define FB_PNOISE 0x10000 // 16bit rorate
|
||||
//#define FB_PNOISE 0x08000
|
||||
|
||||
// noise generator start preset (for periodic noise)
|
||||
#define NG_PRESET 0x0f35
|
||||
|
||||
//#define WAVE_HEIGHT (0x7FFF)
|
||||
|
||||
// Volume table.
|
||||
//
|
||||
// 2dB = 20*log(a/b)
|
||||
// 10^(2/20)*b = a;
|
||||
// value = 0x7fff;
|
||||
// value /= 1.258925411794;
|
||||
const int16 volTable[16] = {
|
||||
32767, 26027, 20674, 16422, 13044, 10361, 8230, 6537, 5193, 4125, 3276, 2602, 2067, 1642, 1304, 0
|
||||
};
|
||||
|
||||
#define FREQ_DIV 111844
|
||||
#define MULT FREQ_DIV
|
||||
|
||||
// fill buff
|
||||
int SoundGenPCJr::chanGen(int chan, int16 *stream, int len) {
|
||||
ToneChan *tpcm;
|
||||
int fillSize;
|
||||
int retVal;
|
||||
|
||||
tpcm = &_tchannel[chan];
|
||||
|
||||
retVal = -1;
|
||||
|
||||
while (len > 0) {
|
||||
if (tpcm->noteCount <= 0) {
|
||||
// get new tone data
|
||||
if ((tpcm->avail) && (getNextNote(chan) == 0)) {
|
||||
tpcm->atten = volumeCalc(&_channel[chan]);
|
||||
tpcm->freqCount = _channel[chan].freqCount;
|
||||
tpcm->genType = _channel[chan].genType;
|
||||
|
||||
// setup counters 'n stuff
|
||||
// SAMPLE_RATE samples per sec.. tone changes 60 times per sec
|
||||
tpcm->noteCount = SAMPLE_RATE / 60;
|
||||
retVal = 0;
|
||||
} else {
|
||||
// if it doesn't return an
|
||||
tpcm->genType = kGenSilence;
|
||||
tpcm->noteCount = len;
|
||||
tpcm->avail = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// write nothing
|
||||
if ((tpcm->freqCount == 0) || (tpcm->atten == 0xf)) {
|
||||
tpcm->genType = kGenSilence;
|
||||
}
|
||||
|
||||
// find which is smaller.. the buffer or the
|
||||
fillSize = (tpcm->noteCount <= len) ? tpcm->noteCount : len;
|
||||
|
||||
switch (tpcm->genType) {
|
||||
case kGenTone:
|
||||
fillSize = fillSquare(tpcm, stream, fillSize);
|
||||
break;
|
||||
case kGenPeriod:
|
||||
case kGenWhite:
|
||||
fillSize = fillNoise(tpcm, stream, fillSize);
|
||||
break;
|
||||
case kGenSilence:
|
||||
default:
|
||||
// fill with whitespace
|
||||
memset(stream, 0, fillSize * sizeof(int16));
|
||||
break;
|
||||
}
|
||||
|
||||
tpcm->noteCount -= fillSize;
|
||||
stream += fillSize;
|
||||
len -= fillSize;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
int SoundGenPCJr::fillSquare(ToneChan *t, int16 *buf, int len) {
|
||||
int count;
|
||||
|
||||
if (t->genType != t->genTypePrev) {
|
||||
// make sure the freqCount is checked
|
||||
t->freqCountPrev = -1;
|
||||
t->sign = 1;
|
||||
t->genTypePrev = t->genType;
|
||||
}
|
||||
|
||||
if (t->freqCount != t->freqCountPrev) {
|
||||
//t->scale = (int)( (double)t->samp->freq*t->freqCount/FREQ_DIV * MULT + 0.5);
|
||||
t->scale = (SAMPLE_RATE / 2) * t->freqCount;
|
||||
//t->count = t->scale;
|
||||
t->freqCountPrev = t->freqCount;
|
||||
}
|
||||
|
||||
count = len;
|
||||
|
||||
int16 amp = (int16)(volTable[t->atten] * _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / Audio::Mixer::kMaxMixerVolume);
|
||||
while (count > 0) {
|
||||
*(buf++) = t->sign ? amp : -amp;
|
||||
count--;
|
||||
|
||||
// get next sample
|
||||
t->count -= MULT;
|
||||
while (t->count <= 0) {
|
||||
t->sign ^= 1;
|
||||
t->count += t->scale;
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
int SoundGenPCJr::fillNoise(ToneChan *t, int16 *buf, int len) {
|
||||
int count;
|
||||
|
||||
if (t->genType != t->genTypePrev) {
|
||||
// make sure the freqCount is checked
|
||||
t->freqCountPrev = -1;
|
||||
t->genTypePrev = t->genType;
|
||||
}
|
||||
|
||||
if (t->freqCount != t->freqCountPrev) {
|
||||
//t->scale = (int)( (double)t->samp->freq*t->freqCount/FREQ_DIV * MULT + 0.5);
|
||||
t->scale = (SAMPLE_RATE / 2) * t->freqCount;
|
||||
t->count = t->scale;
|
||||
t->freqCountPrev = t->freqCount;
|
||||
|
||||
t->feedback = (t->genType == kGenWhite) ? FB_WNOISE : _periodicNoiseMask;
|
||||
// reset noise shifter
|
||||
t->noiseState = NG_PRESET;
|
||||
t->sign = t->noiseState & 1;
|
||||
}
|
||||
|
||||
count = len;
|
||||
|
||||
int16 amp = (int16)(volTable[t->atten] * _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType) / Audio::Mixer::kMaxMixerVolume);
|
||||
while (count > 0) {
|
||||
*(buf++) = t->sign ? amp : -amp;
|
||||
count--;
|
||||
|
||||
// get next sample
|
||||
t->count -= MULT;
|
||||
while (t->count <= 0) {
|
||||
if (t->noiseState & 1)
|
||||
t->noiseState ^= t->feedback;
|
||||
|
||||
t->noiseState >>= 1;
|
||||
t->sign = t->noiseState & 1;
|
||||
t->count += t->scale;
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
int SoundGenPCJr::readBuffer(int16 *stream, const int len) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
if (_chanAllocated < len) {
|
||||
free(_chanData);
|
||||
_chanData = (int16 *)malloc(len << 1);
|
||||
_chanAllocated = len;
|
||||
}
|
||||
memset(stream, 0, len << 1);
|
||||
|
||||
assert(stream);
|
||||
|
||||
bool finished = true;
|
||||
|
||||
for (int i = 0; i < CHAN_MAX; i++) {
|
||||
// get channel data(chan.userdata)
|
||||
if (chanGen(i, _chanData, len) == 0) {
|
||||
// divide by number of channels then add to stream
|
||||
int streamCount = len;
|
||||
int16 *sPtr = stream;
|
||||
int16 *cPtr = _chanData;
|
||||
|
||||
while (streamCount--)
|
||||
*(sPtr++) += *(cPtr++) / CHAN_MAX;
|
||||
|
||||
finished = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (finished)
|
||||
_vm->_sound->soundIsFinished();
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
130
engines/agi/sound_pcjr.h
Normal file
130
engines/agi/sound_pcjr.h
Normal file
@@ -0,0 +1,130 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SOUND_PCJR_H
|
||||
#define AGI_SOUND_PCJR_H
|
||||
|
||||
#include "audio/audiostream.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define CHAN_MAX 4
|
||||
|
||||
#define SAMPLE_RATE 22050
|
||||
|
||||
enum GenType {
|
||||
kGenSilence,
|
||||
kGenTone,
|
||||
kGenPeriod,
|
||||
kGenWhite
|
||||
};
|
||||
|
||||
struct SndGenChan {
|
||||
const byte *data;
|
||||
uint16 duration;
|
||||
uint16 avail; // turned on (1) but when the channel's data runs out, it's set to (0)
|
||||
uint16 dissolveCount;
|
||||
byte attenuation;
|
||||
byte attenuationCopy;
|
||||
|
||||
GenType genType;
|
||||
|
||||
// for the sample mixer
|
||||
int freqCount;
|
||||
};
|
||||
|
||||
struct ToneChan {
|
||||
int avail;
|
||||
|
||||
int noteCount; // length of tone.. duration
|
||||
|
||||
int freqCount;
|
||||
int freqCountPrev;
|
||||
int atten; // volume
|
||||
|
||||
GenType genType;
|
||||
int genTypePrev;
|
||||
|
||||
int count;
|
||||
int scale;
|
||||
int sign;
|
||||
unsigned int noiseState; /* noise generator */
|
||||
int feedback; /* noise feedback mask */
|
||||
};
|
||||
|
||||
class SoundGenPCJr : public SoundGen, public Audio::AudioStream {
|
||||
public:
|
||||
SoundGenPCJr(AgiBase *vm, Audio::Mixer *pMixer);
|
||||
~SoundGenPCJr() override;
|
||||
|
||||
void play(int resnum) override;
|
||||
void stop() override;
|
||||
|
||||
// AudioStream API
|
||||
int readBuffer(int16 *buffer, const int numSamples) override;
|
||||
|
||||
bool isStereo() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool endOfData() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
int getRate() const override {
|
||||
// FIXME: Ideally, we should use _sampleRate.
|
||||
return 22050;
|
||||
}
|
||||
|
||||
private:
|
||||
int getNextNote(int ch);
|
||||
int getNextNote_v2(int ch);
|
||||
int getNextNote_v1(int ch);
|
||||
int volumeCalc(SndGenChan *chan);
|
||||
|
||||
void writeData(uint8 val);
|
||||
|
||||
int chanGen(int chan, int16 *stream, int len);
|
||||
|
||||
int fillNoise(ToneChan *t, int16 *buf, int len);
|
||||
int fillSquare(ToneChan *t, int16 *buf, int len);
|
||||
|
||||
private:
|
||||
Common::Mutex _mutex;
|
||||
|
||||
SndGenChan _channel[CHAN_MAX];
|
||||
ToneChan _tchannel[CHAN_MAX];
|
||||
int16 *_chanData;
|
||||
int _chanAllocated;
|
||||
|
||||
int _dissolveMethod;
|
||||
int32 _periodicNoiseMask;
|
||||
|
||||
uint8 *_v1data;
|
||||
uint32 _v1size;
|
||||
int _v1duration;
|
||||
|
||||
int _reg;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SOUND_PCJR_H */
|
||||
372
engines/agi/sound_sarien.cpp
Normal file
372
engines/agi/sound_sarien.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/random.h"
|
||||
#include "audio/mixer.h"
|
||||
|
||||
#include "agi/agi.h"
|
||||
|
||||
#include "agi/sound_sarien.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define USE_INTERPOLATION
|
||||
|
||||
static const int16 waveformRamp[WAVEFORM_SIZE] = {
|
||||
0, 8, 16, 24, 32, 40, 48, 56,
|
||||
64, 72, 80, 88, 96, 104, 112, 120,
|
||||
128, 136, 144, 152, 160, 168, 176, 184,
|
||||
192, 200, 208, 216, 224, 232, 240, 255,
|
||||
0, -248, -240, -232, -224, -216, -208, -200,
|
||||
-192, -184, -176, -168, -160, -152, -144, -136,
|
||||
-128, -120, -112, -104, -96, -88, -80, -72,
|
||||
-64, -56, -48, -40, -32, -24, -16, -8 // Ramp up
|
||||
};
|
||||
|
||||
static const int16 waveformSquare[WAVEFORM_SIZE] = {
|
||||
255, 230, 220, 220, 220, 220, 220, 220,
|
||||
220, 220, 220, 220, 220, 220, 220, 220,
|
||||
220, 220, 220, 220, 220, 220, 220, 220,
|
||||
220, 220, 220, 220, 220, 220, 220, 110,
|
||||
-255, -230, -220, -220, -220, -220, -220, -220,
|
||||
-220, -220, -220, -220, -220, -220, -220, -220,
|
||||
-220, -220, -220, -220, -220, -220, -220, -220,
|
||||
-220, -220, -220, -110, 0, 0, 0, 0 // Square
|
||||
};
|
||||
|
||||
static const int16 waveformMac[WAVEFORM_SIZE] = {
|
||||
45, 110, 135, 161, 167, 173, 175, 176,
|
||||
156, 137, 123, 110, 91, 72, 35, -2,
|
||||
-60, -118, -142, -165, -170, -176, -177, -179,
|
||||
-177, -176, -164, -152, -117, -82, -17, 47,
|
||||
92, 137, 151, 166, 170, 173, 171, 169,
|
||||
151, 133, 116, 100, 72, 43, -7, -57,
|
||||
-99, -141, -156, -170, -174, -177, -178, -179,
|
||||
-175, -172, -165, -159, -137, -114, -67, -19
|
||||
};
|
||||
|
||||
/**
|
||||
* AGI sound note structure.
|
||||
*/
|
||||
struct AgiNote {
|
||||
uint16 duration; ///< Note duration
|
||||
uint16 freqDiv; ///< Note frequency divisor (10-bit)
|
||||
uint8 attenuation; ///< Note volume attenuation (4-bit)
|
||||
|
||||
/** Reads an AgiNote through the given pointer. */
|
||||
void read(const uint8 *ptr) {
|
||||
duration = READ_LE_UINT16(ptr);
|
||||
uint16 freqByte0 = *(ptr + 2); // Bits 4-9 of the frequency divisor
|
||||
uint16 freqByte1 = *(ptr + 3); // Bits 0-3 of the frequency divisor
|
||||
// Merge the frequency divisor's bits together into a single variable
|
||||
freqDiv = ((freqByte0 & 0x3F) << 4) | (freqByte1 & 0x0F);
|
||||
attenuation = *(ptr + 4) & 0x0F;
|
||||
}
|
||||
};
|
||||
|
||||
SoundGenSarien::SoundGenSarien(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer), _chn() {
|
||||
_sndBuffer = (int16 *)calloc(2, BUFFER_SIZE);
|
||||
|
||||
memset(_sndBuffer, 0, BUFFER_SIZE << 1);
|
||||
_env = false;
|
||||
_playingSound = -1;
|
||||
_playing = false;
|
||||
_useChorus = true; // FIXME: Currently always true?
|
||||
|
||||
switch (_vm->_soundemu) {
|
||||
default:
|
||||
case SOUND_EMU_NONE:
|
||||
_waveform = waveformRamp;
|
||||
_env = true;
|
||||
break;
|
||||
case SOUND_EMU_AMIGA:
|
||||
case SOUND_EMU_PC:
|
||||
_waveform = waveformSquare;
|
||||
break;
|
||||
case SOUND_EMU_MAC:
|
||||
_waveform = waveformMac;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_env) {
|
||||
debug(0, "Initializing sound: envelopes enabled (decay=%d, sustain=%d)", ENV_DECAY, ENV_SUSTAIN);
|
||||
} else {
|
||||
debug(0, "Initializing sound: envelopes disabled");
|
||||
}
|
||||
|
||||
_mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
|
||||
}
|
||||
|
||||
SoundGenSarien::~SoundGenSarien() {
|
||||
_mixer->stopHandle(*_soundHandle);
|
||||
|
||||
free(_sndBuffer);
|
||||
}
|
||||
|
||||
int SoundGenSarien::readBuffer(int16 *buffer, const int numSamples) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
fillAudio(buffer, numSamples / 2);
|
||||
|
||||
return numSamples;
|
||||
}
|
||||
|
||||
void SoundGenSarien::play(int resnum) {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
AgiSoundEmuType type = (AgiSoundEmuType)_vm->_game.sounds[resnum]->type();
|
||||
|
||||
assert(type == AGI_SOUND_4CHN);
|
||||
|
||||
_playingSound = resnum;
|
||||
|
||||
PCjrSound *pcjrSound = (PCjrSound *) _vm->_game.sounds[resnum];
|
||||
|
||||
// Initialize channel info
|
||||
for (int i = 0; i < NUM_CHANNELS; i++) {
|
||||
_chn[i].type = type;
|
||||
_chn[i].flags = AGI_SOUND_LOOP;
|
||||
|
||||
if (_env) {
|
||||
_chn[i].flags |= AGI_SOUND_ENVELOPE;
|
||||
_chn[i].adsr = AGI_SOUND_ENV_ATTACK;
|
||||
}
|
||||
|
||||
_chn[i].ins = _waveform;
|
||||
_chn[i].size = WAVEFORM_SIZE;
|
||||
_chn[i].ptr = pcjrSound->getVoicePointer(i % 4);
|
||||
_chn[i].timer = 0;
|
||||
_chn[i].vol = 0;
|
||||
_chn[i].end = 0;
|
||||
}
|
||||
|
||||
memset(_sndBuffer, 0, BUFFER_SIZE << 1);
|
||||
}
|
||||
|
||||
void SoundGenSarien::stop() {
|
||||
Common::StackLock lock(_mutex);
|
||||
|
||||
_playingSound = -1;
|
||||
|
||||
for (int i = 0; i < NUM_CHANNELS; i++)
|
||||
stopNote(i);
|
||||
}
|
||||
|
||||
void SoundGenSarien::stopNote(int i) {
|
||||
_chn[i].adsr = AGI_SOUND_ENV_RELEASE;
|
||||
|
||||
if (_useChorus) {
|
||||
// Stop chorus ;)
|
||||
if (_chn[i].type == AGI_SOUND_4CHN &&
|
||||
_vm->_soundemu == SOUND_EMU_NONE && i < 3) {
|
||||
stopNote(i + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SoundGenSarien::playNote(int i, int freq, int vol) {
|
||||
if (!_vm->getFlag(VM_FLAG_SOUND_ON))
|
||||
vol = 0;
|
||||
else if (vol && _vm->_soundemu == SOUND_EMU_PC)
|
||||
vol = 160;
|
||||
|
||||
_chn[i].phase = 0;
|
||||
_chn[i].freq = freq;
|
||||
_chn[i].vol = vol;
|
||||
_chn[i].env = 0x10000;
|
||||
_chn[i].adsr = AGI_SOUND_ENV_ATTACK;
|
||||
|
||||
if (_useChorus) {
|
||||
// Add chorus ;)
|
||||
if (_chn[i].type == AGI_SOUND_4CHN &&
|
||||
_vm->_soundemu == SOUND_EMU_NONE && i < 3) {
|
||||
|
||||
int newfreq = freq * 1007 / 1000;
|
||||
|
||||
if (freq == newfreq)
|
||||
newfreq++;
|
||||
|
||||
playNote(i + 4, newfreq, vol * 2 / 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SoundGenSarien::playSound() {
|
||||
int i;
|
||||
AgiNote note;
|
||||
|
||||
if (_playingSound == -1)
|
||||
return;
|
||||
|
||||
_playing = false;
|
||||
for (i = 0; i < (_vm->_soundemu == SOUND_EMU_PC ? 1 : 4); i++) {
|
||||
_playing |= !_chn[i].end;
|
||||
note.read(_chn[i].ptr); // Read a single note (Doesn't advance the pointer)
|
||||
|
||||
if (_chn[i].end)
|
||||
continue;
|
||||
|
||||
if ((--_chn[i].timer) <= 0) {
|
||||
stopNote(i);
|
||||
|
||||
if (note.freqDiv != 0) {
|
||||
int volume = (note.attenuation == 0x0F) ? 0 : (0xFF - note.attenuation * 2);
|
||||
playNote(i, note.freqDiv * 10, volume);
|
||||
}
|
||||
|
||||
_chn[i].timer = note.duration;
|
||||
|
||||
if (_chn[i].timer == 0xffff) {
|
||||
_chn[i].end = 1;
|
||||
_chn[i].vol = 0;
|
||||
_chn[i].env = 0;
|
||||
|
||||
if (_useChorus) {
|
||||
// chorus
|
||||
if (_chn[i].type == AGI_SOUND_4CHN && _vm->_soundemu == SOUND_EMU_NONE && i < 3) {
|
||||
_chn[i + 4].vol = 0;
|
||||
_chn[i + 4].env = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
_chn[i].ptr += 5; // Advance the pointer to the next note data (5 bytes per note)
|
||||
}
|
||||
}
|
||||
|
||||
if (!_playing) {
|
||||
_vm->_sound->soundIsFinished();
|
||||
|
||||
_playingSound = -1;
|
||||
}
|
||||
}
|
||||
|
||||
uint32 SoundGenSarien::mixSound() {
|
||||
int i, p;
|
||||
const int16 *src;
|
||||
int c, b, m;
|
||||
|
||||
memset(_sndBuffer, 0, BUFFER_SIZE << 1);
|
||||
|
||||
if (!_playing || _playingSound == -1)
|
||||
return BUFFER_SIZE;
|
||||
|
||||
// Handle PCjr 4-channel sound mixing here
|
||||
for (c = 0; c < NUM_CHANNELS; c++) {
|
||||
if (!_chn[c].vol)
|
||||
continue;
|
||||
|
||||
m = _chn[c].flags & AGI_SOUND_ENVELOPE ?
|
||||
_chn[c].vol * _chn[c].env >> 16 : _chn[c].vol;
|
||||
|
||||
if (_chn[c].type != AGI_SOUND_4CHN || c != 3) {
|
||||
src = _chn[c].ins;
|
||||
|
||||
p = _chn[c].phase;
|
||||
for (i = 0; i < BUFFER_SIZE; i++) {
|
||||
b = src[p >> 8];
|
||||
#ifdef USE_INTERPOLATION
|
||||
b += ((src[((p >> 8) + 1) % _chn[c].size] - src[p >> 8]) * (p & 0xff)) >> 8;
|
||||
#endif
|
||||
_sndBuffer[i] += (b * m) >> 4;
|
||||
|
||||
p += (uint32) 118600 * 4 / _chn[c].freq;
|
||||
|
||||
// FIXME: Fingolfin asks: why is there a FIXME here? Please either clarify what
|
||||
// needs fixing, or remove it!
|
||||
// FIXME
|
||||
if (_chn[c].flags & AGI_SOUND_LOOP) {
|
||||
p %= _chn[c].size << 8;
|
||||
} else {
|
||||
if (p >= _chn[c].size << 8) {
|
||||
p = _chn[c].vol = 0;
|
||||
_chn[c].end = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
_chn[c].phase = p;
|
||||
} else {
|
||||
// Add white noise
|
||||
for (i = 0; i < BUFFER_SIZE; i++) {
|
||||
b = _vm->_rnd->getRandomNumber(255) - 128;
|
||||
_sndBuffer[i] += (b * m) >> 4;
|
||||
}
|
||||
}
|
||||
|
||||
switch (_chn[c].adsr) {
|
||||
case AGI_SOUND_ENV_ATTACK:
|
||||
// not implemented
|
||||
_chn[c].adsr = AGI_SOUND_ENV_DECAY;
|
||||
break;
|
||||
case AGI_SOUND_ENV_DECAY:
|
||||
if (_chn[c].env > _chn[c].vol * ENV_SUSTAIN + ENV_DECAY) {
|
||||
_chn[c].env -= ENV_DECAY;
|
||||
} else {
|
||||
_chn[c].env = _chn[c].vol * ENV_SUSTAIN;
|
||||
_chn[c].adsr = AGI_SOUND_ENV_SUSTAIN;
|
||||
}
|
||||
break;
|
||||
case AGI_SOUND_ENV_SUSTAIN:
|
||||
break;
|
||||
case AGI_SOUND_ENV_RELEASE:
|
||||
if (_chn[c].env >= ENV_RELEASE) {
|
||||
_chn[c].env -= ENV_RELEASE;
|
||||
} else {
|
||||
_chn[c].env = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return BUFFER_SIZE;
|
||||
}
|
||||
|
||||
void SoundGenSarien::fillAudio(int16 *stream, uint len) {
|
||||
uint32 p = 0;
|
||||
|
||||
// current number of audio bytes in _sndBuffer
|
||||
static uint32 data_available = 0;
|
||||
// offset of start of audio bytes in _sndBuffer
|
||||
static uint32 data_offset = 0;
|
||||
|
||||
len <<= 2;
|
||||
|
||||
debugC(5, kDebugLevelSound, "(%p, %d)", (void *)stream, len);
|
||||
|
||||
while (len > data_available) {
|
||||
memcpy((uint8 *)stream + p, (uint8 *)_sndBuffer + data_offset, data_available);
|
||||
p += data_available;
|
||||
len -= data_available;
|
||||
|
||||
playSound();
|
||||
data_available = mixSound() << 1;
|
||||
data_offset = 0;
|
||||
}
|
||||
|
||||
memcpy((uint8 *)stream + p, (uint8 *)_sndBuffer + data_offset, len);
|
||||
data_offset += len;
|
||||
data_available -= len;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
118
engines/agi/sound_sarien.h
Normal file
118
engines/agi/sound_sarien.h
Normal file
@@ -0,0 +1,118 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SOUND_SARIEN_H
|
||||
#define AGI_SOUND_SARIEN_H
|
||||
|
||||
#include "audio/audiostream.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define BUFFER_SIZE 410
|
||||
|
||||
#define WAVEFORM_SIZE 64
|
||||
#define ENV_ATTACK 10000 /**< envelope attack rate */
|
||||
#define ENV_DECAY 1000 /**< envelope decay rate */
|
||||
#define ENV_SUSTAIN 100 /**< envelope sustain level */
|
||||
#define ENV_RELEASE 7500 /**< envelope release rate */
|
||||
#define NUM_CHANNELS 7 /**< number of sound channels */
|
||||
|
||||
enum AgiSoundFlags {
|
||||
AGI_SOUND_LOOP = 0x0001,
|
||||
AGI_SOUND_ENVELOPE = 0x0002
|
||||
};
|
||||
enum AgiSoundEnv {
|
||||
AGI_SOUND_ENV_ATTACK = 3,
|
||||
AGI_SOUND_ENV_DECAY = 2,
|
||||
AGI_SOUND_ENV_SUSTAIN = 1,
|
||||
AGI_SOUND_ENV_RELEASE = 0
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* AGI engine sound channel structure.
|
||||
*/
|
||||
struct ChannelInfo {
|
||||
AgiSoundEmuType type;
|
||||
const uint8 *ptr; // Pointer to the AgiNote data
|
||||
const int16 *ins;
|
||||
int32 size;
|
||||
uint32 phase;
|
||||
uint32 flags; // ORs values from AgiSoundFlags
|
||||
AgiSoundEnv adsr;
|
||||
int32 timer;
|
||||
uint32 end;
|
||||
uint32 freq;
|
||||
uint32 vol;
|
||||
uint32 env;
|
||||
};
|
||||
|
||||
class SoundGenSarien : public SoundGen, public Audio::AudioStream {
|
||||
public:
|
||||
SoundGenSarien(AgiBase *vm, Audio::Mixer *pMixer);
|
||||
~SoundGenSarien() override;
|
||||
|
||||
void play(int resnum) override;
|
||||
void stop() override;
|
||||
|
||||
// AudioStream API
|
||||
int readBuffer(int16 *buffer, const int numSamples) override;
|
||||
|
||||
bool isStereo() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool endOfData() const override {
|
||||
return false;
|
||||
}
|
||||
|
||||
int getRate() const override {
|
||||
// FIXME: Ideally, we should use _sampleRate.
|
||||
return 22050;
|
||||
}
|
||||
|
||||
private:
|
||||
Common::Mutex _mutex;
|
||||
|
||||
ChannelInfo _chn[NUM_CHANNELS];
|
||||
uint8 _env;
|
||||
|
||||
int16 *_sndBuffer;
|
||||
const int16 *_waveform;
|
||||
|
||||
bool _useChorus;
|
||||
|
||||
bool _playing;
|
||||
int _playingSound;
|
||||
|
||||
private:
|
||||
void playSound();
|
||||
uint32 mixSound();
|
||||
void fillAudio(int16 *stream, uint len);
|
||||
|
||||
void stopNote(int i);
|
||||
void playNote(int i, int freq, int vol);
|
||||
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SOUND_SARIEN_H */
|
||||
542
engines/agi/sprite.cpp
Normal file
542
engines/agi/sprite.cpp
Normal file
@@ -0,0 +1,542 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/sprite.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/text.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
SpritesMgr::SpritesMgr(AgiEngine *agi, GfxMgr *gfx) {
|
||||
_vm = agi;
|
||||
_gfx = gfx;
|
||||
}
|
||||
|
||||
SpritesMgr::~SpritesMgr() {
|
||||
_spriteRegularList.clear();
|
||||
_spriteStaticList.clear();
|
||||
}
|
||||
|
||||
static bool sortSpriteHelper(const Sprite &entry1, const Sprite &entry2) {
|
||||
if (entry1.sortOrder == entry2.sortOrder) {
|
||||
// If sort-order is the same, we sort according to given order
|
||||
// which makes this sort stable.
|
||||
return entry1.givenOrderNr < entry2.givenOrderNr;
|
||||
}
|
||||
return entry1.sortOrder < entry2.sortOrder;
|
||||
}
|
||||
|
||||
void SpritesMgr::buildRegularSpriteList() {
|
||||
ScreenObjEntry *screenObj = nullptr;
|
||||
uint16 givenOrderNr = 0;
|
||||
|
||||
freeList(_spriteRegularList);
|
||||
for (screenObj = _vm->_game.screenObjTable; screenObj < &_vm->_game.screenObjTable[SCREENOBJECTS_MAX]; screenObj++) {
|
||||
if ((screenObj->flags & (fAnimated | fUpdate | fDrawn)) == (fAnimated | fUpdate | fDrawn)) {
|
||||
buildSpriteListAdd(givenOrderNr, screenObj, _spriteRegularList);
|
||||
givenOrderNr++;
|
||||
}
|
||||
}
|
||||
|
||||
// Now sort this list
|
||||
Common::sort(_spriteRegularList.begin(), _spriteRegularList.end(), sortSpriteHelper);
|
||||
// warning("buildRegular: %d", _spriteRegularList.size());
|
||||
}
|
||||
|
||||
void SpritesMgr::buildStaticSpriteList() {
|
||||
ScreenObjEntry *screenObj = nullptr;
|
||||
uint16 givenOrderNr = 0;
|
||||
|
||||
freeList(_spriteStaticList);
|
||||
for (screenObj = _vm->_game.screenObjTable; screenObj < &_vm->_game.screenObjTable[SCREENOBJECTS_MAX]; screenObj++) {
|
||||
if ((screenObj->flags & (fAnimated | fUpdate | fDrawn)) == (fAnimated | fDrawn)) { // DIFFERENCE IN HERE!
|
||||
buildSpriteListAdd(givenOrderNr, screenObj, _spriteStaticList);
|
||||
givenOrderNr++;
|
||||
}
|
||||
}
|
||||
|
||||
// Now sort this list
|
||||
Common::sort(_spriteStaticList.begin(), _spriteStaticList.end(), sortSpriteHelper);
|
||||
}
|
||||
|
||||
void SpritesMgr::buildAllSpriteLists() {
|
||||
buildStaticSpriteList();
|
||||
buildRegularSpriteList();
|
||||
}
|
||||
|
||||
void SpritesMgr::buildSpriteListAdd(uint16 givenOrderNr, ScreenObjEntry *screenObj, SpriteList &spriteList) {
|
||||
Sprite spriteEntry;
|
||||
|
||||
// Check, if screen object points to currently loaded view, if not don't add it
|
||||
if (!(_vm->_game.dirView[screenObj->currentViewNr].flags & RES_LOADED))
|
||||
return;
|
||||
|
||||
spriteEntry.givenOrderNr = givenOrderNr;
|
||||
// warning("sprite add objNr %d", screenObjPtr->objectNr);
|
||||
if (screenObj->flags & fFixedPriority) {
|
||||
spriteEntry.sortOrder = _gfx->priorityToY(screenObj->priority);
|
||||
// warning(" - priorityToY (fixed) %d -> %d", screenObj->priority, spriteEntry.sortOrder);
|
||||
} else {
|
||||
spriteEntry.sortOrder = screenObj->yPos;
|
||||
// warning(" - Ypos %d -> %d", screenObjPtr->yPos, spriteEntry.sortOrder);
|
||||
}
|
||||
|
||||
spriteEntry.screenObjPtr = screenObj;
|
||||
spriteEntry.xPos = screenObj->xPos;
|
||||
spriteEntry.yPos = (screenObj->yPos) - (screenObj->ySize) + 1;
|
||||
spriteEntry.xSize = screenObj->xSize;
|
||||
spriteEntry.ySize = screenObj->ySize;
|
||||
|
||||
// Checking, if xPos/yPos/right/bottom are valid and do not go outside of playscreen (visual screen)
|
||||
// Original AGI did not do this (but it then resulted in memory corruption)
|
||||
if (spriteEntry.xPos < 0) {
|
||||
warning("buildSpriteListAdd(): ignoring screen obj %d, b/c xPos (%d) < 0", screenObj->objectNr, spriteEntry.xPos);
|
||||
return;
|
||||
}
|
||||
if (spriteEntry.yPos < 0) {
|
||||
warning("buildSpriteListAdd(): ignoring screen obj %d, b/c yPos (%d) < 0", screenObj->objectNr, spriteEntry.yPos);
|
||||
return;
|
||||
}
|
||||
int16 xRight = spriteEntry.xPos + spriteEntry.xSize;
|
||||
if (xRight > SCRIPT_HEIGHT) {
|
||||
warning("buildSpriteListAdd(): ignoring screen obj %d, b/c rightPos (%d) > %d", screenObj->objectNr, xRight, SCRIPT_WIDTH);
|
||||
return;
|
||||
}
|
||||
int16 yBottom = spriteEntry.yPos + spriteEntry.ySize;
|
||||
if (yBottom > SCRIPT_HEIGHT) {
|
||||
warning("buildSpriteListAdd(): ignoring screen obj %d, b/c bottomPos (%d) > %d", screenObj->objectNr, yBottom, SCRIPT_HEIGHT);
|
||||
return;
|
||||
}
|
||||
|
||||
// warning("list-add: %d, %d, original yPos: %d, ySize: %d", spriteEntry.xPos, spriteEntry.yPos, screenObj->yPos, screenObj->ySize);
|
||||
spriteEntry.backgroundBuffer = (uint8 *)malloc(spriteEntry.xSize * spriteEntry.ySize * 2); // for visual + priority data
|
||||
assert(spriteEntry.backgroundBuffer);
|
||||
spriteList.push_back(spriteEntry);
|
||||
}
|
||||
|
||||
void SpritesMgr::freeList(SpriteList &spriteList) {
|
||||
SpriteList::iterator iter;
|
||||
for (iter = spriteList.reverse_begin(); iter != spriteList.end(); iter--) {
|
||||
Sprite &sprite = *iter;
|
||||
|
||||
free(sprite.backgroundBuffer);
|
||||
}
|
||||
spriteList.clear();
|
||||
}
|
||||
|
||||
void SpritesMgr::freeRegularSprites() {
|
||||
freeList(_spriteRegularList);
|
||||
}
|
||||
|
||||
void SpritesMgr::freeStaticSprites() {
|
||||
freeList(_spriteStaticList);
|
||||
}
|
||||
|
||||
void SpritesMgr::freeAllSprites() {
|
||||
freeList(_spriteRegularList);
|
||||
freeList(_spriteStaticList);
|
||||
}
|
||||
|
||||
void SpritesMgr::eraseSprites(SpriteList &spriteList) {
|
||||
SpriteList::iterator iter;
|
||||
// warning("eraseSprites - count %d", spriteList.size());
|
||||
for (iter = spriteList.reverse_begin(); iter != spriteList.end(); iter--) {
|
||||
Sprite &sprite = *iter;
|
||||
_gfx->block_restore(sprite.xPos, sprite.yPos, sprite.xSize, sprite.ySize, sprite.backgroundBuffer);
|
||||
}
|
||||
|
||||
freeList(spriteList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase updating sprites.
|
||||
* This function follows the list of all updating sprites and restores
|
||||
* the visible and priority data of their background buffers back to
|
||||
* the AGI screen.
|
||||
*
|
||||
* @see erase_nonupd_sprites()
|
||||
* @see erase_both()
|
||||
*/
|
||||
void SpritesMgr::eraseRegularSprites() {
|
||||
eraseSprites(_spriteRegularList);
|
||||
}
|
||||
|
||||
void SpritesMgr::eraseStaticSprites() {
|
||||
eraseSprites(_spriteStaticList);
|
||||
}
|
||||
|
||||
void SpritesMgr::eraseSprites() {
|
||||
eraseSprites(_spriteRegularList);
|
||||
eraseSprites(_spriteStaticList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw all sprites in the given list.
|
||||
*/
|
||||
void SpritesMgr::drawSprites(SpriteList &spriteList) {
|
||||
for (auto &sprite : spriteList) {
|
||||
_gfx->block_save(sprite.xPos, sprite.yPos, sprite.xSize, sprite.ySize, sprite.backgroundBuffer);
|
||||
drawCel(sprite.screenObjPtr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blit updating sprites.
|
||||
* This function follows the list of all updating sprites and blits
|
||||
* them on the AGI screen.
|
||||
*
|
||||
* @see blit_nonupd_sprites()
|
||||
* @see blit_both()
|
||||
*/
|
||||
void SpritesMgr::drawRegularSpriteList() {
|
||||
debugC(7, kDebugLevelSprites, "drawRegularSpriteList()");
|
||||
drawSprites(_spriteRegularList);
|
||||
}
|
||||
|
||||
void SpritesMgr::drawStaticSpriteList() {
|
||||
//debugC(7, kDebugLevelSprites, "drawRegularSpriteList()");
|
||||
drawSprites(_spriteStaticList);
|
||||
}
|
||||
|
||||
void SpritesMgr::drawAllSpriteLists() {
|
||||
drawSprites(_spriteStaticList);
|
||||
drawSprites(_spriteRegularList);
|
||||
}
|
||||
|
||||
void SpritesMgr::drawCel(ScreenObjEntry *screenObj) {
|
||||
int16 curX = screenObj->xPos;
|
||||
int16 baseX = screenObj->xPos;
|
||||
int16 curY = screenObj->yPos;
|
||||
AgiViewCel *celPtr = screenObj->celData;
|
||||
byte *celDataPtr = celPtr->rawBitmap;
|
||||
uint8 remainingCelHeight = celPtr->height;
|
||||
uint8 celWidth = celPtr->width;
|
||||
byte celClearKey = celPtr->clearKey;
|
||||
byte viewPriority = screenObj->priority;
|
||||
byte screenPriority = 0;
|
||||
byte curColor = 0;
|
||||
byte isViewHidden = true;
|
||||
|
||||
// Adjust vertical position, given yPos is lower left, but we need upper left
|
||||
curY = curY - celPtr->height + 1;
|
||||
|
||||
while (remainingCelHeight) {
|
||||
for (int16 loopX = 0; loopX < celWidth; loopX++) {
|
||||
curColor = *celDataPtr++;
|
||||
|
||||
if (curColor != celClearKey) {
|
||||
screenPriority = _gfx->getPriority(curX, curY);
|
||||
if (screenPriority <= 2) {
|
||||
// control data found
|
||||
if (_gfx->checkControlPixel(curX, curY, viewPriority)) {
|
||||
_gfx->putPixel(curX, curY, GFX_SCREEN_MASK_VISUAL, curColor, 0);
|
||||
isViewHidden = false;
|
||||
}
|
||||
} else if (screenPriority <= viewPriority) {
|
||||
_gfx->putPixel(curX, curY, GFX_SCREEN_MASK_ALL, curColor, viewPriority);
|
||||
isViewHidden = false;
|
||||
}
|
||||
|
||||
}
|
||||
curX++;
|
||||
}
|
||||
|
||||
// go to next vertical position
|
||||
remainingCelHeight--;
|
||||
curX = baseX;
|
||||
curY++;
|
||||
}
|
||||
|
||||
if (screenObj->objectNr == 0) { // if ego, update if ego is visible at the moment
|
||||
_vm->setFlag(VM_FLAG_EGO_INVISIBLE, isViewHidden);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void SpritesMgr::showSprite(ScreenObjEntry *screenObj) {
|
||||
int16 x = 0;
|
||||
int16 y = 0;
|
||||
int16 width = 0;
|
||||
int16 height = 0;
|
||||
|
||||
int16 view_height_prev = 0;
|
||||
int16 view_width_prev = 0;
|
||||
|
||||
int16 y2 = 0;
|
||||
int16 height1 = 0;
|
||||
int16 height2 = 0;
|
||||
|
||||
int16 x2 = 0;
|
||||
int16 width1 = 0;
|
||||
int16 width2 = 0;
|
||||
|
||||
if (!_vm->_game.pictureShown)
|
||||
return;
|
||||
|
||||
view_height_prev = screenObj->ySize_prev;
|
||||
view_width_prev = screenObj->xSize_prev;
|
||||
|
||||
screenObj->ySize_prev = screenObj->ySize;
|
||||
screenObj->xSize_prev = screenObj->xSize;
|
||||
|
||||
if (screenObj->yPos < screenObj->yPos_prev) {
|
||||
y = screenObj->yPos_prev;
|
||||
y2 = screenObj->yPos;
|
||||
|
||||
height1 = view_height_prev;
|
||||
height2 = screenObj->ySize;
|
||||
} else {
|
||||
y = screenObj->yPos;
|
||||
y2 = screenObj->yPos_prev;
|
||||
|
||||
height1 = screenObj->ySize;
|
||||
height2 = view_height_prev;
|
||||
}
|
||||
|
||||
if ((y2 - height2) > (y - height1)) {
|
||||
height = height1;
|
||||
} else {
|
||||
height = y - y2 + height2;
|
||||
}
|
||||
|
||||
if (screenObj->xPos > screenObj->xPos_prev) {
|
||||
x = screenObj->xPos_prev;
|
||||
x2 = screenObj->xPos;
|
||||
width1 = view_width_prev;
|
||||
width2 = screenObj->xSize;
|
||||
} else {
|
||||
x = screenObj->xPos;
|
||||
x2 = screenObj->xPos_prev;
|
||||
width1 = screenObj->xSize;
|
||||
width2 = view_width_prev;
|
||||
}
|
||||
|
||||
if ((x2 + width2) < (x + width1)) {
|
||||
width = width1;
|
||||
} else {
|
||||
width = width2 + x2 - x;
|
||||
}
|
||||
|
||||
if ((x + width) > 161) {
|
||||
width = 161 - x;
|
||||
}
|
||||
|
||||
if (1 < (height - y)) {
|
||||
height = y + 1;
|
||||
}
|
||||
|
||||
// render this block
|
||||
int16 upperY = y - height + 1;
|
||||
_gfx->render_Block(x, upperY, width, height);
|
||||
}
|
||||
|
||||
void SpritesMgr::showSprites(SpriteList &spriteList) {
|
||||
ScreenObjEntry *screenObjPtr;
|
||||
|
||||
for (auto &sprite : spriteList) {
|
||||
screenObjPtr = sprite.screenObjPtr;
|
||||
|
||||
showSprite(screenObjPtr);
|
||||
|
||||
if (screenObjPtr->stepTimeCount == screenObjPtr->stepTime) {
|
||||
if ((screenObjPtr->xPos == screenObjPtr->xPos_prev) && (screenObjPtr->yPos == screenObjPtr->yPos_prev)) {
|
||||
screenObjPtr->flags |= fDidntMove;
|
||||
} else {
|
||||
screenObjPtr->xPos_prev = screenObjPtr->xPos;
|
||||
screenObjPtr->yPos_prev = screenObjPtr->yPos;
|
||||
screenObjPtr->flags &= ~fDidntMove;
|
||||
}
|
||||
}
|
||||
}
|
||||
_vm->_system->updateScreen();
|
||||
//_vm->_system->delayMillis(20);
|
||||
}
|
||||
|
||||
void SpritesMgr::showRegularSpriteList() {
|
||||
debugC(7, kDebugLevelSprites, "showRegularSpriteList()");
|
||||
showSprites(_spriteRegularList);
|
||||
}
|
||||
|
||||
void SpritesMgr::showStaticSpriteList() {
|
||||
debugC(7, kDebugLevelSprites, "showStaticSpriteList()");
|
||||
showSprites(_spriteStaticList);
|
||||
}
|
||||
|
||||
void SpritesMgr::showAllSpriteLists() {
|
||||
showSprites(_spriteStaticList);
|
||||
showSprites(_spriteRegularList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show object and description
|
||||
* This function shows an object from the player's inventory, displaying
|
||||
* a message box with the object description.
|
||||
* @param n Number of the object to show
|
||||
*/
|
||||
void SpritesMgr::showObject(int16 viewNr) {
|
||||
ScreenObjEntry screenObj;
|
||||
uint8 *backgroundBuffer = nullptr;
|
||||
|
||||
_vm->loadResource(RESOURCETYPE_VIEW, viewNr);
|
||||
_vm->setView(&screenObj, viewNr);
|
||||
|
||||
screenObj.ySize_prev = screenObj.celData->height;
|
||||
screenObj.xSize_prev = screenObj.celData->width;
|
||||
screenObj.xPos_prev = ((SCRIPT_WIDTH - 1) - screenObj.xSize) / 2;
|
||||
screenObj.xPos = screenObj.xPos_prev;
|
||||
screenObj.yPos_prev = SCRIPT_HEIGHT - 1;
|
||||
screenObj.yPos = screenObj.yPos_prev;
|
||||
screenObj.priority = 15;
|
||||
screenObj.flags = fFixedPriority; // Original AGI did "| fFixedPriority" on uninitialized memory
|
||||
screenObj.objectNr = 255; // ???
|
||||
|
||||
backgroundBuffer = (uint8 *)malloc(screenObj.xSize * screenObj.ySize * 2); // for visual + priority data
|
||||
|
||||
_gfx->block_save(screenObj.xPos, (screenObj.yPos - screenObj.ySize + 1), screenObj.xSize, screenObj.ySize, backgroundBuffer);
|
||||
drawCel(&screenObj);
|
||||
showSprite(&screenObj);
|
||||
|
||||
_vm->_text->messageBox((char *)_vm->_game.views[viewNr].description);
|
||||
|
||||
_gfx->block_restore(screenObj.xPos, (screenObj.yPos - screenObj.ySize + 1), screenObj.xSize, screenObj.ySize, backgroundBuffer);
|
||||
showSprite(&screenObj);
|
||||
|
||||
free(backgroundBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add view to picture.
|
||||
* This function is used to implement the add.to.pic AGI command. It
|
||||
* copies the specified cel from a view resource on the current picture.
|
||||
* This cel is not a sprite, it can't be moved or removed.
|
||||
* @param view number of view resource
|
||||
* @param loop number of loop in the specified view resource
|
||||
* @param cel number of cel in the specified loop
|
||||
* @param x x coordinate to place the view
|
||||
* @param y y coordinate to place the view
|
||||
* @param pri priority to use
|
||||
* @param mar if < 4, create a margin around the base of the cel
|
||||
*/
|
||||
void SpritesMgr::addToPic(int16 viewNr, int16 loopNr, int16 celNr, int16 xPos, int16 yPos, int16 priority, int16 border) {
|
||||
debugC(3, kDebugLevelSprites, "addToPic(view=%d, loop=%d, cel=%d, x=%d, y=%d, pri=%d, border=%d)", viewNr, loopNr, celNr, xPos, yPos, priority, border);
|
||||
|
||||
_vm->recordImageStackCall(ADD_VIEW, viewNr, loopNr, celNr, xPos, yPos, priority, border);
|
||||
|
||||
ScreenObjEntry *screenObj = &_vm->_game.addToPicView;
|
||||
screenObj->objectNr = -1; // addToPic-view
|
||||
|
||||
_vm->setView(screenObj, viewNr);
|
||||
_vm->setLoop(screenObj, loopNr);
|
||||
_vm->setCel(screenObj, celNr);
|
||||
|
||||
screenObj->xSize_prev = screenObj->xSize;
|
||||
screenObj->ySize_prev = screenObj->ySize;
|
||||
screenObj->xPos_prev = xPos;
|
||||
screenObj->xPos = xPos;
|
||||
screenObj->yPos_prev = yPos;
|
||||
screenObj->yPos = yPos;
|
||||
screenObj->flags = fIgnoreObjects | fIgnoreHorizon | fFixedPriority;
|
||||
screenObj->priority = 15;
|
||||
_vm->fixPosition(screenObj);
|
||||
if (priority == 0) {
|
||||
screenObj->flags = fIgnoreHorizon;
|
||||
}
|
||||
screenObj->priority = priority;
|
||||
|
||||
eraseSprites();
|
||||
|
||||
// bugs related to this code: required by Gold Rush (see Sarien bug #587558)
|
||||
if (screenObj->priority == 0) {
|
||||
screenObj->priority = _gfx->priorityFromY(screenObj->yPos);
|
||||
}
|
||||
drawCel(screenObj);
|
||||
|
||||
if (border <= 3) {
|
||||
// Create priority-box
|
||||
addToPicDrawPriorityBox(screenObj, border);
|
||||
}
|
||||
buildAllSpriteLists();
|
||||
drawAllSpriteLists();
|
||||
showSprite(screenObj);
|
||||
}
|
||||
|
||||
// bugs previously related to this:
|
||||
// Sarien bug #247)
|
||||
void SpritesMgr::addToPicDrawPriorityBox(ScreenObjEntry *screenObj, int16 border) {
|
||||
int16 priorityFromY = _gfx->priorityFromY(screenObj->yPos);
|
||||
int16 priorityHeight = 0;
|
||||
int16 curY = 0;
|
||||
int16 curX = 0;
|
||||
int16 height = 0;
|
||||
int16 width = 0;
|
||||
int16 offsetX = 0;
|
||||
|
||||
// Figure out the height of the box
|
||||
curY = screenObj->yPos;
|
||||
do {
|
||||
priorityHeight++;
|
||||
if (curY <= 0)
|
||||
break;
|
||||
curY--;
|
||||
} while (_gfx->priorityFromY(curY) == priorityFromY);
|
||||
|
||||
// box height may not be larger than the actual view
|
||||
if (screenObj->ySize < priorityHeight)
|
||||
priorityHeight = screenObj->ySize;
|
||||
|
||||
// now actually draw lower horizontal line
|
||||
curY = screenObj->yPos;
|
||||
curX = screenObj->xPos;
|
||||
|
||||
width = screenObj->xSize;
|
||||
while (width) {
|
||||
_gfx->putPixel(curX, curY, GFX_SCREEN_MASK_PRIORITY, 0, border);
|
||||
curX++;
|
||||
width--;
|
||||
}
|
||||
|
||||
if (priorityHeight > 1) {
|
||||
// Actual rectangle is needed
|
||||
curY = screenObj->yPos;
|
||||
curX = screenObj->xPos;
|
||||
offsetX = screenObj->xSize - 1;
|
||||
|
||||
height = priorityHeight - 1;
|
||||
while (height) {
|
||||
curY--;
|
||||
height--;
|
||||
_gfx->putPixel(curX, curY, GFX_SCREEN_MASK_PRIORITY, 0, border); // left line
|
||||
_gfx->putPixel(curX + offsetX, curY, GFX_SCREEN_MASK_PRIORITY, 0, border); // right line
|
||||
}
|
||||
|
||||
// and finally the upper horizontal line
|
||||
width = screenObj->xSize - 2;
|
||||
curX++;
|
||||
while (width > 0) {
|
||||
_gfx->putPixel(curX, curY, GFX_SCREEN_MASK_PRIORITY, 0, border);
|
||||
curX++;
|
||||
width--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
103
engines/agi/sprite.h
Normal file
103
engines/agi/sprite.h
Normal file
@@ -0,0 +1,103 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SPRITE_H
|
||||
#define AGI_SPRITE_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* Sprite structure.
|
||||
* This structure holds information on visible and priority data of
|
||||
* a rectangular area of the AGI screen. Sprites are chained in two
|
||||
* circular lists, one for updating and other for non-updating sprites.
|
||||
*/
|
||||
struct Sprite {
|
||||
uint16 givenOrderNr;
|
||||
uint16 sortOrder;
|
||||
ScreenObjEntry *screenObjPtr; /**< pointer to view table entry */
|
||||
int16 xPos; /**< x coordinate of the sprite */
|
||||
int16 yPos; /**< y coordinate of the sprite */
|
||||
int16 xSize; /**< width of the sprite */
|
||||
int16 ySize; /**< height of the sprite */
|
||||
byte *backgroundBuffer; /**< buffer to store background data */
|
||||
};
|
||||
|
||||
typedef Common::List<Sprite> SpriteList;
|
||||
typedef Common::List<Sprite *> SpritePtrList;
|
||||
|
||||
class AgiEngine;
|
||||
class GfxMgr;
|
||||
class Obejcts;
|
||||
|
||||
class SpritesMgr {
|
||||
private:
|
||||
GfxMgr *_gfx;
|
||||
AgiEngine *_vm;
|
||||
|
||||
//
|
||||
// Sprite management functions
|
||||
//
|
||||
|
||||
SpriteList _spriteRegularList;
|
||||
SpriteList _spriteStaticList;
|
||||
|
||||
public:
|
||||
void buildRegularSpriteList();
|
||||
void buildStaticSpriteList();
|
||||
void buildAllSpriteLists();
|
||||
void buildSpriteListAdd(uint16 givenOrderNr, ScreenObjEntry *screenObj, SpriteList &spriteList);
|
||||
void freeList(SpriteList &spriteList);
|
||||
void freeRegularSprites();
|
||||
void freeStaticSprites();
|
||||
void freeAllSprites();
|
||||
|
||||
void eraseSprites(SpriteList &spriteList);
|
||||
void eraseRegularSprites();
|
||||
void eraseStaticSprites();
|
||||
void eraseSprites();
|
||||
|
||||
void drawSprites(SpriteList &spriteList);
|
||||
void drawRegularSpriteList();
|
||||
void drawStaticSpriteList();
|
||||
void drawAllSpriteLists();
|
||||
|
||||
void drawCel(ScreenObjEntry *screenObj);
|
||||
|
||||
void showSprite(ScreenObjEntry *screenObj);
|
||||
void showSprites(SpriteList &spriteList);
|
||||
void showRegularSpriteList();
|
||||
void showStaticSpriteList();
|
||||
void showAllSpriteLists();
|
||||
|
||||
void showObject(int16 viewNr);
|
||||
|
||||
public:
|
||||
SpritesMgr(AgiEngine *agi, GfxMgr *gfx);
|
||||
~SpritesMgr();
|
||||
|
||||
void addToPic(int16 viewNr, int16 loopNr, int16 celNr, int16 xPos, int16 yPos, int16 priority, int16 border);
|
||||
void addToPicDrawPriorityBox(ScreenObjEntry *screenObj, int16 border);
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SPRITE_H */
|
||||
1234
engines/agi/systemui.cpp
Normal file
1234
engines/agi/systemui.cpp
Normal file
File diff suppressed because it is too large
Load Diff
165
engines/agi/systemui.h
Normal file
165
engines/agi/systemui.h
Normal file
@@ -0,0 +1,165 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_SYSTEMUI_H
|
||||
#define AGI_SYSTEMUI_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define SYSTEMUI_SAVEDGAME_MAXIMUM_SLOTS 100
|
||||
#define SYSTEMUI_SAVEDGAME_SLOTS_ON_SCREEN 12
|
||||
#define SYSTEMUI_SAVEDGAME_DESCRIPTION_LEN 30
|
||||
#define SYSTEMUI_SAVEDGAME_DISPLAYTEXT_LEN 31
|
||||
#define SYSTEMUI_SAVEDGAME_DISPLAYTEXT_PREFIX_LEN 3
|
||||
|
||||
struct SystemUISavedGameEntry {
|
||||
int16 slotId;
|
||||
bool exists;
|
||||
bool isValid;
|
||||
char description[SYSTEMUI_SAVEDGAME_DESCRIPTION_LEN + 1]; // actual description
|
||||
char displayText[SYSTEMUI_SAVEDGAME_DISPLAYTEXT_LEN + 1]; // modified description, meant for display purposes only
|
||||
};
|
||||
typedef Common::Array<SystemUISavedGameEntry> SystemUISavedGameArray;
|
||||
|
||||
struct SystemUIButtonEntry {
|
||||
Common::Rect rect;
|
||||
const char *text;
|
||||
int16 textWidth;
|
||||
bool active;
|
||||
bool isDefault;
|
||||
};
|
||||
typedef Common::Array<SystemUIButtonEntry> SystemUIButtonArray;
|
||||
|
||||
class SystemUI {
|
||||
public:
|
||||
SystemUI(AgiEngine *vm, GfxMgr *gfx, TextMgr *text);
|
||||
~SystemUI();
|
||||
|
||||
private:
|
||||
AgiEngine *_vm;
|
||||
GfxMgr *_gfx;
|
||||
TextMgr *_text;
|
||||
|
||||
public:
|
||||
const char *getStatusTextScore();
|
||||
const char *getStatusTextSoundOn();
|
||||
const char *getStatusTextSoundOff();
|
||||
|
||||
void pauseDialog();
|
||||
bool restartDialog();
|
||||
bool quitDialog();
|
||||
|
||||
private:
|
||||
|
||||
|
||||
public:
|
||||
const char *getInventoryTextNothing();
|
||||
const char *getInventoryTextYouAreCarrying();
|
||||
const char *getInventoryTextSelectItems();
|
||||
const char *getInventoryTextReturnToGame();
|
||||
|
||||
bool askForCommand(Common::String &commandText);
|
||||
|
||||
int16 figureOutAutomaticSaveGameSlot(const char *automaticSaveDescription);
|
||||
int16 figureOutAutomaticRestoreGameSlot(const char *automaticSaveDescription);
|
||||
|
||||
int16 askForSaveGameSlot();
|
||||
int16 askForRestoreGameSlot();
|
||||
bool askForSaveGameDescription(int16 slotId, Common::String &newDescription);
|
||||
|
||||
void savedGameSlot_KeyPress(uint16 newKey);
|
||||
|
||||
private:
|
||||
int16 askForSavedGameSlot(const char *slotListText);
|
||||
bool askForSavedGameVerification(const char *verifyText, const char *verifyButton1, const char *verifyButton2, const char *actualDescription, int16 slotId);
|
||||
|
||||
bool askForVerification(const char *verifyText, const char *button1Text, const char *button2Text, bool continueOnMessageBoxClick);
|
||||
|
||||
void createSavedGameDisplayText(char *destDisplayText, const char *actualDescription, int16 slotId, bool fillUpWithSpaces);
|
||||
void clearSavedGameSlots();
|
||||
void readSavedGameSlots(bool filterNonexistant, bool withAutoSaveSlot);
|
||||
void figureOutAutomaticSavedGameSlot(const char *automaticSaveDescription, int16 &matchedGameSlotId, int16 &freshGameSlotId);
|
||||
|
||||
void drawSavedGameSlots();
|
||||
void drawSavedGameSlotSelector(bool active);
|
||||
|
||||
SystemUISavedGameArray _savedGameArray;
|
||||
int16 _savedGameUpmostSlotNr;
|
||||
int16 _savedGameSelectedSlotNr;
|
||||
|
||||
private:
|
||||
SystemUIButtonArray _buttonArray;
|
||||
|
||||
Common::Rect createRect(int16 x, int16 adjX, int16 y, int16 adjY, int16 width, int16 adjWidth, int16 height, int16 adjHeight);
|
||||
|
||||
void drawButton(SystemUIButtonEntry *button);
|
||||
void drawButtonAppleIIgs(SystemUIButtonEntry *buttonEntry);
|
||||
void drawButtonAppleIIgsEdgePixels(int16 x, int16 adjX, int16 y, int16 adjY, byte *edgeBitmap, bool mirrored, bool upsideDown);
|
||||
void drawButtonAmiga(SystemUIButtonEntry *buttonEntry);
|
||||
void drawButtonAtariST(SystemUIButtonEntry *buttonEntry);
|
||||
|
||||
public:
|
||||
void askForVerificationKeyPress(uint16 newKey);
|
||||
|
||||
private:
|
||||
bool _askForVerificationContinueOnMessageBoxClick;
|
||||
bool _askForVerificationCancelled;
|
||||
int16 _askForVerificationMouseLockedButtonNr;
|
||||
int16 _askForVerificationMouseActiveButtonNr;
|
||||
|
||||
private:
|
||||
const char *_textStatusScore;
|
||||
const char *_textStatusSoundOn;
|
||||
const char *_textStatusSoundOff;
|
||||
|
||||
const char *_textEnterCommand;
|
||||
|
||||
const char *_textPause;
|
||||
const char *_textPauseButton;
|
||||
const char *_textRestart;
|
||||
const char *_textRestartButton1;
|
||||
const char *_textRestartButton2;
|
||||
const char *_textQuit;
|
||||
const char *_textQuitButton1;
|
||||
const char *_textQuitButton2;
|
||||
|
||||
const char *_textInventoryNothing;
|
||||
const char *_textInventoryYouAreCarrying;
|
||||
const char *_textInventorySelectItems;
|
||||
const char *_textInventoryReturnToGame;
|
||||
|
||||
const char *_textSaveGameSelectSlot;
|
||||
const char *_textSaveGameEnterDescription;
|
||||
const char *_textSaveGameVerify;
|
||||
const char *_textSaveGameVerifyButton1;
|
||||
const char *_textSaveGameVerifyButton2;
|
||||
|
||||
const char *_textRestoreGameNoSlots;
|
||||
const char *_textRestoreGameSelectSlot;
|
||||
const char *_textRestoreGameError;
|
||||
const char *_textRestoreGameVerify;
|
||||
const char *_textRestoreGameVerifyButton1;
|
||||
const char *_textRestoreGameVerifyButton2;
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_SYSTEMUI_H */
|
||||
1385
engines/agi/text.cpp
Normal file
1385
engines/agi/text.cpp
Normal file
File diff suppressed because it is too large
Load Diff
219
engines/agi/text.h
Normal file
219
engines/agi/text.h
Normal file
@@ -0,0 +1,219 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_TEXT_H
|
||||
#define AGI_TEXT_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
struct TextPos_Struct {
|
||||
int16 row;
|
||||
int16 column;
|
||||
};
|
||||
|
||||
#define TEXTPOSARRAY_MAX 5
|
||||
|
||||
struct TextAttrib_Struct {
|
||||
byte foreground;
|
||||
byte background;
|
||||
byte combinedForeground;
|
||||
byte combinedBackground;
|
||||
};
|
||||
|
||||
#define TEXTATTRIBARRAY_MAX 5
|
||||
|
||||
struct MessageState_Struct {
|
||||
uint8 type;
|
||||
int16 wanted_Text_Width;
|
||||
TextPos_Struct wanted_TextPos;
|
||||
bool dialogue_Open;
|
||||
uint8 newline_Char;
|
||||
bool window_Active;
|
||||
TextPos_Struct textPos;
|
||||
TextPos_Struct textPos_Edge;
|
||||
int16 textSize_Width;
|
||||
int16 textSize_Height;
|
||||
uint16 printed_Height;
|
||||
|
||||
int16 backgroundPos_x;
|
||||
int16 backgroundPos_y; // original AGI used lowerY here, we use upperY so that upscaling is easier
|
||||
int16 backgroundSize_Width;
|
||||
int16 backgroundSize_Height;
|
||||
};
|
||||
|
||||
// this defines here are for calculating character-size inside the visual-screen!
|
||||
#define FONT_VISUAL_WIDTH 4
|
||||
#define FONT_VISUAL_HEIGHT 8
|
||||
|
||||
#define FONT_DISPLAY_WIDTH 8
|
||||
#define FONT_DISPLAY_HEIGHT 8
|
||||
#define FONT_ROW_CHARACTERS 25
|
||||
#define FONT_COLUMN_CHARACTERS 40
|
||||
#define FONT_BYTES_PER_CHARACTER 8
|
||||
|
||||
#define HEIGHT_MAX 20
|
||||
|
||||
#define TEXT_STRING_MAX_SIZE 40
|
||||
|
||||
class TextMgr {
|
||||
private:
|
||||
Words *_words;
|
||||
GfxMgr *_gfx;
|
||||
AgiEngine *_vm;
|
||||
SystemUI *_systemUI;
|
||||
|
||||
public:
|
||||
TextMgr(AgiEngine *vm, Words *words, GfxMgr *gfx);
|
||||
~TextMgr();
|
||||
|
||||
void init(SystemUI *systemUI);
|
||||
|
||||
TextPos_Struct _textPos;
|
||||
int16 _textPosArrayCount;
|
||||
TextPos_Struct _textPosArray[TEXTPOSARRAY_MAX];
|
||||
|
||||
TextAttrib_Struct _textAttrib;
|
||||
int16 _textAttribArrayCount;
|
||||
TextAttrib_Struct _textAttribArray[TEXTATTRIBARRAY_MAX];
|
||||
|
||||
uint16 _window_Row_Min;
|
||||
uint16 _window_Row_Max;
|
||||
int16 _reset_Column;
|
||||
|
||||
void configureScreen(uint16 gameRow);
|
||||
uint16 getWindowRowMin();
|
||||
|
||||
void dialogueOpen();
|
||||
void dialogueClose();
|
||||
|
||||
void charPos_Clip(int16 &row, int16 &column);
|
||||
void charPos_Set(int16 row, int16 column);
|
||||
void charPos_Set(TextPos_Struct *posPtr);
|
||||
void charPos_Get(int16 &row, int16 &column);
|
||||
void charPos_Get(TextPos_Struct *posPtr);
|
||||
void charPos_Push();
|
||||
void charPos_Pop();
|
||||
void charPos_SetInsideWindow(int16 windowRow, int16 windowColumn);
|
||||
void charAttrib_Set(byte foreground, byte background);
|
||||
byte charAttrib_GetForeground();
|
||||
byte charAttrib_GetBackground();
|
||||
void charAttrib_Push();
|
||||
void charAttrib_Pop();
|
||||
byte calculateTextBackground(byte background);
|
||||
|
||||
void display(int16 textNr, int16 textRow, int16 textColumn);
|
||||
void displayAdjustRTL(int16 textRow, int16 textColumn, char *text, int16 calculatedWidth);
|
||||
void displayText(const char *textPtr, bool disabledLook = false);
|
||||
void displayCharacter(byte character, bool disabledLook = false);
|
||||
|
||||
void displayTextInsideWindow(const char *textPtr, int16 windowRow, int16 windowColumn);
|
||||
|
||||
MessageState_Struct _messageState;
|
||||
|
||||
void printAt(int16 textNr, int16 textPos_Row, int16 textPos_Column, int16 text_Width);
|
||||
void print(int16 textNr);
|
||||
|
||||
bool messageBox(const char *textPtr);
|
||||
void messageBox_KeyPress(uint16 newKey);
|
||||
|
||||
bool _messageBoxCancelled;
|
||||
|
||||
void drawMessageBox(const char *textPtr, int16 forcedHeight = 0, int16 wantedWidth = 0, bool forcedWidth = false);
|
||||
void getMessageBoxInnerDisplayDimensions(int16 &x, int16 &y, int16 &width, int16 &height);
|
||||
bool isMouseWithinMessageBox();
|
||||
void closeWindow(bool ttsStopSpeech = true);
|
||||
|
||||
void statusRow_Set(int16 row);
|
||||
int16 statusRow_Get();
|
||||
|
||||
void statusEnable();
|
||||
void statusDisable();
|
||||
bool statusEnabled();
|
||||
|
||||
void statusDraw(bool ttsVoiceScore = false, bool ttsVoiceSound = false);
|
||||
void statusClear();
|
||||
|
||||
bool _statusEnabled;
|
||||
int16 _statusRow;
|
||||
|
||||
void clearLine(int16 row, byte color);
|
||||
void clearLines(int16 row_Upper, int16 row_Lower, byte color);
|
||||
void clearBlock(int16 row_Upper, int16 column_Upper, int16 row_Lower, int16 column_Lower, byte color);
|
||||
|
||||
void clearBlockInsideWindow(int16 windowRow, int16 windowColumn, int16 width, byte color);
|
||||
|
||||
bool _inputEditEnabled;
|
||||
byte _inputCursorChar;
|
||||
|
||||
bool _optionCommandPromptWindow;
|
||||
|
||||
bool _promptEnabled;
|
||||
int16 _promptRow;
|
||||
int16 _promptCursorPos;
|
||||
byte _prompt[42];
|
||||
byte _promptPrevious[42];
|
||||
|
||||
bool inputGetEditStatus();
|
||||
void inputEditOn();
|
||||
void inputEditOff();
|
||||
void inputSetCursorChar(int16 cursorChar);
|
||||
byte inputGetCursorChar();
|
||||
|
||||
void promptReset();
|
||||
void promptEnable();
|
||||
void promptDisable();
|
||||
bool promptIsEnabled();
|
||||
|
||||
void promptRow_Set(int16 row);
|
||||
int16 promptRow_Get();
|
||||
void promptKeyPress(uint16 newKey);
|
||||
void promptCancelLine();
|
||||
void promptEchoLine();
|
||||
void promptRedraw();
|
||||
void promptClear(); // for AGI1
|
||||
void promptRememberForAutoComplete(bool entered = false); // for auto-completion
|
||||
|
||||
void promptCommandWindow(bool recallLastCommand, uint16 newKey);
|
||||
|
||||
int16 _inputStringRow;
|
||||
int16 _inputStringColumn;
|
||||
bool _inputStringEntered;
|
||||
int16 _inputStringMaxLen;
|
||||
int16 _inputStringCursorPos;
|
||||
byte _inputString[42];
|
||||
|
||||
bool stringWasEntered();
|
||||
void stringPos_Get(int16 &row, int16 &column);
|
||||
int16 stringGetMaxLen();
|
||||
void stringSet(const char *text);
|
||||
void stringEdit(int16 stringMaxLen);
|
||||
void stringKeyPress(uint16 newKey);
|
||||
void stringRememberForAutoComplete(bool entered = false); // for auto-completion
|
||||
|
||||
char *stringPrintf(const char *originalText);
|
||||
char *stringWordWrap(const char *originalText, int16 maxWidth, int16 *calculatedWidthPtr = nullptr, int16 *calculatedHeightPtr = nullptr);
|
||||
};
|
||||
|
||||
Common::String rightAlign(Common::String line, va_list args);
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_TEXT_H */
|
||||
740
engines/agi/view.cpp
Normal file
740
engines/agi/view.cpp
Normal file
@@ -0,0 +1,740 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/graphics.h"
|
||||
#include "agi/sprite.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
// Apple II V2+ views use different color values.
|
||||
static const byte apple2ViewColorMap[16] = {
|
||||
/*00*/ 0x00,
|
||||
/*01*/ 0x04,
|
||||
/*02*/ 0x01,
|
||||
/*03*/ 0x05,
|
||||
/*04*/ 0x02,
|
||||
/*05*/ 0x08,
|
||||
/*06*/ 0x09,
|
||||
/*07*/ 0x0b,
|
||||
/*08*/ 0x06,
|
||||
/*09*/ 0x0d,
|
||||
/*0a*/ 0x07,
|
||||
/*0b*/ 0x0c,
|
||||
/*0c*/ 0x0a,
|
||||
/*0d*/ 0x0e,
|
||||
/*0e*/ 0x03,
|
||||
/*0f*/ 0x0f
|
||||
};
|
||||
|
||||
void AgiEngine::updateView(ScreenObjEntry *screenObj) {
|
||||
if (screenObj->flags & fDontUpdate) {
|
||||
screenObj->flags &= ~fDontUpdate;
|
||||
return;
|
||||
}
|
||||
|
||||
int16 celNr = screenObj->currentCelNr;
|
||||
int16 lastCelNr = screenObj->celCount - 1;
|
||||
|
||||
switch (screenObj->cycle) {
|
||||
case kCycleNormal:
|
||||
celNr++;
|
||||
if (celNr > lastCelNr)
|
||||
celNr = 0;
|
||||
break;
|
||||
case kCycleEndOfLoop:
|
||||
if (celNr < lastCelNr) {
|
||||
debugC(5, kDebugLevelResources, "cel %d (last = %d)", celNr + 1, lastCelNr);
|
||||
if (++celNr != lastCelNr)
|
||||
break;
|
||||
}
|
||||
if (!screenObj->ignoreLoopFlag) {
|
||||
setFlag(screenObj->loop_flag, true);
|
||||
} else {
|
||||
warning("kCycleEndOfLoop: skip setting flag %d", screenObj->loop_flag);
|
||||
}
|
||||
screenObj->flags &= ~fCycling;
|
||||
screenObj->direction = 0;
|
||||
screenObj->cycle = kCycleNormal;
|
||||
break;
|
||||
case kCycleRevLoop:
|
||||
if (celNr) {
|
||||
celNr--;
|
||||
if (celNr)
|
||||
break;
|
||||
}
|
||||
if (!screenObj->ignoreLoopFlag) {
|
||||
setFlag(screenObj->loop_flag, true);
|
||||
} else {
|
||||
warning("kCycleRevLoop: skip setting flag %d", screenObj->loop_flag);
|
||||
}
|
||||
screenObj->flags &= ~fCycling;
|
||||
screenObj->direction = 0;
|
||||
screenObj->cycle = kCycleNormal;
|
||||
break;
|
||||
case kCycleReverse:
|
||||
if (celNr == 0) {
|
||||
celNr = lastCelNr;
|
||||
} else {
|
||||
celNr--;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
setCel(screenObj, celNr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an AGI view resource.
|
||||
* This function decodes the raw data of the specified AGI view resource
|
||||
* and fills the corresponding views array element.
|
||||
* @param viewNr number of view resource to decode
|
||||
*/
|
||||
int AgiEngine::decodeView(byte *resourceData, uint16 resourceSize, int16 viewNr) {
|
||||
AgiView *viewData = &_game.views[viewNr];
|
||||
|
||||
debugC(5, kDebugLevelResources, "decodeView(%d)", viewNr);
|
||||
|
||||
if (resourceSize < 5)
|
||||
error("unexpected end of view data for view %d", viewNr);
|
||||
|
||||
if (getVersion() < 0x2000) {
|
||||
viewData->headerStepSize = resourceData[0];
|
||||
viewData->headerCycleTime = resourceData[1];
|
||||
} else {
|
||||
viewData->headerStepSize = 0;
|
||||
viewData->headerCycleTime = 0;
|
||||
}
|
||||
|
||||
bool isAGI256Data = false;
|
||||
if (getFeatures() & GF_AGI256) {
|
||||
uint16 headerId = READ_LE_UINT16(resourceData);
|
||||
isAGI256Data = (headerId == 0xF00F); // AGI 256-2 view detected, 256 color view
|
||||
}
|
||||
|
||||
// Apple II V2+ views stopped including the first two bytes
|
||||
int headerLoopCountOffset = 2;
|
||||
int viewHeaderSize = 5;
|
||||
const bool apple2 = (getPlatform() == Common::kPlatformApple2) && getVersion() >= 0x2000;
|
||||
if (apple2) {
|
||||
headerLoopCountOffset = 0;
|
||||
viewHeaderSize = 3;
|
||||
}
|
||||
|
||||
byte headerLoopCount = resourceData[headerLoopCountOffset];
|
||||
uint16 headerDescriptionOffset = READ_LE_UINT16(resourceData + headerLoopCountOffset + 1);
|
||||
|
||||
viewData->loopCount = headerLoopCount;
|
||||
viewData->description = nullptr;
|
||||
viewData->loop = nullptr;
|
||||
|
||||
if (headerDescriptionOffset) {
|
||||
// Figure out length of description
|
||||
uint16 descriptionPos = headerDescriptionOffset;
|
||||
uint16 descriptionLen = 0;
|
||||
while (descriptionPos < resourceSize) {
|
||||
if (resourceData[descriptionPos] == 0)
|
||||
break;
|
||||
descriptionPos++;
|
||||
descriptionLen++;
|
||||
}
|
||||
// Allocate memory for description
|
||||
viewData->description = new byte[descriptionLen + 1];
|
||||
// Copy description over
|
||||
memcpy(viewData->description, resourceData + headerDescriptionOffset, descriptionLen);
|
||||
viewData->description[descriptionLen] = 0; // set terminator
|
||||
}
|
||||
|
||||
if (!viewData->loopCount) // no loops, exit now
|
||||
return errOK;
|
||||
|
||||
// Check, if at least the loop-offsets are available
|
||||
if (resourceSize < viewHeaderSize + (headerLoopCount * 2))
|
||||
error("unexpected end of view data for view %d", viewNr);
|
||||
|
||||
// Allocate space for loop-information
|
||||
AgiViewLoop *loopData = new AgiViewLoop[headerLoopCount];
|
||||
viewData->loop = loopData;
|
||||
|
||||
for (int16 loopNr = 0; loopNr < headerLoopCount; loopNr++) {
|
||||
uint16 loopOffset = READ_LE_UINT16(resourceData + viewHeaderSize + (loopNr * 2));
|
||||
|
||||
// Check, if at least the loop-header is available
|
||||
if (resourceSize < (loopOffset + 1))
|
||||
error("unexpected end of view data for view %d", viewNr);
|
||||
|
||||
// loop-header:
|
||||
// celCount:BYTE [ upper nibble used as mirror data in 2.230 ]
|
||||
// relativeCelOffset[0]:WORD
|
||||
// relativeCelOffset[1]:WORD
|
||||
// etc.
|
||||
byte loopHeaderCelCountByte = resourceData[loopOffset];
|
||||
const bool isMirrorDataInLoopHeader = (getVersion() == 0x2230);
|
||||
if (isMirrorDataInLoopHeader) {
|
||||
loopData->celCount = loopHeaderCelCountByte & 0x0f;
|
||||
} else {
|
||||
loopData->celCount = loopHeaderCelCountByte;
|
||||
}
|
||||
loopData->cel = nullptr;
|
||||
|
||||
// Check, if at least the cel-offsets for current loop are available
|
||||
if (resourceSize < (loopOffset + 1 + (loopData->celCount * 2)))
|
||||
error("unexpected end of view data for view %d", viewNr);
|
||||
|
||||
if (loopData->celCount) {
|
||||
// Allocate space for cel-information of current loop
|
||||
AgiViewCel *celData = new AgiViewCel[loopData->celCount];
|
||||
loopData->cel = celData;
|
||||
|
||||
for (int16 celNr = 0; celNr < loopData->celCount; celNr++) {
|
||||
uint16 celOffset = READ_LE_UINT16(resourceData + loopOffset + 1 + (celNr * 2));
|
||||
celOffset += loopOffset; // cel offset is relative to loop offset, so adjust accordingly
|
||||
|
||||
// Check, if at least the cel-header is available
|
||||
if (resourceSize < (celOffset + 3))
|
||||
error("unexpected end of view data for view %d", viewNr);
|
||||
|
||||
// cel-header:
|
||||
// width:BYTE
|
||||
// height:BYTE
|
||||
// Transparency + Mirroring:BYTE
|
||||
// celData follows
|
||||
byte celHeaderWidth = resourceData[celOffset + 0];
|
||||
byte celHeaderHeight = resourceData[celOffset + 1];
|
||||
byte celHeaderTransparencyMirror = resourceData[celOffset + 2];
|
||||
if (apple2) {
|
||||
// Apple II views switched the transparency and mirror bits
|
||||
celHeaderTransparencyMirror = (celHeaderTransparencyMirror << 4) | (celHeaderTransparencyMirror >> 4);
|
||||
}
|
||||
|
||||
byte celHeaderClearKey;
|
||||
bool celHeaderMirrored = false;
|
||||
if (!isAGI256Data) {
|
||||
// regular AGI view data
|
||||
// Transparency + Mirroring byte is as follows:
|
||||
// Bit 0-3 - clear key
|
||||
// Bit 4-6 - original loop, that is not supposed to be mirrored in any case
|
||||
// Bit 7 - apply mirroring
|
||||
celHeaderClearKey = celHeaderTransparencyMirror & 0x0F; // bit 0-3 is the clear key
|
||||
if (apple2) {
|
||||
// Apple II views use different color values
|
||||
celHeaderClearKey = apple2ViewColorMap[celHeaderClearKey];
|
||||
}
|
||||
|
||||
if (isMirrorDataInLoopHeader) {
|
||||
// 2.230 (early version of xmascard): mirror data is in loop header.
|
||||
if (loopHeaderCelCountByte & 0x80) {
|
||||
// mirror bit is set
|
||||
// there is a second mirror bit whose purpose is currently unknown;
|
||||
// both bits are set in every xmascard loop with mirror data.
|
||||
byte celHeaderMirrorLoop = (loopHeaderCelCountByte >> 4) & 0x03;
|
||||
if (celHeaderMirrorLoop != loopNr) {
|
||||
// only set to mirrored in case we are not the original loop
|
||||
celHeaderMirrored = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 2.272+: mirror data is in cel header
|
||||
if (celHeaderTransparencyMirror & 0x80) {
|
||||
// mirror bit is set
|
||||
byte celHeaderMirrorLoop = (celHeaderTransparencyMirror >> 4) & 0x07;
|
||||
if (celHeaderMirrorLoop != loopNr) {
|
||||
// only set to mirrored in case we are not the original loop
|
||||
celHeaderMirrored = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// AGI256-2 view data
|
||||
celHeaderClearKey = celHeaderTransparencyMirror; // full 8 bits for clear key
|
||||
}
|
||||
|
||||
celData->width = celHeaderWidth;
|
||||
celData->height = celHeaderHeight;
|
||||
celData->clearKey = celHeaderClearKey;
|
||||
celData->mirrored = celHeaderMirrored;
|
||||
|
||||
// Now decompress cel-data
|
||||
if ((celHeaderWidth == 0) && (celHeaderHeight == 0))
|
||||
error("view cel is 0x0");
|
||||
|
||||
byte *celCompressedData = resourceData + celOffset + 3;
|
||||
uint16 celCompressedSize = resourceSize - (celOffset + 3);
|
||||
|
||||
if (celCompressedSize == 0)
|
||||
error("compressed size of loop within view %d is 0 bytes", viewNr);
|
||||
|
||||
if (!isAGI256Data) {
|
||||
unpackViewCelData(celData, celCompressedData, celCompressedSize, viewNr);
|
||||
} else {
|
||||
unpackViewCelDataAGI256(celData, celCompressedData, celCompressedSize, viewNr);
|
||||
}
|
||||
celData++;
|
||||
}
|
||||
}
|
||||
|
||||
loopData++;
|
||||
}
|
||||
|
||||
return errOK;
|
||||
}
|
||||
|
||||
void AgiEngine::unpackViewCelData(AgiViewCel *celData, byte *compressedData, uint16 compressedSize, int16 viewNr) {
|
||||
byte *rawBitmap = new byte[celData->width * celData->height];
|
||||
int16 remainingHeight = celData->height;
|
||||
int16 remainingWidth = celData->width;
|
||||
int16 adjustPreChangeSingle = 0;
|
||||
int16 adjustAfterChangeSingle = +1;
|
||||
const bool apple2 = (getPlatform() == Common::kPlatformApple2) && getVersion() >= 0x2000;
|
||||
|
||||
celData->rawBitmap = rawBitmap;
|
||||
|
||||
if (celData->mirrored) {
|
||||
adjustPreChangeSingle = -1;
|
||||
adjustAfterChangeSingle = 0;
|
||||
rawBitmap += celData->width;
|
||||
}
|
||||
|
||||
while (remainingHeight) {
|
||||
if (!compressedSize)
|
||||
error("unexpected end of data, while unpacking view %d", viewNr);
|
||||
|
||||
byte curByte = *compressedData++;
|
||||
compressedSize--;
|
||||
|
||||
byte curColor;
|
||||
byte curChunkLen;
|
||||
if (curByte == 0) {
|
||||
curColor = celData->clearKey;
|
||||
curChunkLen = remainingWidth;
|
||||
} else {
|
||||
curColor = curByte >> 4;
|
||||
curChunkLen = curByte & 0x0F;
|
||||
if (curChunkLen > remainingWidth)
|
||||
error("invalid chunk in view %d", viewNr);
|
||||
if (apple2) {
|
||||
curColor = apple2ViewColorMap[curColor];
|
||||
}
|
||||
}
|
||||
|
||||
switch (curChunkLen) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
rawBitmap += adjustPreChangeSingle;
|
||||
*rawBitmap = curColor;
|
||||
rawBitmap += adjustAfterChangeSingle;
|
||||
break;
|
||||
default:
|
||||
if (celData->mirrored)
|
||||
rawBitmap -= curChunkLen;
|
||||
memset(rawBitmap, curColor, curChunkLen);
|
||||
if (!celData->mirrored)
|
||||
rawBitmap += curChunkLen;
|
||||
break;
|
||||
}
|
||||
|
||||
remainingWidth -= curChunkLen;
|
||||
|
||||
// Each row is terminated by a zero byte; any remaining pixels are transparent.
|
||||
// Apple II views don't use terminators, instead they explicitly draw remaining
|
||||
// transparent pixels with a normal chunk byte, and rows end when they're full.
|
||||
// The Apple II method uses one less byte on full rows.
|
||||
if (curByte == 0 || (apple2 && remainingWidth == 0)) {
|
||||
remainingWidth = celData->width;
|
||||
remainingHeight--;
|
||||
|
||||
if (celData->mirrored)
|
||||
rawBitmap += celData->width * 2;
|
||||
}
|
||||
}
|
||||
|
||||
// for CGA rendering, apply dithering
|
||||
switch (_renderMode) {
|
||||
case Common::kRenderCGA: {
|
||||
uint16 totalPixels = celData->width * celData->height;
|
||||
|
||||
// dither clear key
|
||||
celData->clearKey = _gfx->getCGAMixtureColor(celData->clearKey);
|
||||
|
||||
rawBitmap = celData->rawBitmap;
|
||||
for (uint16 pixelNr = 0; pixelNr < totalPixels; pixelNr++) {
|
||||
byte curColor = *rawBitmap;
|
||||
*rawBitmap = _gfx->getCGAMixtureColor(curColor);
|
||||
rawBitmap++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AgiEngine::unpackViewCelDataAGI256(AgiViewCel *celData, byte *compressedData, uint16 compressedSize, int16 viewNr) {
|
||||
byte *rawBitmap = new byte[celData->width * celData->height];
|
||||
int16 remainingHeight = celData->height;
|
||||
int16 remainingWidth = celData->width;
|
||||
|
||||
celData->rawBitmap = rawBitmap;
|
||||
|
||||
while (remainingHeight) {
|
||||
if (!compressedSize)
|
||||
error("unexpected end of data, while unpacking AGI256 view %d", viewNr);
|
||||
|
||||
byte curByte = *compressedData++;
|
||||
compressedSize--;
|
||||
|
||||
if (curByte == 0) {
|
||||
// Go to next vertical position
|
||||
if (remainingWidth) {
|
||||
// fill remaining bytes with clear key
|
||||
memset(rawBitmap, celData->clearKey, remainingWidth);
|
||||
rawBitmap += remainingWidth;
|
||||
remainingWidth = 0;
|
||||
}
|
||||
} else {
|
||||
if (!remainingWidth) {
|
||||
error("broken view data, while unpacking AGI256 view %d", viewNr);
|
||||
break;
|
||||
}
|
||||
*rawBitmap = curByte;
|
||||
rawBitmap++;
|
||||
remainingWidth--;
|
||||
}
|
||||
|
||||
if (curByte == 0) {
|
||||
remainingWidth = celData->width;
|
||||
remainingHeight--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads all data in a view resource
|
||||
* @param viewNr number of view resource
|
||||
*/
|
||||
void AgiEngine::unloadView(int16 viewNr) {
|
||||
AgiView *viewData = &_game.views[viewNr];
|
||||
|
||||
debugC(5, kDebugLevelResources, "discard view %d", viewNr);
|
||||
if (!(_game.dirView[viewNr].flags & RES_LOADED))
|
||||
return;
|
||||
|
||||
// Rebuild sprite list, see Sarien bug #779302
|
||||
_sprites->eraseSprites();
|
||||
|
||||
// free data
|
||||
for (int16 loopNr = 0; loopNr < viewData->loopCount; loopNr++) {
|
||||
AgiViewLoop *loopData = &viewData->loop[loopNr];
|
||||
for (int16 celNr = 0; celNr < loopData->celCount; celNr++) {
|
||||
AgiViewCel *celData = &loopData->cel[celNr];
|
||||
|
||||
delete[] celData->rawBitmap;
|
||||
}
|
||||
delete[] loopData->cel;
|
||||
}
|
||||
delete[] viewData->loop;
|
||||
delete[] viewData->description;
|
||||
|
||||
viewData->headerCycleTime = 0;
|
||||
viewData->headerStepSize = 0;
|
||||
viewData->description = nullptr;
|
||||
viewData->loop = nullptr;
|
||||
viewData->loopCount = 0;
|
||||
|
||||
// Mark this view as not loaded anymore
|
||||
_game.dirView[viewNr].flags &= ~RES_LOADED;
|
||||
|
||||
_sprites->buildAllSpriteLists();
|
||||
_sprites->drawAllSpriteLists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a view table entry to use the specified view resource.
|
||||
* @param screenObj pointer to screen object
|
||||
* @param viewNr number of AGI view resource
|
||||
*/
|
||||
void AgiEngine::setView(ScreenObjEntry *screenObj, int16 viewNr) {
|
||||
if (!(_game.dirView[viewNr].flags & RES_LOADED)) {
|
||||
// View resource currently not loaded, this is probably a game bug
|
||||
// Load the resource now to fix the issue, and give out a warning
|
||||
// This happens in at least Larry 1 for Apple IIgs right after getting beaten up by taxi driver
|
||||
// Original interpreter bombs out in this situation saying "view not loaded, Press ESC to quit"
|
||||
warning("setView() called on screen object %d to use view %d, but view not loaded", screenObj->objectNr, viewNr);
|
||||
warning("probably game script bug, trying to load view into memory");
|
||||
if (loadResource(RESOURCETYPE_VIEW, viewNr) != errOK) {
|
||||
// loading failed, we better error() out now
|
||||
error("setView() called to set view %d for screen object %d, which is not loaded atm and loading failed", viewNr, screenObj->objectNr);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
screenObj->viewResource = &_game.views[viewNr];
|
||||
screenObj->currentViewNr = viewNr;
|
||||
screenObj->loopCount = screenObj->viewResource->loopCount;
|
||||
screenObj->viewReplaced = true;
|
||||
|
||||
if (getVersion() < 0x2000) {
|
||||
screenObj->stepSize = screenObj->viewResource->headerStepSize;
|
||||
screenObj->cycleTime = screenObj->viewResource->headerCycleTime;
|
||||
screenObj->cycleTimeCount = 0;
|
||||
}
|
||||
if (screenObj->currentLoopNr >= screenObj->loopCount) {
|
||||
setLoop(screenObj, 0);
|
||||
} else {
|
||||
setLoop(screenObj, screenObj->currentLoopNr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a view table entry to use the specified loop of the current view.
|
||||
* @param screenObj pointer to screen object
|
||||
* @param loopNr number of loop
|
||||
*/
|
||||
void AgiEngine::setLoop(ScreenObjEntry *screenObj, int16 loopNr) {
|
||||
if (!(_game.dirView[screenObj->currentViewNr].flags & RES_LOADED)) {
|
||||
error("setLoop() called on screen object %d, which has no loaded view resource assigned to it", screenObj->objectNr);
|
||||
return;
|
||||
}
|
||||
assert(screenObj->viewResource);
|
||||
|
||||
if (screenObj->loopCount == 0) {
|
||||
warning("setLoop() called on screen object %d, which has no loops (view %d)", screenObj->objectNr, screenObj->currentViewNr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (loopNr >= screenObj->loopCount) {
|
||||
// requested loop not existent
|
||||
// instead of error()ing out, we instead clip it
|
||||
// At least required for possibly Manhunter 1 according to previous comment when leaving the arcade machine
|
||||
// TODO: Check MH1
|
||||
// WORKAROUND: This code caused an issue in KQ1 when bowing to the king in room 53. Bug #7045
|
||||
// When ego finishes bowing, the script sets his view to 0 and loop to 1 so that he faces left,
|
||||
// but it does this by setting the loop first and then the view. The previous view is 71 and only
|
||||
// has one loop. This code treated that as an invalid set.loop and would clip it to 0, but that
|
||||
// caused ego to face away from the king. For now, we detect this and set the view to 0 first.
|
||||
if (getGameID() == GID_KQ1 && screenObj->currentViewNr == 71 && loopNr == 1) {
|
||||
setView(screenObj, 0);
|
||||
} else {
|
||||
int16 requestedLoopNr = loopNr;
|
||||
|
||||
loopNr = screenObj->loopCount - 1;
|
||||
|
||||
warning("Non-existent loop requested for screen object %d", screenObj->objectNr);
|
||||
warning("view %d, requested loop %d -> clipped to loop %d", screenObj->currentViewNr, requestedLoopNr, loopNr);
|
||||
}
|
||||
}
|
||||
|
||||
AgiViewLoop *curViewLoop = &_game.views[screenObj->currentViewNr].loop[loopNr];
|
||||
|
||||
screenObj->currentLoopNr = loopNr;
|
||||
screenObj->loopData = curViewLoop;
|
||||
screenObj->celCount = curViewLoop->celCount;
|
||||
|
||||
if (screenObj->currentCelNr >= screenObj->celCount) {
|
||||
setCel(screenObj, 0);
|
||||
} else {
|
||||
setCel(screenObj, screenObj->currentCelNr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a view table entry to use the specified cel of the current loop.
|
||||
* @param screenObj pointer to screen object
|
||||
* @param celNr number of cel
|
||||
*/
|
||||
void AgiEngine::setCel(ScreenObjEntry *screenObj, int16 celNr) {
|
||||
if (!(_game.dirView[screenObj->currentViewNr].flags & RES_LOADED)) {
|
||||
error("setCel() called on screen object %d, which has no loaded view resource assigned to it", screenObj->objectNr);
|
||||
return;
|
||||
}
|
||||
assert(screenObj->viewResource);
|
||||
|
||||
if (screenObj->loopCount == 0) {
|
||||
warning("setLoop() called on screen object %d, which has no loops (view %d)", screenObj->objectNr, screenObj->currentViewNr);
|
||||
return;
|
||||
}
|
||||
|
||||
AgiViewLoop *curViewLoop = &_game.views[screenObj->currentViewNr].loop[screenObj->currentLoopNr];
|
||||
|
||||
// Added by Amit Vainsencher <amitv@subdimension.com> to prevent
|
||||
// crash in KQ1 -- not in the Sierra interpreter
|
||||
if (curViewLoop->celCount == 0) {
|
||||
warning("setCel() called on screen object %d, which has no cels (view %d)", screenObj->objectNr, screenObj->currentViewNr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (celNr >= screenObj->celCount) {
|
||||
// requested cel not existent
|
||||
// instead of error()ing out, we instead clip it
|
||||
// At least required for King's Quest 3 on Apple IIgs - walking the planks death cutscene
|
||||
// see bug #5832, which is a game bug!
|
||||
int16 requestedCelNr = celNr;
|
||||
|
||||
celNr = screenObj->celCount - 1;
|
||||
|
||||
warning("Non-existent cel requested for screen object %d", screenObj->objectNr);
|
||||
warning("view %d, loop %d, requested cel %d -> clipped to cel %d", screenObj->currentViewNr, screenObj->currentLoopNr, requestedCelNr, celNr);
|
||||
}
|
||||
|
||||
screenObj->currentCelNr = celNr;
|
||||
|
||||
AgiViewCel *curViewCel;
|
||||
curViewCel = &curViewLoop->cel[celNr];
|
||||
screenObj->celData = curViewCel;
|
||||
screenObj->xSize = curViewCel->width;
|
||||
screenObj->ySize = curViewCel->height;
|
||||
|
||||
// If position isn't appropriate, update it accordingly
|
||||
clipViewCoordinates(screenObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restrict view table entry's position so it stays wholly inside the screen.
|
||||
* Also take horizon into account when clipping if not set to ignore it.
|
||||
* @param v pointer to view table entry
|
||||
*/
|
||||
void AgiEngine::clipViewCoordinates(ScreenObjEntry *screenObj) {
|
||||
if (screenObj->xPos + screenObj->xSize > SCRIPT_WIDTH) {
|
||||
screenObj->flags |= fUpdatePos;
|
||||
screenObj->xPos = SCRIPT_WIDTH - screenObj->xSize;
|
||||
}
|
||||
if (screenObj->yPos - screenObj->ySize + 1 < 0) {
|
||||
screenObj->flags |= fUpdatePos;
|
||||
screenObj->yPos = screenObj->ySize - 1;
|
||||
}
|
||||
if (screenObj->yPos <= _game.horizon && (~screenObj->flags & fIgnoreHorizon)) {
|
||||
screenObj->flags |= fUpdatePos;
|
||||
screenObj->yPos = _game.horizon + 1;
|
||||
}
|
||||
|
||||
if (getVersion() < 0x2000) {
|
||||
screenObj->flags |= fDontUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view table entry as updating.
|
||||
* @param viewPtr pointer to view table entry
|
||||
*/
|
||||
void AgiEngine::startUpdate(ScreenObjEntry *viewPtr) {
|
||||
if (~viewPtr->flags & fUpdate) {
|
||||
_sprites->eraseSprites();
|
||||
viewPtr->flags |= fUpdate;
|
||||
_sprites->buildAllSpriteLists();
|
||||
_sprites->drawAllSpriteLists();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the view table entry as non-updating.
|
||||
* @param viewPtr pointer to view table entry
|
||||
*/
|
||||
void AgiEngine::stopUpdate(ScreenObjEntry *viewPtr) {
|
||||
if (viewPtr->flags & fUpdate) {
|
||||
_sprites->eraseSprites();
|
||||
viewPtr->flags &= ~fUpdate;
|
||||
_sprites->buildAllSpriteLists();
|
||||
_sprites->drawAllSpriteLists();
|
||||
}
|
||||
}
|
||||
|
||||
// loops to use according to direction and number of loops in
|
||||
// the view resource
|
||||
static const int loopTable2[] = {
|
||||
0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01, 0x01
|
||||
};
|
||||
|
||||
static const int loopTable4[] = {
|
||||
0x04, 0x03, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01, 0x01
|
||||
};
|
||||
|
||||
/**
|
||||
* Update view table entries.
|
||||
* This function is called at the end of each interpreter cycle
|
||||
* to update the view table entries and blit the sprites.
|
||||
*/
|
||||
void AgiEngine::updateScreenObjTable() {
|
||||
ScreenObjEntry *screenObj;
|
||||
int16 changeCount, loopNr;
|
||||
|
||||
changeCount = 0;
|
||||
for (screenObj = _game.screenObjTable; screenObj < &_game.screenObjTable[SCREENOBJECTS_MAX]; screenObj++) {
|
||||
if ((screenObj->flags & (fAnimated | fUpdate | fDrawn)) != (fAnimated | fUpdate | fDrawn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changeCount++;
|
||||
|
||||
loopNr = 4;
|
||||
if (!(screenObj->flags & fFixLoop)) {
|
||||
switch (screenObj->loopCount) {
|
||||
case 2:
|
||||
case 3:
|
||||
loopNr = loopTable2[screenObj->direction];
|
||||
break;
|
||||
case 4:
|
||||
loopNr = loopTable4[screenObj->direction];
|
||||
break;
|
||||
default:
|
||||
// for KQ4
|
||||
if (getVersion() == 0x3086 || getGameID() == GID_KQ4)
|
||||
loopNr = loopTable4[screenObj->direction];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// AGI 2.272 (ddp, xmas) doesn't test step_time_count!
|
||||
if (loopNr != 4 && loopNr != screenObj->currentLoopNr) {
|
||||
if (getVersion() <= 0x2272 || screenObj->stepTimeCount == 1) {
|
||||
setLoop(screenObj, loopNr);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenObj->flags & fCycling) {
|
||||
if (screenObj->cycleTimeCount) {
|
||||
screenObj->cycleTimeCount--;
|
||||
if (screenObj->cycleTimeCount == 0) {
|
||||
updateView(screenObj);
|
||||
screenObj->cycleTimeCount = screenObj->cycleTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changeCount) {
|
||||
_sprites->eraseRegularSprites();
|
||||
updatePosition();
|
||||
_sprites->buildRegularSpriteList();
|
||||
_sprites->drawRegularSpriteList();
|
||||
_sprites->showRegularSpriteList();
|
||||
|
||||
_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY].flags &= ~(fOnWater | fOnLand);
|
||||
}
|
||||
}
|
||||
|
||||
bool AgiEngine::isEgoView(const ScreenObjEntry *screenObj) {
|
||||
return screenObj == _game.screenObjTable;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
157
engines/agi/view.h
Normal file
157
engines/agi/view.h
Normal file
@@ -0,0 +1,157 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_VIEW_H
|
||||
#define AGI_VIEW_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
struct AgiViewCel {
|
||||
uint8 height;
|
||||
uint8 width;
|
||||
uint8 clearKey;
|
||||
bool mirrored;
|
||||
byte *rawBitmap;
|
||||
};
|
||||
|
||||
struct AgiViewLoop {
|
||||
int16 celCount;
|
||||
AgiViewCel *cel;
|
||||
};
|
||||
|
||||
/**
|
||||
* AGI view resource structure.
|
||||
*/
|
||||
struct AgiView {
|
||||
byte headerStepSize;
|
||||
byte headerCycleTime;
|
||||
byte *description;
|
||||
int16 loopCount;
|
||||
AgiViewLoop *loop;
|
||||
|
||||
void reset() {
|
||||
headerStepSize = 0;
|
||||
headerCycleTime = 0;
|
||||
description = nullptr;
|
||||
loopCount = 0;
|
||||
loop = nullptr;
|
||||
}
|
||||
|
||||
AgiView() { reset(); }
|
||||
};
|
||||
|
||||
enum MotionType {
|
||||
kMotionNormal = 0,
|
||||
kMotionWander = 1,
|
||||
kMotionFollowEgo = 2,
|
||||
kMotionMoveObj = 3,
|
||||
kMotionEgo = 4 // used by us for mouse movement only?
|
||||
};
|
||||
|
||||
enum CycleType {
|
||||
kCycleNormal = 0,
|
||||
kCycleEndOfLoop = 1,
|
||||
kCycleRevLoop = 2,
|
||||
kCycleReverse = 3
|
||||
};
|
||||
|
||||
enum ViewFlags {
|
||||
fDrawn = (1 << 0), // 0x0001
|
||||
fIgnoreBlocks = (1 << 1), // 0x0002
|
||||
fFixedPriority = (1 << 2), // 0x0004
|
||||
fIgnoreHorizon = (1 << 3), // 0x0008
|
||||
fUpdate = (1 << 4), // 0x0010
|
||||
fCycling = (1 << 5), // 0x0020
|
||||
fAnimated = (1 << 6), // 0x0040
|
||||
fMotion = (1 << 7), // 0x0080
|
||||
fOnWater = (1 << 8), // 0x0100
|
||||
fIgnoreObjects = (1 << 9), // 0x0200
|
||||
fUpdatePos = (1 << 10), // 0x0400
|
||||
fOnLand = (1 << 11), // 0x0800
|
||||
fDontUpdate = (1 << 12), // 0x1000
|
||||
fFixLoop = (1 << 13), // 0x2000
|
||||
fDidntMove = (1 << 14), // 0x4000
|
||||
fAdjEgoXY = (1 << 15) // 0x8000
|
||||
};
|
||||
|
||||
/**
|
||||
* AGI screen object table entry
|
||||
*/
|
||||
struct ScreenObjEntry {
|
||||
int16 objectNr; // 0-255 -> regular screenObjTable, -1 -> addToPic-view
|
||||
uint8 stepTime;
|
||||
uint8 stepTimeCount;
|
||||
int16 xPos;
|
||||
int16 yPos;
|
||||
uint8 currentViewNr;
|
||||
bool viewReplaced;
|
||||
struct AgiView *viewResource;
|
||||
uint8 currentLoopNr;
|
||||
uint8 loopCount;
|
||||
struct AgiViewLoop *loopData;
|
||||
uint8 currentCelNr;
|
||||
uint8 celCount;
|
||||
struct AgiViewCel *celData;
|
||||
//int16 xPos2;
|
||||
//int16 yPos2;
|
||||
int16 xSize;
|
||||
int16 ySize;
|
||||
|
||||
int16 xPos_prev;
|
||||
int16 yPos_prev;
|
||||
int16 xSize_prev;
|
||||
int16 ySize_prev;
|
||||
|
||||
uint8 stepSize;
|
||||
uint8 cycleTime;
|
||||
uint8 cycleTimeCount;
|
||||
uint8 direction;
|
||||
MotionType motionType;
|
||||
CycleType cycle;
|
||||
uint8 priority;
|
||||
uint16 flags;
|
||||
// kMotionMoveObj
|
||||
int16 move_x;
|
||||
int16 move_y;
|
||||
uint8 move_stepSize;
|
||||
uint8 move_flag;
|
||||
// kMotionFollowEgo
|
||||
uint8 follow_stepSize;
|
||||
uint8 follow_flag;
|
||||
uint8 follow_count;
|
||||
// kMotionWander
|
||||
uint8 wander_count;
|
||||
// end of motion related variables
|
||||
uint8 loop_flag;
|
||||
bool ignoreLoopFlag;
|
||||
|
||||
void reset() { memset(this, 0, sizeof(ScreenObjEntry)); }
|
||||
ScreenObjEntry() { reset(); }
|
||||
|
||||
void setLoopFlag(uint8 flag) {
|
||||
loop_flag = flag;
|
||||
ignoreLoopFlag = false;
|
||||
}
|
||||
}; // struct vt_entry
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_VIEW_H */
|
||||
255
engines/agi/wagparser.cpp
Normal file
255
engines/agi/wagparser.cpp
Normal file
@@ -0,0 +1,255 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/file.h"
|
||||
#include "common/util.h"
|
||||
#include "common/fs.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/textconsole.h"
|
||||
#include "common/formats/ini-file.h"
|
||||
|
||||
#include "agi/wagparser.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
WagProperty::WagProperty() {
|
||||
setDefaults();
|
||||
}
|
||||
|
||||
WagProperty::~WagProperty() {
|
||||
deleteData();
|
||||
}
|
||||
|
||||
WagProperty::WagProperty(const WagProperty &other) {
|
||||
deepCopy(other);
|
||||
}
|
||||
|
||||
WagProperty &WagProperty::operator=(const WagProperty &other) {
|
||||
if (&other != this) deepCopy(other); // Don't do self-assignment
|
||||
return *this;
|
||||
}
|
||||
|
||||
void WagProperty::deepCopy(const WagProperty &other) {
|
||||
_readOk = other._readOk;
|
||||
_propCode = other._propCode;
|
||||
_propType = other._propType;
|
||||
_propNum = other._propNum;
|
||||
_propSize = other._propSize;
|
||||
|
||||
if (other._propData != nullptr) {
|
||||
_propData = (char *)calloc(other._propSize + 1UL, 1); // Allocate space for property's data plus trailing zero
|
||||
memcpy(_propData, other._propData, other._propSize + 1UL); // Copy the whole thing
|
||||
} else {
|
||||
_propData = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool WagProperty::read(Common::SeekableReadStream &stream) {
|
||||
// First read the property's header
|
||||
_propCode = (enum WagPropertyCode)stream.readByte();
|
||||
_propType = (enum WagPropertyType)stream.readByte();
|
||||
_propNum = stream.readByte();
|
||||
_propSize = stream.readUint16LE();
|
||||
|
||||
if (stream.eos() || stream.err()) { // Check that we got the whole header
|
||||
_readOk = false;
|
||||
return _readOk;
|
||||
}
|
||||
|
||||
// Then read the property's data
|
||||
_propData = (char *)calloc(_propSize + 1UL, 1); // Allocate space for property's data plus trailing zero
|
||||
uint32 readBytes = stream.read(_propData, _propSize); // Read the data in
|
||||
_propData[_propSize] = 0; // Set the trailing zero for easy C-style string access
|
||||
|
||||
_readOk = (readBytes == _propSize); // Check that we got the whole data
|
||||
return _readOk;
|
||||
}
|
||||
|
||||
void WagProperty::clear() {
|
||||
deleteData();
|
||||
setDefaults();
|
||||
}
|
||||
|
||||
void WagProperty::setDefaults() {
|
||||
_readOk = false;
|
||||
_propCode = PC_UNDEFINED;
|
||||
_propType = PT_UNDEFINED;
|
||||
_propNum = 0;
|
||||
_propSize = 0;
|
||||
_propData = nullptr;
|
||||
}
|
||||
|
||||
void WagProperty::deleteData() {
|
||||
if (_propData)
|
||||
free(_propData);
|
||||
_propData = nullptr;
|
||||
}
|
||||
|
||||
WagFileParser::WagFileParser() :
|
||||
_parsedOk(false) {
|
||||
}
|
||||
|
||||
WagFileParser::~WagFileParser() {
|
||||
}
|
||||
|
||||
bool WagFileParser::checkAgiVersionProperty(const WagProperty &version) const {
|
||||
if (version.getCode() == WagProperty::PC_INTVERSION && // Must be AGI interpreter version property
|
||||
version.getSize() >= 3 && // Need at least three characters for a version number like "X.Y"
|
||||
Common::isDigit(version.getData()[0]) && // And the first character must be a digit
|
||||
(version.getData()[1] == ',' || version.getData()[1] == '.')) { // And the second a comma or a period
|
||||
|
||||
for (int i = 2; i < version.getSize(); i++) // And the rest must all be digits
|
||||
if (!Common::isDigit(version.getData()[i]))
|
||||
return false; // Bail out if found a non-digit after the decimal point
|
||||
|
||||
return true;
|
||||
} else // Didn't pass the preliminary test so fails
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16 WagFileParser::convertToAgiVersionNumber(const WagProperty &version) {
|
||||
// Examples of the conversion: "2.44" -> 0x2440, "2.917" -> 0x2917, "3.002086" -> 0x3086.
|
||||
if (checkAgiVersionProperty(version)) { // Check that the string is a valid AGI interpreter version string
|
||||
// Convert first ascii digit to an integer and put it in the fourth nibble (Bits 12...15) of the version number
|
||||
// and at the same time set all other nibbles to zero.
|
||||
uint16 agiVerNum = ((uint16)(version.getData()[0] - '0')) << (3 * 4);
|
||||
|
||||
// Convert at most three least significant digits of the version number's minor part
|
||||
// (i.e. the part after the decimal point) and put them in order to the third, second
|
||||
// and the first nibble of the version number. Just to clarify version.getSize() - 2
|
||||
// is the number of digits after the decimal point.
|
||||
int32 digitCount = MIN<int32>(3, ((int32) version.getSize()) - 2); // How many digits left to convert
|
||||
for (int i = 0; i < digitCount; i++)
|
||||
agiVerNum |= ((uint16)(version.getData()[version.getSize() - digitCount + i] - '0')) << ((2 - i) * 4);
|
||||
|
||||
debug(3, "WagFileParser: Converted AGI version from string %s to number 0x%x", version.getData(), agiVerNum);
|
||||
return agiVerNum;
|
||||
} else // Not a valid AGI interpreter version string
|
||||
return 0; // Can't convert, so failure
|
||||
}
|
||||
|
||||
bool WagFileParser::checkWagVersion(Common::SeekableReadStream &stream) {
|
||||
if (stream.size() >= WINAGI_VERSION_LENGTH) { // Stream has space to contain the WinAGI version string
|
||||
// Read the last WINAGI_VERSION_LENGTH bytes of the stream and make a string out of it
|
||||
char str[WINAGI_VERSION_LENGTH + 1]; // Allocate space for the trailing zero also
|
||||
uint32 oldStreamPos = stream.pos(); // Save the old stream position
|
||||
stream.seek(stream.size() - WINAGI_VERSION_LENGTH);
|
||||
uint32 readBytes = stream.read(str, WINAGI_VERSION_LENGTH);
|
||||
stream.seek(oldStreamPos); // Seek back to the old stream position
|
||||
str[readBytes] = 0; // Set the trailing zero to finish the C-style string
|
||||
if (readBytes != WINAGI_VERSION_LENGTH) { // Check that we got the whole version string
|
||||
debug(3, "WagFileParser::checkWagVersion: Error reading WAG file version from stream");
|
||||
return false;
|
||||
}
|
||||
debug(3, "WagFileParser::checkWagVersion: Read WinAGI version string (\"%s\")", str);
|
||||
|
||||
// Check that the WinAGI version string is one of the two version strings
|
||||
// WinAGI 1.1.21 recognizes as acceptable in the end of a *.wag file.
|
||||
// Note that they are all of length 16 and are padded with spaces to be that long.
|
||||
return scumm_stricmp(str, "WINAGI v1.0 ") == 0 ||
|
||||
scumm_stricmp(str, "1.0 BETA ") == 0;
|
||||
} else { // Stream is too small to contain the WinAGI version string
|
||||
debug(3, "WagFileParser::checkWagVersion: Stream is too small to contain a valid WAG file");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void WagFileParser::addPropFromIni(Common::INIFile &iniWagFile, Common::String section, Common::String key, Agi::WagProperty::WagPropertyCode code) {
|
||||
WagProperty property;
|
||||
property.setPropCode(code);
|
||||
Common::String value;
|
||||
if (iniWagFile.getKey(key, section, value)) {
|
||||
property.setPropDataSize(value);
|
||||
_propList.push_back(property);
|
||||
}
|
||||
}
|
||||
|
||||
bool WagFileParser::parse(const Common::FSNode &node) {
|
||||
WagProperty property; // Temporary property used for reading
|
||||
Common::SeekableReadStream *stream = nullptr; // The file stream
|
||||
|
||||
_parsedOk = false; // We haven't parsed the file yet
|
||||
|
||||
stream = node.createReadStream(); // Open the file
|
||||
if (stream) { // Check that opening the file was successful
|
||||
if (checkWagVersion(*stream)) { // Check that WinAGI version string is valid
|
||||
// It seems we've got a valid *.wag file so let's parse its properties from the start.
|
||||
stream->seek(0); // Rewind the stream
|
||||
if (!_propList.empty()) _propList.clear(); // Clear out old properties (If any)
|
||||
|
||||
do { // Parse the properties
|
||||
if (property.read(*stream)) { // Read the property and check it was read ok
|
||||
_propList.push_back(property); // Add read property to properties list
|
||||
debug(4, "WagFileParser::parse: Read property with code %d, type %d, number %d, size %d, data \"%s\"",
|
||||
property.getCode(), property.getType(), property.getNumber(), property.getSize(), property.getData());
|
||||
} else // Reading failed, let's bail out
|
||||
break;
|
||||
} while (!endOfProperties(*stream)); // Loop until the end of properties
|
||||
|
||||
// File was parsed successfully only if we got to the end of properties
|
||||
// and all the properties were read successfully (Also the last).
|
||||
_parsedOk = endOfProperties(*stream) && property.readOk();
|
||||
|
||||
if (!_parsedOk) // Error parsing stream
|
||||
warning("Error parsing WAG file (%s). WAG file ignored", node.getPath().toString(Common::Path::kNativeSeparator).c_str());
|
||||
} else {
|
||||
// Invalid WinAGI version string or it couldn't be read
|
||||
// Let's try to read WAG file as newer INI format
|
||||
Common::INIFile iniWagFile;
|
||||
_parsedOk = iniWagFile.loadFromStream(*stream);
|
||||
if (_parsedOk) {
|
||||
addPropFromIni(iniWagFile, "General", "Interpreter", WagProperty::PC_INTVERSION);
|
||||
addPropFromIni(iniWagFile, "General", "GameID", WagProperty::PC_GAMEID);
|
||||
addPropFromIni(iniWagFile, "General", "Description", WagProperty::PC_GAMEDESC);
|
||||
addPropFromIni(iniWagFile, "General", "GameVersion", WagProperty::PC_GAMEVERSION);
|
||||
addPropFromIni(iniWagFile, "General", "LastEdit", WagProperty::PC_GAMELAST);
|
||||
} else
|
||||
warning("Invalid WAG file (%s) version or error reading it. WAG file ignored", node.getPath().toString(Common::Path::kNativeSeparator).c_str());
|
||||
}
|
||||
|
||||
} else // Couldn't open file
|
||||
warning("Couldn't open WAG file (%s). WAG file ignored", node.getPath().toString(Common::Path::kNativeSeparator).c_str());
|
||||
|
||||
delete stream;
|
||||
return _parsedOk;
|
||||
}
|
||||
|
||||
const WagProperty *WagFileParser::getProperty(const WagProperty::WagPropertyCode code) const {
|
||||
for (PropertyList::const_iterator iter = _propList.begin(); iter != _propList.end(); ++iter)
|
||||
if (iter->getCode() == code) return iter;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool WagFileParser::endOfProperties(const Common::SeekableReadStream &stream) const {
|
||||
return stream.pos() >= (stream.size() - WINAGI_VERSION_LENGTH);
|
||||
}
|
||||
|
||||
|
||||
void WagProperty::setPropCode(WagPropertyCode propCode) {
|
||||
_propCode = propCode;
|
||||
}
|
||||
|
||||
void WagProperty::setPropDataSize(Common::String str) {
|
||||
_propData = scumm_strdup(str.c_str());
|
||||
_propSize = str.size();
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
307
engines/agi/wagparser.h
Normal file
307
engines/agi/wagparser.h
Normal file
@@ -0,0 +1,307 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Agi {
|
||||
|
||||
/**
|
||||
* WagProperty represents a single property from WinAGI's *.wag file.
|
||||
* A property consists of a header and of data.
|
||||
* The header consists of the following:
|
||||
* - Property code (Integer/Enumeration, 1 byte)
|
||||
* - Property type (Integer/Enumeration, 1 byte)
|
||||
* - Property number (Integer, 1 byte)
|
||||
* - Property size (Little endian integer, 2 bytes)
|
||||
* And then there's the data with as many bytes as defined in the header's property size variable.
|
||||
*/
|
||||
class WagProperty {
|
||||
// Constants, enumerations etc
|
||||
public:
|
||||
/**
|
||||
* Property codes taken from WinAGI 1.1.21's source code file WinAGI/AGIObjects.bas.
|
||||
*/
|
||||
enum WagPropertyCode {
|
||||
PC_GAMEDESC = 129, ///< Game description (WinAGI 1.1.21 limits these to 4096 bytes)
|
||||
PC_GAMEAUTHOR, ///< Game author (WinAGI 1.1.21 limits these to 256 bytes)
|
||||
PC_GAMEID, ///< Game ID
|
||||
PC_INTVERSION, ///< Interpreter version (WinAGI 1.1.21 defaults to version 2.917)
|
||||
PC_GAMELAST, ///< Last edit date
|
||||
PC_GAMEVERSION, ///< Game version (WinAGI 1.1.21 limits these to 256 bytes)
|
||||
PC_GAMEABOUT, ///< About game (WinAGI 1.1.21 limits these to 4096 bytes)
|
||||
PC_GAMEEXEC, ///< Game executable
|
||||
PC_RESDIR, ///< Resource directory name
|
||||
PC_DEFSYNTAX, ///< Default syntax
|
||||
PC_INVOBJDESC = 144,
|
||||
PC_VOCABWORDDESC = 160,
|
||||
PC_PALETTE = 172,
|
||||
PC_USERESNAMES = 180,
|
||||
PC_LOGIC = 192,
|
||||
PC_PICTURE = 208,
|
||||
PC_SOUND = 224,
|
||||
PC_VIEW = 240,
|
||||
PC_UNDEFINED = 0x100 ///< An undefined property code (Added for ScummVM).
|
||||
};
|
||||
|
||||
/**
|
||||
* Property types taken from WinAGI 1.1.21's source code file WinAGI/AGIObjects.bas.
|
||||
* At the moment these aren't really at all needed by ScummVM. Just here if anyone decides to use them.
|
||||
*/
|
||||
enum WagPropertyType {
|
||||
PT_ID,
|
||||
PT_DESC,
|
||||
PT_SYNTAX,
|
||||
PT_CRC32,
|
||||
PT_KEY,
|
||||
PT_INST0,
|
||||
PT_INST1,
|
||||
PT_INST2,
|
||||
PT_MUTE0,
|
||||
PT_MUTE1,
|
||||
PT_MUTE2,
|
||||
PT_MUTE3,
|
||||
PT_TPQN,
|
||||
PT_ROOM,
|
||||
PT_VIS0,
|
||||
PT_VIS1,
|
||||
PT_VIS2,
|
||||
PT_VIS3,
|
||||
PT_ALL = 0xff,
|
||||
PT_UNDEFINED = 0x100 ///< An undefined property type (Added for ScummVM).
|
||||
};
|
||||
|
||||
// Constructors, destructors, operators etc
|
||||
public:
|
||||
/**
|
||||
* Creates an empty WagProperty object.
|
||||
* No property header or property data in it.
|
||||
*/
|
||||
WagProperty();
|
||||
|
||||
/**
|
||||
* Destructor. Releases allocated memory if any etc. The usual.
|
||||
*/
|
||||
~WagProperty();
|
||||
|
||||
/**
|
||||
* Copy constructor. Deep copies the variables.
|
||||
*/
|
||||
WagProperty(const WagProperty &other);
|
||||
|
||||
/**
|
||||
* Assignment operator. Deep copies the variables.
|
||||
*/
|
||||
WagProperty &operator=(const WagProperty &other);
|
||||
|
||||
// Non-public helper methods
|
||||
protected:
|
||||
/**
|
||||
* Sets the default values for member variables.
|
||||
*/
|
||||
void setDefaults();
|
||||
|
||||
/**
|
||||
* Delete's the property's data from memory if we have it, otherwise does nothing.
|
||||
*/
|
||||
void deleteData();
|
||||
|
||||
/**
|
||||
* Deep copies the parameter object to this object.
|
||||
* @param other The object to be deep copied to this object.
|
||||
*/
|
||||
void deepCopy(const WagProperty &other);
|
||||
|
||||
// Public methods that have side-effects
|
||||
public:
|
||||
/**
|
||||
* Read in a property (Header and data).
|
||||
* @return True if reading was a success, false otherwise.
|
||||
*/
|
||||
bool read(Common::SeekableReadStream &stream);
|
||||
|
||||
/**
|
||||
* Clears the property.
|
||||
* After this the property is empty. No header or data.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
// Public access functions
|
||||
public:
|
||||
/**
|
||||
* Was the property read ok from the source stream?
|
||||
*/
|
||||
bool readOk() const { return _readOk; }
|
||||
|
||||
/**
|
||||
* Return the property's code.
|
||||
* @return The property's code if readOk(), PC_UNDEFINED otherwise.
|
||||
*/
|
||||
enum WagPropertyCode getCode() const { return _propCode; }
|
||||
|
||||
/**
|
||||
* Return the property's type.
|
||||
* @return The property's type if readOk(), PT_UNDEFINED otherwise.
|
||||
*/
|
||||
enum WagPropertyType getType() const { return _propType; }
|
||||
|
||||
/**
|
||||
* Return the property's number.
|
||||
* @return The property's number if readOk(), 0 otherwise.
|
||||
*/
|
||||
byte getNumber() const { return _propNum; }
|
||||
|
||||
/**
|
||||
* Return the property's data's length.
|
||||
* @return The property's data's length if readOk(), 0 otherwise.
|
||||
*/
|
||||
uint16 getSize() const { return _propSize; }
|
||||
|
||||
/**
|
||||
* Return property's data. Constant access version.
|
||||
* Can be used as a C-style string (i.e. this is guaranteed to have a trailing zero).
|
||||
* @return The property's data if readOk(), NULL otherwise.
|
||||
*/
|
||||
const char *getData() const { return _propData; }
|
||||
|
||||
/**
|
||||
* Set property's code
|
||||
* @param propCode the code value to set
|
||||
*/
|
||||
void setPropCode(WagPropertyCode propCode);
|
||||
|
||||
/**
|
||||
* Set property's data and property's size
|
||||
* @param str the string that according to it these are set
|
||||
*/
|
||||
void setPropDataSize(Common::String str);
|
||||
|
||||
// Member variables
|
||||
protected:
|
||||
bool _readOk; ///< Was the property read ok from the source stream?
|
||||
enum WagPropertyCode _propCode; ///< Property code (Part of the property's header)
|
||||
enum WagPropertyType _propType; ///< Property type (Part of the property's header)
|
||||
byte _propNum; ///< Property number (Part of the property's header)
|
||||
uint16 _propSize; ///< Property's size (Part of the property's header)
|
||||
char *_propData; ///< The property's data (Plus a trailing zero for C-style string access)
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Class for parsing *.wag files created by WinAGI.
|
||||
* Using this class you can get information about fanmade AGI games if they have provided a *.wag file with them.
|
||||
*/
|
||||
class WagFileParser {
|
||||
// Constants, type definitions, enumerations etc.
|
||||
public:
|
||||
enum {
|
||||
WINAGI_VERSION_LENGTH = 16 ///< WinAGI's version string's length (Always 16)
|
||||
};
|
||||
typedef Common::Array<WagProperty> PropertyList; ///< A type definition for an array of *.wag file properties
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructor. Creates a WagFileParser object in a default state.
|
||||
*/
|
||||
WagFileParser();
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
~WagFileParser();
|
||||
|
||||
/**
|
||||
* Add property to _propList from INI file, if key exist
|
||||
* @param iniWagFile ini loaded from WAG file stream
|
||||
* @param section ini section
|
||||
* @param key ini key
|
||||
* @param code corresponding WAG code
|
||||
*/
|
||||
void addPropFromIni(Common::INIFile &iniWagFile, Common::String section, Common::String key, Agi::WagProperty::WagPropertyCode code);
|
||||
|
||||
/**
|
||||
* Loads a *.wag file and parses it.
|
||||
* @note After this you can access the loaded properties using getProperty() and getProperties() etc.
|
||||
* @param filename Name of the file to be parsed.
|
||||
* @return True if parsed successfully, false otherwise.
|
||||
*/
|
||||
bool parse(const Common::FSNode &node);
|
||||
|
||||
/**
|
||||
* Get list of the loaded properties.
|
||||
* @note Use only after a call to parse() first.
|
||||
* @return The list of loaded properties.
|
||||
*/
|
||||
const PropertyList &getProperties() const { return _propList; }
|
||||
|
||||
/**
|
||||
* Get property with the given property code.
|
||||
* @note Use only after a call to parse() first.
|
||||
* @return Pointer to the property if its found in memory, NULL otherwise.
|
||||
*
|
||||
* TODO/FIXME: Handle cases where several properties with the given property code are found.
|
||||
* At the moment we don't need this functionality because the properties we use
|
||||
* for fallback detection probably don't have multiples in the WAG-file.
|
||||
* TODO: Make this faster than linear time if desired/needed.
|
||||
*/
|
||||
const WagProperty *getProperty(const WagProperty::WagPropertyCode code) const;
|
||||
|
||||
/**
|
||||
* Tests if the given property contains a valid AGI interpreter version string.
|
||||
* A valid AGI interpreter version string is of the form "X.Y" or "X,Y" where
|
||||
* X is a single decimal digit and Y is a string of decimal digits (At least one digit).
|
||||
* @param version The property to be tested.
|
||||
* @return True if the given property contains a valid AGI interpreter version string, false otherwise.
|
||||
*/
|
||||
bool checkAgiVersionProperty(const WagProperty &version) const;
|
||||
|
||||
/**
|
||||
* Convert property's data to an AGI interpreter version number.
|
||||
* @param version The property to be converted (Property code should be PC_INTVERSION).
|
||||
* @return AGI interpreter version number if successful, 0 otherwise.
|
||||
*/
|
||||
uint16 convertToAgiVersionNumber(const WagProperty &version);
|
||||
|
||||
/**
|
||||
* Was the file parsed successfully?
|
||||
* @return True if file was parsed successfully, false otherwise.
|
||||
*/
|
||||
bool parsedOk() const { return _parsedOk; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Checks if stream has a valid WinAGI version string in its end.
|
||||
* @param stream The stream to be checked.
|
||||
* @return True if reading was successful and stream contains a valid WinAGI version string, false otherwise.
|
||||
*/
|
||||
bool checkWagVersion(Common::SeekableReadStream &stream);
|
||||
|
||||
/**
|
||||
* Checks if we're at or past the end of the properties stored in the stream.
|
||||
* @param stream The stream whose seeking position is to be checked.
|
||||
* @return True if stream's seeking position is at or past the end of the properties, false otherwise.
|
||||
*/
|
||||
bool endOfProperties(const Common::SeekableReadStream &stream) const;
|
||||
|
||||
// Member variables
|
||||
protected:
|
||||
PropertyList _propList; ///< List of loaded properties from the file.
|
||||
bool _parsedOk; ///< Did the parsing of the file go ok?
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
462
engines/agi/words.cpp
Normal file
462
engines/agi/words.cpp
Normal file
@@ -0,0 +1,462 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "agi/agi.h"
|
||||
#include "agi/words.h"
|
||||
|
||||
#include "common/textconsole.h"
|
||||
|
||||
namespace Agi {
|
||||
|
||||
Words::Words(AgiEngine *vm) :
|
||||
_vm(vm),
|
||||
_hasExtendedCharacters(false) {
|
||||
|
||||
clearEgoWords();
|
||||
}
|
||||
|
||||
Words::~Words() {
|
||||
clearEgoWords();
|
||||
}
|
||||
|
||||
int Words::loadDictionary_v1(Common::SeekableReadStream &stream) {
|
||||
char str[64];
|
||||
int k;
|
||||
|
||||
debug(0, "Loading dictionary");
|
||||
|
||||
// Loop through alphabet, as words in the dictionary file are sorted by
|
||||
// first character
|
||||
stream.seek(26 * 2, SEEK_CUR);
|
||||
do {
|
||||
// Read next word
|
||||
for (k = 0; k < ARRAYSIZE(str) - 1; k++) {
|
||||
str[k] = stream.readByte();
|
||||
if (str[k] == 0 || (uint8)str[k] == 0xFF)
|
||||
break;
|
||||
}
|
||||
|
||||
// Store word in dictionary
|
||||
if (k > 0) {
|
||||
WordEntry newWord;
|
||||
newWord.word = Common::String(str, k + 1);
|
||||
newWord.id = stream.readUint16LE();
|
||||
_dictionary[str[0]].push_back(newWord);
|
||||
}
|
||||
} while ((uint8)str[0] != 0xFF);
|
||||
|
||||
return errOK;
|
||||
}
|
||||
|
||||
int Words::loadDictionary(const char *fname) {
|
||||
Common::File fp;
|
||||
if (!fp.open(fname)) {
|
||||
warning("loadDictionary: can't open %s", fname);
|
||||
// FIXME
|
||||
return errOK; // err_BadFileOpen
|
||||
}
|
||||
|
||||
debug(0, "Loading dictionary: %s", fname);
|
||||
return loadDictionary(fp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all words from WORDS.TOK into the dictionary.
|
||||
*
|
||||
* Note that this parser handles words that start with a digit. These appear in
|
||||
* fan games because AGI Studio allowed them and placed them at the start of the
|
||||
* 'A' section. These words had no effect because the interpreter only matched
|
||||
* user input that began with A-Z, and the matching logic happened to skip words
|
||||
* until it reached one with the expected first letter. In the past, these words
|
||||
* caused problems for our parser. See bugs #6415, #15000
|
||||
*/
|
||||
int Words::loadDictionary(Common::SeekableReadStream &stream) {
|
||||
// Read words for each letter (A-Z)
|
||||
const uint32 start = stream.pos();
|
||||
for (int i = 0; i < 26; i++) {
|
||||
// Read letter index and seek to first word
|
||||
stream.seek(start + i * 2);
|
||||
int offset = stream.readUint16BE();
|
||||
if (offset == 0) {
|
||||
continue; // no words
|
||||
}
|
||||
stream.seek(start + offset);
|
||||
|
||||
// Read all words in this letter's section
|
||||
char str[64] = { 0 };
|
||||
int prevWordLength = 0;
|
||||
int k = stream.readByte(); // copy-count of first word
|
||||
while (!stream.eos() && !stream.err() && k <= prevWordLength) {
|
||||
// Read word
|
||||
char c = 0;
|
||||
while (!(c & 0x80) && k < ARRAYSIZE(str) - 1) {
|
||||
c = stream.readByte();
|
||||
str[k++] = (c ^ 0x7F) & 0x7F;
|
||||
}
|
||||
str[k] = 0;
|
||||
|
||||
// Read word id
|
||||
uint16 wordId = stream.readUint16BE();
|
||||
if (stream.eos() || stream.err()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Store word in dictionary
|
||||
WordEntry newWord;
|
||||
newWord.word = Common::String(str, k);
|
||||
newWord.id = wordId;
|
||||
_dictionary[str[0]].push_back(newWord);
|
||||
|
||||
// Read next word's copy count, or this letter's zero terminator.
|
||||
// Stop on zero if the word we read begins with the expected letter,
|
||||
// otherwise this is a fan game and we just read a word that starts
|
||||
// with a digit at the start of the 'A' section. Bugs #6413, #15000
|
||||
k = stream.readByte();
|
||||
if (k == 0 && str[0] == 'a' + i) {
|
||||
break;
|
||||
}
|
||||
prevWordLength = k;
|
||||
}
|
||||
}
|
||||
|
||||
return errOK;
|
||||
}
|
||||
|
||||
int Words::loadExtendedDictionary(const char *fname) {
|
||||
Common::File fp;
|
||||
if (!fp.open(fname)) {
|
||||
warning("loadWords: can't open %s", fname);
|
||||
// FIXME
|
||||
return errOK; // err_BadFileOpen
|
||||
}
|
||||
debug(0, "Loading extended dictionary: %s", fname);
|
||||
|
||||
// skip the header
|
||||
fp.readString('\n');
|
||||
|
||||
while (!fp.eos() && !fp.err()) {
|
||||
WordEntry newWord;
|
||||
newWord.word = fp.readString();
|
||||
newWord.id = atoi(fp.readString('\n').c_str());
|
||||
if (!newWord.word.empty()) {
|
||||
_dictionary[(byte)newWord.word[0]].push_back(newWord);
|
||||
}
|
||||
}
|
||||
|
||||
_hasExtendedCharacters = true;
|
||||
return errOK;
|
||||
}
|
||||
|
||||
void Words::unloadDictionary() {
|
||||
_dictionary.clear();
|
||||
}
|
||||
|
||||
void Words::clearEgoWords() {
|
||||
for (int16 wordNr = 0; wordNr < MAX_WORDS; wordNr++) {
|
||||
_egoWords[wordNr].id = 0;
|
||||
_egoWords[wordNr].word.clear();
|
||||
}
|
||||
_egoWordCount = 0;
|
||||
}
|
||||
|
||||
|
||||
static bool isCharSeparator(const char curChar) {
|
||||
switch (curChar) {
|
||||
case ' ':
|
||||
case ',':
|
||||
case '.':
|
||||
case '?':
|
||||
case '!':
|
||||
case '(':
|
||||
case ')':
|
||||
case ';':
|
||||
case ':':
|
||||
case '[':
|
||||
case ']':
|
||||
case '{':
|
||||
case '}':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool isCharInvalid(const char curChar) {
|
||||
switch (curChar) {
|
||||
case 0x27: // '
|
||||
case 0x60: // `
|
||||
case '-':
|
||||
case '\\':
|
||||
case '"':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Words::cleanUpInput(const char *rawUserInput, Common::String &cleanInput) {
|
||||
cleanInput.clear();
|
||||
|
||||
byte curChar = *rawUserInput;
|
||||
while (curChar) {
|
||||
// skip separators / invalid characters
|
||||
if (isCharSeparator(curChar) || isCharInvalid(curChar)) {
|
||||
rawUserInput++;
|
||||
curChar = *rawUserInput;
|
||||
} else {
|
||||
do {
|
||||
if (!isCharInvalid(curChar)) {
|
||||
// not invalid char, add it to the cleaned up input
|
||||
cleanInput += curChar;
|
||||
}
|
||||
|
||||
rawUserInput++;
|
||||
curChar = *rawUserInput;
|
||||
|
||||
if (isCharSeparator(curChar)) {
|
||||
cleanInput += ' ';
|
||||
break;
|
||||
}
|
||||
} while (curChar);
|
||||
}
|
||||
}
|
||||
if (cleanInput.hasSuffix(" ")) {
|
||||
// ends with a space? remove it
|
||||
cleanInput.deleteLastChar();
|
||||
}
|
||||
}
|
||||
|
||||
int16 Words::findWordInDictionary(const Common::String &userInputLowercase, uint16 userInputLen, uint16 userInputPos, uint16 &foundWordLen) {
|
||||
uint16 userInputLeft = userInputLen - userInputPos;
|
||||
uint16 wordStartPos = userInputPos;
|
||||
int16 wordId = DICTIONARY_RESULT_UNKNOWN;
|
||||
byte firstChar = userInputLowercase[userInputPos];
|
||||
|
||||
foundWordLen = 0;
|
||||
|
||||
const byte lastCharInAbc = _hasExtendedCharacters ? 0xff : 'z';
|
||||
|
||||
// Words normally have to start with a letter.
|
||||
// ENHANCEMENT: Fan games and translations include words that start with a
|
||||
// digit, even though the original interpreter ignored them. We allow input
|
||||
// words to start with a digit if the dictionary contains such a word.
|
||||
if (('a' <= firstChar && firstChar <= lastCharInAbc) ||
|
||||
('0' <= firstChar && firstChar <= '9' && _dictionary.contains(firstChar))) {
|
||||
if (((userInputPos + 1) < userInputLen) && (userInputLowercase[userInputPos + 1] == ' ')) {
|
||||
// current word is 1 char only?
|
||||
if ((firstChar == 'a') || (firstChar == 'i')) {
|
||||
// and it's "a" or "i"? -> then set current type to ignore
|
||||
wordId = DICTIONARY_RESULT_IGNORE;
|
||||
}
|
||||
}
|
||||
|
||||
const Common::Array<WordEntry> &words = _dictionary.getValOrDefault(firstChar);
|
||||
for (const WordEntry &wordEntry : words) {
|
||||
uint16 dictionaryWordLen = wordEntry.word.size();
|
||||
|
||||
if (dictionaryWordLen <= userInputLeft) {
|
||||
// dictionary word is longer or same length as the remaining user input
|
||||
uint16 curCompareLeft = dictionaryWordLen;
|
||||
uint16 dictionaryWordPos = 0;
|
||||
|
||||
userInputPos = wordStartPos;
|
||||
while (curCompareLeft) {
|
||||
byte curUserInputChar = userInputLowercase[userInputPos];
|
||||
byte curDictionaryChar = wordEntry.word[dictionaryWordPos];
|
||||
|
||||
if (curUserInputChar != curDictionaryChar)
|
||||
break;
|
||||
|
||||
userInputPos++;
|
||||
dictionaryWordPos++;
|
||||
curCompareLeft--;
|
||||
}
|
||||
|
||||
if (!curCompareLeft) {
|
||||
// check, if there is also nothing more of user input left or if a space the follow-up char?
|
||||
if ((userInputPos >= userInputLen) || (userInputLowercase[userInputPos] == ' ')) {
|
||||
// so fully matched, remember match
|
||||
wordId = wordEntry.id;
|
||||
foundWordLen = dictionaryWordLen;
|
||||
|
||||
// perfect match? -> exit loop
|
||||
if (userInputLeft == foundWordLen) {
|
||||
// perfect match -> break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWordLen == 0) {
|
||||
userInputPos = wordStartPos;
|
||||
while (userInputPos < userInputLen) {
|
||||
if (userInputLowercase[userInputPos] == ' ') {
|
||||
break;
|
||||
}
|
||||
userInputPos++;
|
||||
}
|
||||
foundWordLen = userInputPos - wordStartPos;
|
||||
}
|
||||
return wordId;
|
||||
}
|
||||
|
||||
void Words::parseUsingDictionary(const char *rawUserInput) {
|
||||
assert(rawUserInput);
|
||||
debugC(2, kDebugLevelScripts, "parse: userinput = \"%s\"", rawUserInput);
|
||||
|
||||
// Reset result
|
||||
clearEgoWords();
|
||||
|
||||
// clean up user input
|
||||
Common::String userInput;
|
||||
cleanUpInput(rawUserInput, userInput);
|
||||
|
||||
// Sierra compared independent of upper case and lower case
|
||||
Common::String userInputLowercase = userInput;
|
||||
userInputLowercase.toLowercase();
|
||||
|
||||
if (_vm->getLanguage() == Common::RU_RUS) {
|
||||
convertRussianUserInput(userInputLowercase);
|
||||
}
|
||||
|
||||
if (handleSpeedCommands(userInputLowercase)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16 wordCount = 0;
|
||||
uint16 userInputPos = 0;
|
||||
uint16 userInputLen = userInput.size();
|
||||
const char *userInputPtr = userInput.c_str();
|
||||
while (userInputPos < userInputLen) {
|
||||
// Skip trailing space
|
||||
if (userInput[userInputPos] == ' ')
|
||||
userInputPos++;
|
||||
|
||||
uint16 foundWordPos = userInputPos;
|
||||
uint16 foundWordLen = 0;
|
||||
int16 foundWordId = findWordInDictionary(userInputLowercase, userInputLen, userInputPos, foundWordLen);
|
||||
|
||||
if (foundWordId != DICTIONARY_RESULT_IGNORE) {
|
||||
// word not supposed to get ignored
|
||||
// add it now
|
||||
if (foundWordId != DICTIONARY_RESULT_UNKNOWN) {
|
||||
// known word
|
||||
_egoWords[wordCount].id = foundWordId;
|
||||
}
|
||||
|
||||
_egoWords[wordCount].word = Common::String(userInputPtr + foundWordPos, foundWordLen);
|
||||
debugC(2, kDebugLevelScripts, "found word %s (id %d)", _egoWords[wordCount].word.c_str(), _egoWords[wordCount].id);
|
||||
wordCount++;
|
||||
|
||||
if (foundWordId == DICTIONARY_RESULT_UNKNOWN) {
|
||||
// unknown word
|
||||
_vm->setVar(VM_VAR_WORD_NOT_FOUND, wordCount);
|
||||
break; // and exit now
|
||||
}
|
||||
}
|
||||
|
||||
userInputPos += foundWordLen;
|
||||
}
|
||||
|
||||
_egoWordCount = wordCount;
|
||||
|
||||
debugC(4, kDebugLevelScripts, "ego word count = %d", _egoWordCount);
|
||||
if (_egoWordCount > 0) {
|
||||
_vm->setFlag(VM_FLAG_ENTERED_CLI, true);
|
||||
} else {
|
||||
_vm->setFlag(VM_FLAG_ENTERED_CLI, false);
|
||||
}
|
||||
_vm->setFlag(VM_FLAG_SAID_ACCEPTED_INPUT, false);
|
||||
}
|
||||
|
||||
uint16 Words::getEgoWordCount() const {
|
||||
return _egoWordCount;
|
||||
}
|
||||
|
||||
const char *Words::getEgoWord(int16 wordNr) const {
|
||||
assert(wordNr >= 0 && wordNr < MAX_WORDS);
|
||||
return _egoWords[wordNr].word.c_str();
|
||||
}
|
||||
|
||||
uint16 Words::getEgoWordId(int16 wordNr) const {
|
||||
assert(wordNr >= 0 && wordNr < MAX_WORDS);
|
||||
return _egoWords[wordNr].id;
|
||||
}
|
||||
|
||||
bool Words::handleSpeedCommands(const Common::String &userInputLowercase) {
|
||||
// We add speed controls to games that didn't originally have them.
|
||||
// Apple II games had no speed controls, the interpreter ran as fast as it could.
|
||||
// Some Apple IIgs games had speed controls, others didn't. We override the
|
||||
// the speed that the game requests with `timeDelayOverwrite`.
|
||||
switch (_vm->getPlatform()) {
|
||||
case Common::kPlatformApple2:
|
||||
case Common::kPlatformApple2GS:
|
||||
if (userInputLowercase == "fastest") {
|
||||
_vm->_game.setSpeedLevel(0);
|
||||
return true;
|
||||
} else if (userInputLowercase == "fast") {
|
||||
_vm->_game.setSpeedLevel(1);
|
||||
return true;
|
||||
} else if (userInputLowercase == "normal") {
|
||||
_vm->_game.setSpeedLevel(2);
|
||||
return true;
|
||||
} else if (userInputLowercase == "slow") {
|
||||
_vm->_game.setSpeedLevel(3);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Words::convertRussianUserInput(Common::String &userInputLowercase) {
|
||||
const char *conv =
|
||||
// АБВГДЕЖЗИЙКЛМНОП
|
||||
"abvgdewziiklmnop" // 80
|
||||
// РСТУФХЦЧШЩЪЫЬЭЮЯ
|
||||
"rstufxcyhhjijeuq" // 90
|
||||
// абвгдежзийклмноп
|
||||
"abvgdewziiklmnop" // a0
|
||||
" " // b0
|
||||
" " // c0
|
||||
" " // d0
|
||||
// рстуфхцчшщъыьэюя
|
||||
"rstufxcyhhjijeuq" // e0
|
||||
// Ее
|
||||
"ee ";// f0
|
||||
|
||||
Common::String tr;
|
||||
for (uint i = 0; i < userInputLowercase.size(); i++) {
|
||||
if ((byte)userInputLowercase[i] >= 0x80) {
|
||||
tr += conv[(byte)userInputLowercase[i] - 0x80];
|
||||
} else {
|
||||
tr += (byte)userInputLowercase[i];
|
||||
}
|
||||
}
|
||||
userInputLowercase = tr;
|
||||
}
|
||||
|
||||
} // End of namespace Agi
|
||||
79
engines/agi/words.h
Normal file
79
engines/agi/words.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AGI_WORDS_H
|
||||
#define AGI_WORDS_H
|
||||
|
||||
namespace Agi {
|
||||
|
||||
#define DICTIONARY_RESULT_UNKNOWN -1
|
||||
#define DICTIONARY_RESULT_IGNORE 0
|
||||
|
||||
#define EXTENDED_DICTIONARY_FILENAME "words.tok.extended"
|
||||
|
||||
struct WordEntry {
|
||||
uint16 id;
|
||||
Common::String word;
|
||||
};
|
||||
|
||||
class Words {
|
||||
public:
|
||||
Words(AgiEngine *vm);
|
||||
~Words();
|
||||
|
||||
private:
|
||||
AgiEngine *_vm;
|
||||
|
||||
// Dictionary of words in WORDS.TOK or WORDS.TOK.EXTENDED.
|
||||
// key: first character of the word
|
||||
// value: words in the order they appear
|
||||
Common::HashMap<byte, Common::Array<WordEntry>> _dictionary;
|
||||
|
||||
WordEntry _egoWords[MAX_WORDS];
|
||||
uint16 _egoWordCount;
|
||||
|
||||
bool _hasExtendedCharacters; // true on WORDS.TOK.EXTENDED
|
||||
public:
|
||||
uint16 getEgoWordCount() const;
|
||||
const char *getEgoWord(int16 wordNr) const;
|
||||
uint16 getEgoWordId(int16 wordNr) const;
|
||||
|
||||
int loadDictionary_v1(Common::SeekableReadStream &stream);
|
||||
int loadDictionary(const char *fname);
|
||||
int loadDictionary(Common::SeekableReadStream &stream);
|
||||
// used for fan made translations requiring extended char set
|
||||
int loadExtendedDictionary(const char *fname);
|
||||
void unloadDictionary();
|
||||
|
||||
void clearEgoWords();
|
||||
void parseUsingDictionary(const char *rawUserInput);
|
||||
|
||||
private:
|
||||
void cleanUpInput(const char *userInput, Common::String &cleanInput);
|
||||
int16 findWordInDictionary(const Common::String &userInputLowercase, uint16 userInputLen, uint16 userInputPos, uint16 &foundWordLen);
|
||||
|
||||
bool handleSpeedCommands(const Common::String &userInputLowercase);
|
||||
static void convertRussianUserInput(Common::String &userInputLowercase);
|
||||
};
|
||||
|
||||
} // End of namespace Agi
|
||||
|
||||
#endif /* AGI_WORDS_H */
|
||||
Reference in New Issue
Block a user