Initial commit

This commit is contained in:
2026-02-02 04:50:13 +01:00
commit 5b11698731
22592 changed files with 7677434 additions and 0 deletions

1
engines/draci/POTFILES Normal file
View File

@@ -0,0 +1 @@
engines/draci/metaengine.cpp

623
engines/draci/animation.cpp Normal file
View File

@@ -0,0 +1,623 @@
/* 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 "draci/draci.h"
#include "draci/animation.h"
#include "draci/barchive.h"
#include "draci/game.h"
#include "draci/screen.h"
#include "draci/sound.h"
#include "draci/surface.h"
#include "common/memstream.h"
#include "common/system.h"
namespace Draci {
Animation::Animation(DraciEngine *vm, int id, uint z, bool playing) : _vm(vm) {
_id = id;
_index = kIgnoreIndex;
_z = z;
clearShift();
_displacement = kNoDisplacement;
_playing = playing;
_looping = false;
_paused = false;
_canBeQuick = false;
_tick = _vm->_system->getMillis();
_currentFrame = 0;
_hasChangedFrame = true;
_callback = &Animation::doNothing;
_isRelative = false;
}
Animation::~Animation() {
deleteFrames();
}
void Animation::setRelative(int relx, int rely) {
// Delete the previous frame if there is one
if (_frames.size() > 0)
markDirtyRect(_vm->_screen->getSurface());
_displacement.relX = relx;
_displacement.relY = rely;
}
Displacement Animation::getCurrentFrameDisplacement() const {
Displacement dis = _displacement;
dis.relX += lround(dis.extraScaleX * _shift.x);
dis.relY += lround(dis.extraScaleY * _shift.y);
return dis;
}
Common::Point Animation::getCurrentFramePosition() const {
Displacement dis = getCurrentFrameDisplacement();
return Common::Point(dis.relX, dis.relY);
}
void Animation::setLooping(bool looping) {
_looping = looping;
debugC(7, kDraciAnimationDebugLevel, "Setting looping to %d on animation %d",
looping, _id);
}
void Animation::markDirtyRect(Surface *surface) const {
if (getFrameCount() == 0)
return;
// Fetch the current frame's rectangle
const Drawable *frame = getConstCurrentFrame();
Common::Rect frameRect = frame->getRect(getCurrentFrameDisplacement());
// Mark the rectangle dirty on the surface
surface->markDirtyRect(frameRect);
}
void Animation::nextFrame(bool force) {
// If there are no frames or if the animation is not playing, return
if (getFrameCount() == 0 || !_playing)
return;
const Drawable *frame = getConstCurrentFrame();
Surface *surface = _vm->_screen->getSurface();
if (force || (_tick + frame->getDelay() <= _vm->_system->getMillis()) ||
(_canBeQuick && _vm->_game->getEnableQuickHero() && _vm->_game->getWantQuickHero())) {
// If we are at the last frame and not looping, stop the animation
// The animation is also restarted to frame zero
if ((_currentFrame == getFrameCount() - 1) && !_looping) {
// When the animation reaches its end, call the preset callback
(this->*_callback)();
} else {
// Mark old frame dirty so it gets deleted
markDirtyRect(surface);
_shift.x += _relativeShifts[_currentFrame].x;
_shift.y += _relativeShifts[_currentFrame].y;
_currentFrame = nextFrameNum();
_tick = _vm->_system->getMillis();
// Fetch new frame and mark it dirty
markDirtyRect(surface);
// If the animation is paused, then nextFrameNum()
// returns the same frame number even though the time
// has elapsed to switch to another frame. We must not
// flip _hasChangedFrame to true, otherwise the sample
// assigned to this frame will be re-started over and
// over until all sound handles are exhausted (happens,
// e.g., when switching to the inventory which pauses
// all animations).
_hasChangedFrame = !_paused;
}
}
debugC(6, kDraciAnimationDebugLevel,
"anim=%d tick=%d delay=%d tick+delay=%d currenttime=%d frame=%d framenum=%d x=%d y=%d z=%d",
_id, _tick, frame->getDelay(), _tick + frame->getDelay(), _vm->_system->getMillis(),
_currentFrame, _frames.size(), frame->getX() + getRelativeX(), frame->getY() + getRelativeY(), _z);
}
uint Animation::nextFrameNum() const {
if (_paused)
return _currentFrame;
if ((_currentFrame == getFrameCount() - 1) && _looping)
return 0;
else
return _currentFrame + 1;
}
void Animation::drawFrame(Surface *surface) {
// If there are no frames or the animation is not playing, return
if (_frames.size() == 0 || !_playing)
return;
const Drawable *frame = getConstCurrentFrame();
if (_id == kOverlayImage) {
// No displacement or relative animations is supported.
frame->draw(surface, false, 0, 0);
} else {
// Draw frame: first shifted by the relative shift and then
// scaled/shifted by the given displacement.
frame->drawReScaled(surface, false, getCurrentFrameDisplacement());
}
const SoundSample *sample = _samples[_currentFrame];
if (_hasChangedFrame && sample) {
uint duration = _vm->_sound->playSound(sample, Audio::Mixer::kMaxChannelVolume, false);
debugC(3, kDraciSoundDebugLevel,
"Playing sample on animation %d, frame %d: %d+%d at %dHz: %dms",
_id, _currentFrame, sample->_offset, sample->_length, sample->_frequency, duration);
}
_hasChangedFrame = false;
}
void Animation::setPlaying(bool playing) {
_tick = _vm->_system->getMillis();
_playing = playing;
// When restarting an animation, allow playing sounds.
_hasChangedFrame |= playing;
}
void Animation::setScaleFactors(double scaleX, double scaleY) {
debugC(5, kDraciAnimationDebugLevel,
"Setting scaling factors on anim %d (scaleX: %.3f scaleY: %.3f)",
_id, scaleX, scaleY);
markDirtyRect(_vm->_screen->getSurface());
_displacement.extraScaleX = scaleX;
_displacement.extraScaleY = scaleY;
}
void Animation::addFrame(Drawable *frame, const SoundSample *sample) {
_frames.push_back(frame);
_samples.push_back(sample);
_relativeShifts.push_back(Common::Point(0, 0));
}
void Animation::makeLastFrameRelative(int x, int y) {
_relativeShifts.back() = Common::Point(x, y);
}
void Animation::clearShift() {
_shift = Common::Point(0, 0);
}
void Animation::replaceFrame(int i, Drawable *frame, const SoundSample *sample) {
_frames[i] = frame;
_samples[i] = sample;
}
const Drawable *Animation::getConstCurrentFrame() const {
// If there are no frames stored, return NULL
return _frames.size() > 0 ? _frames[_currentFrame] : NULL;
}
Drawable *Animation::getCurrentFrame() {
// If there are no frames stored, return NULL
return _frames.size() > 0 ? _frames[_currentFrame] : NULL;
}
Drawable *Animation::getFrame(int frameNum) {
// If there are no frames stored, return NULL
return _frames.size() > 0 ? _frames[frameNum] : NULL;
}
void Animation::setCurrentFrame(uint frame) {
// Check whether the value is sane
if (frame >= _frames.size()) {
return;
}
_currentFrame = frame;
}
void Animation::deleteFrames() {
// If there are no frames to delete, return
if (_frames.size() == 0) {
return;
}
markDirtyRect(_vm->_screen->getSurface());
for (int i = getFrameCount() - 1; i >= 0; --i) {
delete _frames[i];
_frames.pop_back();
}
_relativeShifts.clear();
_samples.clear();
}
void Animation::exitGameLoop() {
_vm->_game->setExitLoop(true);
}
void Animation::tellWalkingState() {
_vm->_game->heroAnimationFinished();
}
void Animation::play() {
if (isPlaying()) {
return;
}
// Title screen in the intro cutscene
if (getID() == 671 && _vm->_game->getMapID() == 42) {
_vm->setTTSVoice(kNarratorID);
_vm->sayText("Dra\x87\xa1 Historie");
}
// Mark the first frame dirty so it gets displayed
markDirtyRect(_vm->_screen->getSurface());
setPlaying(true);
debugC(3, kDraciAnimationDebugLevel, "Playing animation %d...", getID());
}
void Animation::stop() {
if (!isPlaying()) {
return;
}
// Clean up the last frame that was drawn before stopping
markDirtyRect(_vm->_screen->getSurface());
setPlaying(false);
// Reset the animation to the beginning
setCurrentFrame(0);
clearShift();
debugC(3, kDraciAnimationDebugLevel, "Stopping animation %d...", getID());
}
void Animation::del() {
_vm->_anims->deleteAnimation(this);
}
void AnimationManager::pauseAnimations() {
if (_animationPauseCounter++) {
// Already paused
return;
}
for (auto &anim : _animations) {
if (anim->getID() > 0 || anim->getID() == kTitleText) {
// Clean up the last frame that was drawn before stopping
anim->markDirtyRect(_vm->_screen->getSurface());
anim->setPaused(true);
}
}
}
void AnimationManager::unpauseAnimations() {
if (--_animationPauseCounter) {
// Still paused
return;
}
for (auto &anim : _animations) {
if (anim->isPaused()) {
// Clean up the last frame that was drawn before stopping
anim->markDirtyRect(_vm->_screen->getSurface());
anim->setPaused(false);
}
}
}
Animation *AnimationManager::getAnimation(int id) {
for (auto &anim : _animations) {
if (anim->getID() == id) {
return anim;
}
}
return nullptr;
}
void AnimationManager::insert(Animation *anim, bool allocateIndex) {
if (allocateIndex)
anim->setIndex(++_lastIndex);
Common::List<Animation *>::iterator it;
for (it = _animations.begin(); it != _animations.end(); ++it) {
if (anim->getZ() < (*it)->getZ())
break;
}
_animations.insert(it, anim);
}
void AnimationManager::drawScene(Surface *surf) {
// Fill the screen with color zero since some rooms may rely on the screen being black
_vm->_screen->getSurface()->fill(0);
sortAnimations();
for (auto &anim : _animations) {
if (!(anim->isPlaying())) {
continue;
}
anim->nextFrame(false);
anim->drawFrame(surf);
}
}
void AnimationManager::sortAnimations() {
Common::List<Animation *>::iterator cur;
Common::List<Animation *>::iterator next;
cur = _animations.begin();
// If the list is empty, we're done
if (cur == _animations.end())
return;
bool hasChanged;
do {
hasChanged = false;
cur = _animations.begin();
next = cur;
while (true) {
next++;
// If we are at the last element, we're done
if (next == _animations.end())
break;
// If we find an animation out of order, reinsert it
if ((*next)->getZ() < (*cur)->getZ()) {
Animation *anim = *next;
next = _animations.reverse_erase(next);
insert(anim, false);
hasChanged = true;
}
// Advance to next animation
cur = next;
}
} while (hasChanged);
}
void AnimationManager::deleteAnimation(Animation *anim) {
if (!anim) {
return;
}
Common::List<Animation *>::iterator it;
int index = -1;
// Iterate for the first time to delete the animation
for (it = _animations.begin(); it != _animations.end(); ++it) {
if (*it == anim) {
// Remember index of the deleted animation
index = (*it)->getIndex();
debugC(3, kDraciAnimationDebugLevel, "Deleting animation %d...", anim->getID());
delete *it;
_animations.erase(it);
break;
}
}
// Iterate the second time to decrease indexes greater than the deleted animation index
for (it = _animations.begin(); it != _animations.end(); ++it) {
if ((*it)->getIndex() > index && (*it)->getIndex() != kIgnoreIndex) {
(*it)->setIndex((*it)->getIndex() - 1);
}
}
// Decrement index of last animation
_lastIndex -= 1;
}
void AnimationManager::deleteOverlays() {
debugC(3, kDraciAnimationDebugLevel, "Deleting overlays...");
Common::List<Animation *>::iterator it;
for (it = _animations.begin(); it != _animations.end(); ++it) {
if ((*it)->getID() == kOverlayImage) {
delete *it;
it = _animations.reverse_erase(it);
}
}
}
void AnimationManager::deleteAll() {
debugC(3, kDraciAnimationDebugLevel, "Deleting all animations...");
for (auto *anim : _animations) {
delete anim;
}
_animations.clear();
_lastIndex = -1;
}
void AnimationManager::deleteAfterIndex(int index) {
Common::List<Animation *>::iterator it;
for (it = _animations.begin(); it != _animations.end(); ++it) {
if ((*it)->getIndex() > index) {
debugC(3, kDraciAnimationDebugLevel, "Deleting animation %d...", (*it)->getID());
delete *it;
it = _animations.reverse_erase(it);
}
}
_lastIndex = index;
}
const Animation *AnimationManager::getTopAnimation(int x, int y) const {
Common::List<Animation *>::const_iterator it;
Animation *retval = nullptr;
// Get transparent color for the current screen
const int transparent = _vm->_screen->getSurface()->getTransparentColor();
for (it = _animations.reverse_begin(); it != _animations.end(); --it) {
Animation *anim = *it;
// If the animation is not playing, ignore it
if (!anim->isPlaying() || anim->isPaused()) {
continue;
}
const Drawable *frame = anim->getConstCurrentFrame();
if (frame == nullptr) {
continue;
}
bool matches = false;
if (frame->getRect(anim->getCurrentFrameDisplacement()).contains(x, y)) {
if (frame->getType() == kDrawableText) {
matches = true;
} else if (frame->getType() == kDrawableSprite &&
reinterpret_cast<const Sprite *>(frame)->getPixel(x, y, anim->getCurrentFrameDisplacement()) != transparent) {
matches = true;
}
}
// Return the top-most animation object, unless it is a
// non-clickable sprite (overlay, debugging sprites for
// walking, or title/speech text) and there is an actual object
// underneath it.
if (matches) {
if (anim->getID() > kOverlayImage || anim->getID() < kSpeechText) {
return anim;
} else if (retval == nullptr) {
retval = anim;
}
}
}
// The default return value if no animations were found on these coordinates (not even overlays)
return retval;
}
Animation *AnimationManager::load(uint animNum) {
// Make double-sure that an animation isn't loaded more than twice,
// otherwise horrible things happen in the AnimationManager, because
// they use a simple link-list without duplicate checking. This should
// never happen unless there is a bug in the game, because all GPL2
// commands are guarded.
assert(!getAnimation(animNum));
const BAFile *animFile = _vm->_animationsArchive->getFile(animNum);
Common::MemoryReadStream animationReader(animFile->_data, animFile->_length);
uint numFrames = animationReader.readByte();
// The following two flags are ignored by the played. Memory logic was
// a hint to the old player whether it should cache the sprites or load
// them on demand. We have 1 memory manager and ignore these hints.
animationReader.readByte();
// The disable erasing field is just a (poor) optimization flag that
// turns of drawing the background underneath the sprite. By reading
// the source code of the old player, I'm not sure if that would ever
// have worked. There are only 6 animations in the game with this flag
// true. All of them have just 1 animation phase and they are used to
// patch a part of the original background by a new sprite. This
// should work with the default logic as well---just play this
// animation on top of the background. Since the only meaning of the
// flag was optimization, ignoring should be OK even without dipping
// into details.
animationReader.readByte();
const bool cyclic = animationReader.readByte();
const bool relative = animationReader.readByte();
Animation *anim = new Animation(_vm, animNum, 0, false);
insert(anim, true);
anim->setLooping(cyclic);
anim->setIsRelative(relative);
for (uint i = 0; i < numFrames; ++i) {
uint spriteNum = animationReader.readUint16LE() - 1;
int x = animationReader.readSint16LE();
int y = animationReader.readSint16LE();
uint scaledWidth = animationReader.readUint16LE();
uint scaledHeight = animationReader.readUint16LE();
byte mirror = animationReader.readByte();
int sample = animationReader.readUint16LE() - 1;
uint freq = animationReader.readUint16LE();
uint delay = animationReader.readUint16LE();
// _spritesArchive is flushed when entering a room. All
// scripts in a room are responsible for loading their animations.
const BAFile *spriteFile = _vm->_spritesArchive->getFile(spriteNum);
Sprite *sp = new Sprite(spriteFile->_data, spriteFile->_length,
relative ? 0 : x, relative ? 0 : y, true);
// Some frames set the scaled dimensions to 0 even though other frames
// from the same animations have them set to normal values
// We work around this by assuming it means no scaling is necessary
if (scaledWidth == 0) {
scaledWidth = sp->getWidth();
}
if (scaledHeight == 0) {
scaledHeight = sp->getHeight();
}
sp->setScaled(scaledWidth, scaledHeight);
if (mirror)
sp->setMirrorOn();
sp->setDelay(delay * 10);
anim->addFrame(sp, _vm->_soundsArchive->getSample(sample, freq));
if (relative) {
anim->makeLastFrameRelative(x, y);
}
}
return anim;
}
} // End of namespace Draci

225
engines/draci/animation.h Normal file
View File

@@ -0,0 +1,225 @@
/* 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 DRACI_ANIMATION_H
#define DRACI_ANIMATION_H
#include "common/array.h"
#include "common/list.h"
#include "common/rect.h"
#include "draci/sprite.h"
namespace Draci {
/**
* Animation IDs for those animations that don't have their IDs
* specified in the data files.
*/
enum {
kOverlayImage = -1,
kWalkingMapOverlay = -2,
kWalkingShortestPathOverlay = -3,
kWalkingObliquePathOverlay = -4,
kTitleText = -5,
kSpeechText = -6,
kInventorySprite = -7,
kDialogueLinesID = -8,
kUnused = -12,
kInventoryItemsID = -13
};
/**
* Used by overlays as a neutral index that won't get
* released with the GPL Release command.
*/
enum { kIgnoreIndex = -2 };
class DraciEngine;
class Surface;
struct SoundSample;
class Animation {
typedef void (Animation::* AnimationCallback)();
public:
Animation(DraciEngine *v, int id, uint z, bool playing);
~Animation();
uint getZ() const { return _z; }
void setZ(uint z) { _z = z; }
void setID(int id) { _id = id; }
int getID() const { return _id; }
void nextFrame(bool force);
void drawFrame(Surface *surface);
void addFrame(Drawable *frame, const SoundSample *sample);
void replaceFrame(int i, Drawable *frame, const SoundSample *sample);
const Drawable *getConstCurrentFrame() const;
Drawable *getCurrentFrame();
Drawable *getFrame(int frameNum);
void setCurrentFrame(uint frame);
uint currentFrameNum() const { return _currentFrame; }
uint getFrameCount() const { return _frames.size(); }
void makeLastFrameRelative(int x, int y);
void clearShift();
bool isPlaying() const { return _playing; }
void setPlaying(bool playing);
bool isPaused() const { return _paused; }
void setPaused(bool paused) { _paused = paused; }
bool isLooping() const { return _looping; }
void setLooping(bool looping);
void setIsRelative(bool value) { _isRelative = value; }
bool isRelative() const { return _isRelative; }
void setRelative(int relx, int rely);
int getRelativeX() const { return _displacement.relX; }
int getRelativeY() const { return _displacement.relY; }
const Displacement &getDisplacement() const { return _displacement; } // displacement of the whole animation
Displacement getCurrentFrameDisplacement() const; // displacement of the current frame (includes _shift)
Common::Point getCurrentFramePosition() const; // with displacement and shift applied
void supportsQuickAnimation(bool val) { _canBeQuick = val; }
int getIndex() const { return _index; }
void setIndex(int index) { _index = index; }
void setScaleFactors(double scaleX, double scaleY);
double getScaleX() const { return _displacement.extraScaleX; }
double getScaleY() const { return _displacement.extraScaleY; }
void markDirtyRect(Surface *surface) const;
// Animation callbacks. They can only do simple things, such as
// setting the value of some variable or stopping an animation. In
// particular, they cannot run sub-loops or anything like that, because
// the callback is called at an arbitrary time without much control
// over what the state of the rest of the program is.
void registerCallback(AnimationCallback callback) { _callback = callback; }
void doNothing() {}
void exitGameLoop();
void tellWalkingState();
void play();
void stop();
void del();
private:
uint nextFrameNum() const;
void deleteFrames();
/** Internal animation ID
* (as specified in the data files and the bytecode)
*/
int _id;
/** The recency index of an animation, i.e. the most recently added animation has
* the highest index. Some script commands need this.
*/
int _index;
uint _currentFrame;
uint _z;
Common::Point _shift; // partial sum of _relativeShifts from the beginning of the animation until the current frame
bool _hasChangedFrame;
Displacement _displacement;
bool _isRelative;
uint _tick;
bool _playing;
bool _looping;
bool _paused;
bool _canBeQuick;
/** Array of frames of the animation. The animation object owns these pointers.
*/
Common::Array<Drawable *> _frames;
Common::Array<Common::Point> _relativeShifts;
/** Array of samples played during the animation. The animation
* object doesn't own these pointers, but they are stored in the
* cache.
*/
Common::Array<const SoundSample *> _samples;
AnimationCallback _callback;
DraciEngine *_vm;
};
class AnimationManager {
public:
AnimationManager(DraciEngine *vm) : _vm(vm), _lastIndex(-1), _animationPauseCounter(0) {}
~AnimationManager() { deleteAll(); }
void insert(Animation *anim, bool allocateIndex);
Animation *load(uint animNum);
void pauseAnimations();
void unpauseAnimations();
void deleteAnimation(Animation *anim);
void deleteOverlays();
void deleteAll();
void drawScene(Surface *surf);
Animation *getAnimation(int id);
int getLastIndex() const { return _lastIndex; }
void deleteAfterIndex(int index);
const Animation *getTopAnimation(int x, int y) const;
private:
void sortAnimations();
DraciEngine *_vm;
/** List of animation objects, maintained sorted by decreasing Z-coordinates.
* The animation manager owns the pointers.
*/
Common::List<Animation *> _animations;
/** The index of the most recently added animation.
* See Animation::_index for details.
*/
int _lastIndex;
/** How many times the animations are paused.
* Needed because the animations can be paused once by entering the
* inventory and then again by entering the game menu. When they are
* unpaused the first time, they should be kept paused. */
int _animationPauseCounter;
};
} // End of namespace Draci
#endif // DRACI_ANIMATION_H

401
engines/draci/barchive.cpp Normal file
View File

@@ -0,0 +1,401 @@
/* 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/debug.h"
#include "common/str.h"
#include "common/memstream.h"
#include "draci/barchive.h"
#include "draci/draci.h"
namespace Draci {
const char BArchive::_magicNumber[] = "BAR!";
const char BArchive::_dfwMagicNumber[] = "BS";
/**
* @brief Loads a DFW archive
* @param path Path to input file
*
* Tries to load the file as a DFW archive if opening as BAR fails. Should only be called
* from openArchive(). Only one of the game files appears to use this format (HRA.DFW)
* and this file is compressed using a simple run-length scheme.
*
* archive format: header
* index table
* file0, file1, ...
*
* header format: [uint16LE] file count
* [uint16LE] index table size
* [2 bytes] magic number "BS"
*
* index table format: entry0, entry1, ...
*
* entry<N> format: [uint16LE] compressed size (not including the 2 bytes for the
* "uncompressed size" field)
* [uint32LE] fileN offset from start of file
*
* file<N> format: [uint16LE] uncompressed size
* [uint16LE] compressed size (the same as in the index table entry)
* [byte] stopper mark (for run-length compression)
* [multiple bytes] compressed data
*/
void BArchive::openDFW(const Common::Path &path) {
byte *table;
uint16 tableSize;
byte buf[2];
_f.open(path);
if (!_f.isOpen()) {
debugC(2, kDraciArchiverDebugLevel, "Error opening file");
return;
}
_fileCount = _f.readUint16LE();
tableSize = _f.readUint16LE();
_f.read(buf, 2);
if (memcmp(buf, _dfwMagicNumber, 2) == 0) {
debugC(2, kDraciArchiverDebugLevel, "Success");
_isDFW = true;
} else {
debugC(2, kDraciArchiverDebugLevel, "Not a DFW archive");
_f.close();
return;
}
debugC(2, kDraciArchiverDebugLevel, "Archive info (DFW): %d files", _fileCount);
// Read in index table
table = new byte[tableSize];
_f.read(table, tableSize);
// Read in file headers, but do not read the actual data yet
// The data will be read on demand to save memory
_files = new BAFile[_fileCount];
Common::MemoryReadStream tableReader(table, tableSize);
for (uint i = 0; i < _fileCount; ++i) {
_files[i]._compLength = tableReader.readUint16LE();
_files[i]._offset = tableReader.readUint32LE();
// Seek to the current file
_f.seek(_files[i]._offset);
_files[i]._length = _f.readUint16LE(); // Read in uncompressed length
_f.readUint16LE(); // Compressed length again (already read from the index table)
_files[i]._stopper = _f.readByte();
_files[i]._data = nullptr; // File data will be read in on demand
_files[i]._crc = 0; // Dummy value; not used in DFW archives
}
// Indicate that the archive was successfully opened
_opened = true;
// Cleanup
delete[] table;
}
/**
* @brief BArchive open method
* @param path Path to input file
*
* Opens a BAR (Bob's Archiver) archive, which is the game's archiving format.
* BAR archives have a .DFW file extension, due to a historical interface.
*
* archive format: header,
* file0, file1, ...
* footer
*
* header format: [4 bytes] magic number "BAR!"
* [uint16LE] file count (number of archived streams),
* [uint32LE] footer offset from start of file
*
* file<N> format: [2 bytes] compressed length
* [2 bytes] original length
* [1 byte] compression type
* [1 byte] CRC
* [multiple bytes] actual data
*
* footer format: [array of uint32LE] offsets of individual files from start of archive
* (last entry is footer offset again)
*/
void BArchive::openArchive(const Common::Path &path) {
byte buf[4];
byte *footer;
uint32 footerOffset, footerSize;
// Close previously opened archive (if any)
closeArchive();
debugCN(2, kDraciArchiverDebugLevel, "Loading archive %s: ", path.toString(Common::Path::kNativeSeparator).c_str());
_f.open(path);
if (_f.isOpen()) {
debugC(2, kDraciArchiverDebugLevel, "Success");
} else {
debugC(2, kDraciArchiverDebugLevel, "Error");
return;
}
// Save path for reading in files later on
_path = path;
// Read archive header
debugCN(2, kDraciArchiverDebugLevel, "Checking for BAR magic number: ");
_f.read(buf, 4);
if (memcmp(buf, _magicNumber, 4) == 0) {
debugC(2, kDraciArchiverDebugLevel, "Success");
// Indicate this archive is a BAR
_isDFW = false;
} else {
debugC(2, kDraciArchiverDebugLevel, "Not a BAR archive");
debugCN(2, kDraciArchiverDebugLevel, "Retrying as DFW: ");
_f.close();
// Try to open as DFW
openDFW(_path);
return;
}
_fileCount = _f.readUint16LE();
footerOffset = _f.readUint32LE();
footerSize = _f.size() - footerOffset;
debugC(2, kDraciArchiverDebugLevel, "Archive info: %d files, %d data bytes",
_fileCount, footerOffset - _archiveHeaderSize);
// Read in footer
footer = new byte[footerSize];
_f.seek(footerOffset);
_f.read(footer, footerSize);
Common::MemoryReadStream reader(footer, footerSize);
// Read in file headers, but do not read the actual data yet
// The data will be read on demand to save memory
_files = new BAFile[_fileCount];
for (uint i = 0; i < _fileCount; i++) {
uint32 fileOffset;
fileOffset = reader.readUint32LE();
_f.seek(fileOffset); // Seek to next file in archive
_files[i]._compLength = _f.readUint16LE(); // Compressed size
// should be the same as uncompressed
_files[i]._length = _f.readUint16LE(); // Original size
_files[i]._offset = fileOffset; // Offset of file from start
byte compressionType = _f.readByte();
assert(compressionType == 0 &&
"Compression type flag is non-zero (file is compressed)");
_files[i]._crc = _f.readByte(); // CRC checksum of the file
_files[i]._data = nullptr; // File data will be read in on demand
_files[i]._stopper = 0; // Dummy value; not used in BAR files, needed in DFW
}
// Last footer item should be equal to footerOffset
uint32 footerOffset2 = reader.readUint32LE();
assert(footerOffset2 == footerOffset && "Footer offset mismatch");
// Indicate that the archive has been successfully opened
_opened = true;
delete[] footer;
}
/**
* @brief BArchive close method
*
* Closes the currently opened archive. It can be called explicitly to
* free up memory.
*/
void BArchive::closeArchive() {
if (!_opened) {
return;
}
for (uint i = 0; i < _fileCount; ++i) {
if (_files[i]._data) {
delete[] _files[i]._data;
}
}
delete[] _files;
_f.close();
_opened = false;
_files = nullptr;
_fileCount = 0;
}
/**
* @brief On-demand BAR file loader
* @param i Index of file inside an archive
* @return Pointer to a BAFile coresponding to the opened file or NULL (on failure)
*
* Loads individual BAR files from an archive to memory on demand.
* Should not be called directly.
*/
BAFile *BArchive::loadFileBAR(uint i) {
// Else open archive and read in requested file
if (!_f.isOpen()) {
debugC(2, kDraciArchiverDebugLevel, "Error");
return nullptr;
}
// Read in the file (without the file header)
_f.seek(_files[i]._offset + _fileHeaderSize);
_files[i]._data = new byte[_files[i]._length];
_f.read(_files[i]._data, _files[i]._length);
// Calculate CRC
byte tmp = 0;
for (uint j = 0; j < _files[i]._length; j++) {
tmp ^= _files[i]._data[j];
}
debugC(2, kDraciArchiverDebugLevel, "Read %d bytes", _files[i]._length);
assert(tmp == _files[i]._crc && "CRC checksum mismatch");
return _files + i;
}
/**
* @brief On-demand DFW file loader
* @param i Index of file inside an archive
* @return Pointer to a BAFile coresponding to the opened file or NULL (on failure)
*
* Loads individual DFW files from an archive to memory on demand.
* Should not be called directly.
*/
BAFile *BArchive::loadFileDFW(uint i) {
byte *buf;
// Else open archive and read in requested file
if (!_f.isOpen()) {
debugC(2, kDraciArchiverDebugLevel, "Error");
return nullptr;
}
// Seek to raw data of the file
// Five bytes are for the header (uncompressed and compressed length, stopper mark)
_f.seek(_files[i]._offset + 5);
// Since we are seeking directly to raw data, we subtract 3 bytes from the length
// (to take account the compressed length and stopper mark)
uint16 compressedLength = _files[i]._compLength - 3;
uint16 uncompressedLength = _files[i]._length;
debugC(2, kDraciArchiverDebugLevel,
"File info (DFW): uncompressed %d bytes, compressed %d bytes",
uncompressedLength, compressedLength);
// Allocate a buffer for the file data
buf = new byte[compressedLength];
// Read in file data into the buffer
_f.read(buf, compressedLength);
// Allocate the space for the uncompressed file
byte *dst;
dst = _files[i]._data = new byte[uncompressedLength];
Common::MemoryReadStream data(buf, compressedLength);
// Uncompress file
byte current, what;
byte stopper = _files[i]._stopper;
uint repeat;
uint len = 0; // Sanity check (counts uncompressed bytes)
current = data.readByte(); // Read initial byte
while (!data.eos()) {
if (current != stopper) {
*dst++ = current;
++len;
} else {
// Inflate block
repeat = data.readByte();
what = data.readByte();
len += repeat;
for (uint j = 0; j < repeat; ++j) {
*dst++ = what;
}
}
current = data.readByte();
}
assert(len == _files[i]._length && "Uncompressed file not of the expected length");
delete[] buf;
return _files + i;
}
/**
* Clears the cache of the open files inside the archive without closing it.
* If the files are subsequently accessed, they are read from the disk.
*/
void BArchive::clearCache() {
// Delete all cached data
for (uint i = 0; i < _fileCount; ++i) {
_files[i].close();
}
}
const BAFile *BArchive::getFile(uint i) {
// Check whether requested file exists
if (i >= _fileCount) {
return nullptr;
}
debugCN(2, kDraciArchiverDebugLevel, "Accessing file %d from archive %s... ",
i, _path.toString(Common::Path::kNativeSeparator).c_str());
// Check if file has already been opened and return that
if (_files[i]._data) {
debugC(2, kDraciArchiverDebugLevel, "Cached");
return _files + i;
}
BAFile *file;
// file will be NULL if something goes wrong
if (_isDFW) {
file = loadFileDFW(i);
} else {
file = loadFileBAR(i);
}
return file;
}
} // End of namespace Draci

96
engines/draci/barchive.h Normal file
View File

@@ -0,0 +1,96 @@
/* 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 DRACI_BARCHIVE_H
#define DRACI_BARCHIVE_H
#include "common/str.h"
#include "common/file.h"
namespace Draci {
/**
* Represents individual files inside the archive.
*/
struct BAFile {
uint _compLength; ///< Compressed length (the same as _length if the file is uncompressed)
uint _length; ///< Uncompressed length
uint32 _offset; ///< Offset of file inside archive
byte *_data;
byte _crc;
byte _stopper; ///< Not used in BAR files, needed for DFW
/** Releases the file data (for memory considerations) */
void close() {
delete[] _data;
_data = NULL;
}
};
class BArchive {
public:
BArchive() : _files(NULL), _fileCount(0), _opened(false) {}
BArchive(const Common::Path &path) :
_files(NULL), _fileCount(0), _opened(false) {
openArchive(path);
}
~BArchive() { closeArchive(); }
void openArchive(const Common::Path &path);
void closeArchive();
uint size() const { return _fileCount; }
/**
* Checks whether there is an archive opened. Should be called before reading
* from the archive to check whether openArchive() succeeded.
*/
bool isOpen() const { return _opened; }
void clearCache();
const BAFile *getFile(uint i);
private:
// Archive header data
static const char _magicNumber[];
static const char _dfwMagicNumber[];
static const uint _archiveHeaderSize = 10;
// File stream header data
static const uint _fileHeaderSize = 6;
Common::Path _path; ///< Path to file
BAFile *_files; ///< Internal array of files
uint _fileCount; ///< Number of files in archive
bool _isDFW; ///< True if the archive is in DFW format, false otherwise
bool _opened; ///< True if the archive is opened, false otherwise
Common::File _f; ///< Opened file handle
void openDFW(const Common::Path &path);
BAFile *loadFileDFW(uint i);
BAFile *loadFileBAR(uint i);
};
} // End of namespace Draci
#endif // DRACI_BARCHIVE_H

View 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 draci "Dragon History" yes "" "" "" "midi"

34
engines/draci/console.cpp Normal file
View File

@@ -0,0 +1,34 @@
/* 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 "draci/console.h"
#include "draci/draci.h"
namespace Draci {
DraciConsole::DraciConsole(DraciEngine *vm) : GUI::Debugger(), _vm(vm) {
assert(_vm);
}
DraciConsole::~DraciConsole() {
}
} // End of namespace Draci

42
engines/draci/console.h Normal file
View File

@@ -0,0 +1,42 @@
/* 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 DRACI_CONSOLE_H
#define DRACI_CONSOLE_H
#include "gui/debugger.h"
namespace Draci {
class DraciEngine;
class DraciConsole : public GUI::Debugger {
public:
DraciConsole(DraciEngine *vm);
~DraciConsole(void) override;
private:
DraciEngine *_vm;
};
} // End of namespace Draci
#endif

4
engines/draci/credits.pl Normal file
View File

@@ -0,0 +1,4 @@
begin_section("Draci");
add_person("Denis Kasak", "dkasak13", "");
add_person("Robert &Scaron;palek", "spalek", "");
end_section();

116
engines/draci/detection.cpp Normal file
View File

@@ -0,0 +1,116 @@
/* 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 "base/plugins.h"
#include "engines/advancedDetector.h"
#include "engines/metaengine.h"
#include "draci/draci.h"
#include "draci/detection.h"
static const DebugChannelDef debugFlagList[] = {
{Draci::kDraciGeneralDebugLevel, "general", "Draci general debug info"},
{Draci::kDraciBytecodeDebugLevel, "bytecode", "GPL bytecode instructions"},
{Draci::kDraciArchiverDebugLevel, "archiver", "BAR archiver debug info"},
{Draci::kDraciLogicDebugLevel, "logic", "Game logic debug info"},
{Draci::kDraciAnimationDebugLevel, "animation", "Animation debug info"},
{Draci::kDraciSoundDebugLevel, "sound", "Sound debug info"},
{Draci::kDraciWalkingDebugLevel, "walking", "Walking debug info"},
DEBUG_CHANNEL_END
};
static const PlainGameDescriptor draciGames[] = {
{ "draci", "Dra\304\215\303\255 Historie" },
{ nullptr, nullptr }
};
namespace Draci {
const ADGameDescription gameDescriptions[] = {
{
"draci",
nullptr,
AD_ENTRY1s("INIT.DFW", "b890a5aeebaf16af39219cba2416b0a3", 906),
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_SPEECH)
},
{
"draci",
nullptr,
AD_ENTRY1s("INIT.DFW", "9921c8f0045679a8f37eca8d41c5ec02", 906),
Common::CS_CZE,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_MISSING_VOICE)
},
{
"draci",
nullptr,
AD_ENTRY1s("INIT.DFW", "76b9b78a8a8809a240acc395df4d0715", 906),
Common::PL_POL,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_MISSING_VOICE)
},
{
"draci",
nullptr,
AD_ENTRY1s("INIT.DFW", "9a7115b91cdea361bcaff3e046ac7ded", 906),
Common::DE_DEU,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_SPEECH)
},
AD_TABLE_END_MARKER
};
} // End of namespace Draci
class DraciMetaEngineDetection : public AdvancedMetaEngineDetection<ADGameDescription> {
public:
DraciMetaEngineDetection() : AdvancedMetaEngineDetection(Draci::gameDescriptions, draciGames) {
}
const char *getName() const override {
return "draci";
}
const char *getEngineName() const override {
return "Dra\304\215\303\255 Historie";
}
const char *getOriginalCopyright() const override {
return "Dra\304\215\303\255 Historie (C) 1995 NoSense";
}
const DebugChannelDef *getDebugChannels() const override {
return debugFlagList;
}
};
REGISTER_PLUGIN_STATIC(DRACI_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, DraciMetaEngineDetection);

29
engines/draci/detection.h Normal file
View File

@@ -0,0 +1,29 @@
/* 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 DRACI_DETECTION_H
#define DRACI_DETECTION_H
#define GAMEOPTION_TTS_OBJECTS GUIO_GAMEOPTIONS1
#define GAMEOPTION_TTS_SPEECH GUIO_GAMEOPTIONS2
#define GAMEOPTION_TTS_MISSING_VOICE GUIO_GAMEOPTIONS3
#endif // DRACI_DETECTION_H

678
engines/draci/draci.cpp Normal file
View File

@@ -0,0 +1,678 @@
/* 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/scummsys.h"
#include "common/config-manager.h"
#include "common/debug-channels.h"
#include "common/events.h"
#include "common/file.h"
#include "common/keyboard.h"
#include "common/text-to-speech.h"
#include "engines/util.h"
#include "engines/advancedDetector.h"
#include "graphics/cursorman.h"
#include "graphics/font.h"
#include "draci/draci.h"
#include "draci/animation.h"
#include "draci/barchive.h"
#include "draci/font.h"
#include "draci/game.h"
#include "draci/mouse.h"
#include "draci/music.h"
#include "draci/saveload.h"
#include "draci/screen.h"
#include "draci/script.h"
#include "draci/sound.h"
#include "draci/sprite.h"
namespace Draci {
#ifdef USE_TTS
// Used by all languages but Polish
static const uint16 kamenickyEncodingTable[] = {
0xc48c, 0xc3bc, 0xc3a9, 0xc48f, // Č, ü, é, ď
0xc3a4, 0xc48e, 0xc5a4, 0xc48d, // ä, Ď, Ť, č
0xc49b, 0xc494, 0xc4b9, 0xc38d, // ě, Ě, Ĺ, Í
0xc4be, 0xc4ba, 0xc384, 0xc381, // ľ, ĺ, Ä, Á
0xc389, 0xc5be, 0xc5bd, 0xc3b4, // É, ž, Ž, ô
0xc3b6, 0xc393, 0xc5af, 0xc39a, // ö, Ó, ů, Ú
0xc3bd, 0xc396, 0xc39c, 0xc5a0, // ý, Ö, Ü, Š
0xc4bd, 0xc39d, 0xc598, 0xc5a5, // Ľ, Ý, Ř, ť
0xc3a1, 0xc3ad, 0xc3b3, 0xc3ba, // á, í, ó, ú
0xc588, 0xc587, 0xc5ae, 0xc394, // ň, Ň, Ů, Ô
0xc5a1, 0xc599, 0xc595, 0xc594 // š, ř, ŕ, Ŕ
};
// Name of encoding unknown (described by the Draci website as "some ridiculous proprietary encoding")
// After 0x9b (0xc5bb/Ż), it matches Kamenický encoding (though some of these Czech characters are replaced
// for TTS because it struggles to pronounce them)
static const uint16 polishEncodingTable[] = {
0xc485, 0xc487, 0xc499, 0xc582, // ą, ć, ę, ł
0xc584, 0xc3b3, 0xc59b, 0xc5ba, // ń, ó, ś, ź
0xc5bc, 0xc484, 0xc486, 0xc498, // ż, Ą, Ć, Ę
0xc581, 0xc583, 0xc393, 0xc59a, // Ł, Ń, Ó, Ś
0xc5b9, 0xc5bb, 0x5a, 0x6f, // Ź, Ż, Z, o
0xc3b6, 0xc393, 0xc5af, 0xc39a, // ö, Ó, ů, Ú
0xc3bd, 0xc396, 0xc39c, 0x53, // ý, Ö, Ü, S
0xc4bd, 0xc39d, 0x52, 0x74, // Ľ, Ý, R, t
0xc3a1, 0xc3ad, 0xc3b3, 0xc3ba, // á, í, ó, ú
0x6e, 0x4e, 0xc5ae, 0xc394, // n, N, Ů, Ô
0x73, 0x72, 0x72, 0x52 // s, r, r, R
};
// TTS for all languages but Czech struggles to voice a lot of Czech characters in the credits,
// and oftentimes skips them entirely (i.e. "Špalek" is pronounced as "Palek")
// To more closely resemble how the names are supposed to be pronounced,
// this table replaces certain Czech characters with an alternative
static const uint16 czechCharacterConversionTable[] = {
0x43, 0xc3bc, 0xc3a9, 0x64, // C, ü, é, d
0xc3a4, 0x44, 0x54, 0x63, // ä, D, T, c
0x65, 0x45, 0x4c, 0xc38d, // e, E, L, Í
0x6c, 0xc4ba, 0xc384, 0xc381, // l, ĺ, Ä, Á
0xc389, 0x7a, 0x5a, 0x6f, // É, z, Z, o
0xc3b6, 0xc393, 0x75, 0xc39a, // ö, Ó, u, Ú
0x0079, 0xc396, 0xc39c, 0x53, // y, Ö, Ü, S
0x4c, 0x59, 0x52, 0x74, // L, Y, R, t
0xc3a1, 0xc3ad, 0xc3b3, 0xc3ba, // á, í, ó, ú
0x6e, 0x4e, 0x55, 0x4f, // n, N, U, O
0x73, 0x72, 0x72, 0x52 // s, r, r, R
};
#endif
// Data file paths
const char *objectsPath = "OBJEKTY.DFW";
const char *palettePath = "PALETY.DFW";
const char *spritesPath = "OBR_AN.DFW";
const char *overlaysPath = "OBR_MAS.DFW";
const char *roomsPath = "MIST.DFW";
const char *animationsPath = "ANIM.DFW";
const char *iconsPath = "HRA.DFW";
const char *walkingMapsPath = "MAPY.DFW";
const char *itemsPath = "IKONY.DFW";
const char *itemImagesPath = "OBR_IK.DFW";
const char *initPath = "INIT.DFW";
const char *stringsPath = "RETEZCE.DFW";
const char *soundsPath = "CD2.SAM";
const char *dubbingPath = "CD.SAM";
const char *musicPathMask = "HUDBA%d.MID";
const uint kSoundsFrequency = 13000;
const uint kDubbingFrequency = 22050;
DraciEngine::DraciEngine(OSystem *syst, const ADGameDescription *gameDesc)
: Engine(syst), _gameDescription(gameDesc), _rnd("draci") {
setDebugger(new DraciConsole(this));
_screen = nullptr;
_mouse = nullptr;
_game = nullptr;
_script = nullptr;
_anims = nullptr;
_sound = nullptr;
_music = nullptr;
_smallFont = nullptr;
_bigFont = nullptr;
_iconsArchive = nullptr;
_objectsArchive = nullptr;
_spritesArchive = nullptr;
_paletteArchive = nullptr;
_roomsArchive = nullptr;
_overlaysArchive = nullptr;
_animationsArchive = nullptr;
_walkingMapsArchive = nullptr;
_itemsArchive = nullptr;
_itemImagesArchive = nullptr;
_initArchive = nullptr;
_stringsArchive = nullptr;
_soundsArchive = nullptr;
_dubbingArchive = nullptr;
_showWalkingMap = 0;
_pauseStartTime = 0;
}
bool DraciEngine::hasFeature(EngineFeature f) const {
return (f == kSupportsSubtitleOptions) ||
(f == kSupportsReturnToLauncher) ||
(f == kSupportsLoadingDuringRuntime) ||
(f == kSupportsSavingDuringRuntime);
}
static SoundArchive* openAnyPossibleDubbing() {
debugC(1, kDraciGeneralDebugLevel, "Trying to find original dubbing");
LegacySoundArchive *legacy = new LegacySoundArchive(dubbingPath, kDubbingFrequency);
if (legacy->isOpen() && legacy->size()) {
debugC(1, kDraciGeneralDebugLevel, "Found original dubbing");
return legacy;
}
delete legacy;
// The original uncompressed dubbing cannot be found. Try to open the
// newer compressed version.
debugC(1, kDraciGeneralDebugLevel, "Trying to find compressed dubbing");
ZipSoundArchive *zip = new ZipSoundArchive();
zip->openArchive("dub-raw.zzz", "buf", RAW80, kDubbingFrequency);
if (zip->isOpen() && zip->size()) return zip;
#ifdef USE_FLAC
zip->openArchive("dub-flac.zzz", "flac", FLAC);
if (zip->isOpen() && zip->size()) return zip;
#endif
#ifdef USE_VORBIS
zip->openArchive("dub-ogg.zzz", "ogg", OGG);
if (zip->isOpen() && zip->size()) return zip;
#endif
#ifdef USE_MAD
zip->openArchive("dub-mp3.zzz", "mp3", MP3);
if (zip->isOpen() && zip->size()) return zip;
#endif
// Return an empty (but initialized) archive anyway.
return zip;
}
int DraciEngine::init() {
// Initialize graphics using following:
initGraphics(kScreenWidth, kScreenHeight);
// Open game's archives
_initArchive = new BArchive(initPath);
_objectsArchive = new BArchive(objectsPath);
_spritesArchive = new BArchive(spritesPath);
_paletteArchive = new BArchive(palettePath);
_roomsArchive = new BArchive(roomsPath);
_overlaysArchive = new BArchive(overlaysPath);
_animationsArchive = new BArchive(animationsPath);
_iconsArchive = new BArchive(iconsPath);
_walkingMapsArchive = new BArchive(walkingMapsPath);
_itemsArchive = new BArchive(itemsPath);
_itemImagesArchive = new BArchive(itemImagesPath);
_stringsArchive = new BArchive(stringsPath);
_soundsArchive = new LegacySoundArchive(soundsPath, kSoundsFrequency);
_dubbingArchive = openAnyPossibleDubbing();
_sound = new Sound(_mixer);
_music = new MusicPlayer(musicPathMask);
// Setup mixer
syncSoundSettings();
// Load the game's fonts
_smallFont = new Font(kFontSmall);
_bigFont = new Font(kFontBig);
_screen = new Screen(this);
_anims = new AnimationManager(this);
_mouse = new Mouse(this);
_script = new Script(this);
_game = new Game(this);
if (!_objectsArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening objects archive failed");
return Common::kUnknownError;
}
if (!_spritesArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening sprites archive failed");
return Common::kUnknownError;
}
if (!_paletteArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening palette archive failed");
return Common::kUnknownError;
}
if (!_roomsArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening rooms archive failed");
return Common::kUnknownError;
}
if (!_overlaysArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening overlays archive failed");
return Common::kUnknownError;
}
if (!_animationsArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening animations archive failed");
return Common::kUnknownError;
}
if (!_iconsArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening icons archive failed");
return Common::kUnknownError;
}
if (!_walkingMapsArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening walking maps archive failed");
return Common::kUnknownError;
}
if (!_soundsArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Opening sounds archive failed");
return Common::kUnknownError;
}
if (!_dubbingArchive->isOpen()) {
debugC(2, kDraciGeneralDebugLevel, "WARNING - Opening dubbing archive failed");
}
_showWalkingMap = false;
// Basic archive test
debugC(2, kDraciGeneralDebugLevel, "Running archive tests...");
Common::Path path("INIT.DFW");
BArchive ar(path);
const BAFile *f;
debugC(3, kDraciGeneralDebugLevel, "Number of file streams in archive: %d", ar.size());
if (ar.isOpen()) {
f = ar.getFile(0);
} else {
debugC(2, kDraciGeneralDebugLevel, "ERROR - Archive not opened");
return Common::kUnknownError;
}
debugC(3, kDraciGeneralDebugLevel, "First 10 bytes of file %d: ", 0);
for (uint i = 0; i < 10; ++i) {
debugC(3, kDraciGeneralDebugLevel, "0x%02x%c", f->_data[i], (i < 9) ? ' ' : '\n');
}
return Common::kNoError;
}
void DraciEngine::handleEvents() {
Common::Event event;
while (_eventMan->pollEvent(event)) {
switch (event.type) {
case Common::EVENT_KEYDOWN:
switch (event.kbd.keycode) {
case Common::KEYCODE_RIGHT:
if (gDebugLevel >= 0) {
_game->scheduleEnteringRoomUsingGate(_game->nextRoomNum(), 0);
}
break;
case Common::KEYCODE_LEFT:
if (gDebugLevel >= 0) {
_game->scheduleEnteringRoomUsingGate(_game->prevRoomNum(), 0);
}
break;
default:
break;
}
break;
case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
switch (event.customType) {
case kActionEscape: {
if (_game->getLoopStatus() == kStatusInventory &&
_game->getLoopSubstatus() == kOuterLoop) {
_game->inventoryDone();
break;
}
const int escRoom = _game->getRoomNum() != _game->getMapRoom()
? _game->getEscRoom() : _game->getPreviousRoomNum();
// Check if there is an escape room defined for the current room
if (escRoom >= 0) {
// Schedule room change
// TODO: gate 0 (always present) is not always best for
// returning from the map, e.g. in the starting location.
// also, after loading the game, we shouldn't run any gate
// program, but rather restore the state of all objects.
_game->scheduleEnteringRoomUsingGate(escRoom, 0);
// Immediately cancel any running animation or dubbing and
// end any currently running GPL programs. In the intro it
// works as intended---skipping the rest of it.
//
// In the map, this causes that animation on newly
// discovered locations will be re-run next time and
// cut-scenes won't be played.
_game->setExitLoop(true);
_script->endCurrentProgram(true);
stopTextToSpeech();
}
break;
}
case kActionMap:
if (_game->getLoopStatus() == kStatusOrdinary) {
const int new_room = _game->getRoomNum() != _game->getMapRoom()
? _game->getMapRoom() : _game->getPreviousRoomNum();
_game->scheduleEnteringRoomUsingGate(new_room, 0);
}
break;
case kActionShowWalkMap:
// Show walking map toggle
_showWalkingMap = !_showWalkingMap;
_game->switchWalkingAnimations(_showWalkingMap);
break;
case kActionToggleWalkSpeed:
_game->setWantQuickHero(!_game->getWantQuickHero());
break;
case kActionInventory:
if (_game->getRoomNum() == _game->getMapRoom() ||
_game->getLoopSubstatus() != kOuterLoop) {
break;
}
if (_game->getLoopStatus() == kStatusInventory) {
_game->inventoryDone();
} else if (_game->getLoopStatus() == kStatusOrdinary) {
_game->inventoryInit();
}
break;
case kActionOpenMainMenu:
if (event.kbd.hasFlags(0)) {
openMainMenuDialog();
}
break;
case kActionTogglePointerItem:
case kActionInvRotatePrevious:
case kActionInvRotateNext:
if ((_game->getLoopStatus() == kStatusOrdinary ||
_game->getLoopStatus() == kStatusInventory) &&
_game->getLoopSubstatus() == kOuterLoop &&
_game->getRoomNum() != _game->getMapRoom()) {
_game->inventorySwitch(event.customType);
}
break;
default:
break;
}
break;
default:
_mouse->handleEvent(event);
}
}
// Handle EVENT_QUIT and EVENT_RETURN_TO_LAUNCHER.
if (shouldQuit()) {
_game->setQuit(true);
_script->endCurrentProgram(true);
}
}
DraciEngine::~DraciEngine() {
// Dispose your resources here
// If the common library supported Boost's scoped_ptr<>, then wrapping
// all the following pointers and many more would be appropriate. So
// far, there is only SharedPtr, which I feel being an overkill for
// easy deallocation.
// TODO: We have ScopedPtr nowadays. Maybe should adapt this code then?
delete _smallFont;
delete _bigFont;
delete _mouse;
delete _script;
delete _anims;
delete _game;
delete _screen;
delete _initArchive;
delete _paletteArchive;
delete _objectsArchive;
delete _spritesArchive;
delete _roomsArchive;
delete _overlaysArchive;
delete _animationsArchive;
delete _iconsArchive;
delete _walkingMapsArchive;
delete _itemsArchive;
delete _itemImagesArchive;
delete _stringsArchive;
delete _sound;
delete _music;
delete _soundsArchive;
delete _dubbingArchive;
}
Common::Error DraciEngine::run() {
init();
setTotalPlayTime(0);
_game->init();
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
if (ttsMan != nullptr) {
ttsMan->enable(ConfMan.getBool("tts_enabled_objects") || ConfMan.getBool("tts_enabled_speech") || ConfMan.getBool("tts_enabled_missing_voice"));
ttsMan->setLanguage(ConfMan.get("language"));
}
// Load game from specified slot, if any
if (ConfMan.hasKey("save_slot")) {
loadGameState(ConfMan.getInt("save_slot"));
}
_game->start();
return Common::kNoError;
}
void DraciEngine::pauseEngineIntern(bool pause) {
Engine::pauseEngineIntern(pause);
if (pause) {
_pauseStartTime = _system->getMillis();
_anims->pauseAnimations();
_sound->pauseSound();
_sound->pauseVoice();
_music->pause();
} else {
_anims->unpauseAnimations();
_sound->resumeSound();
_sound->resumeVoice();
_music->resume();
// Adjust engine start time
const int delta = _system->getMillis() - _pauseStartTime;
_game->shiftSpeechAndFadeTick(delta);
_pauseStartTime = 0;
}
}
void DraciEngine::syncSoundSettings() {
Engine::syncSoundSettings();
_sound->setVolume();
_music->syncVolume();
}
Common::String DraciEngine::getSavegameFile(int saveGameIdx) {
return Common::String::format("draci.s%02d", saveGameIdx);
}
Common::Error DraciEngine::loadGameState(int slot) {
// When called from run() using save_slot, the next operation is the
// call to start() calling enterNewRoom().
// When called from handleEvents() in the middle of the game, the next
// operation after handleEvents() exits from loop(), and returns to
// start() to the same place as above.
// In both cases, we are safe to override the data structures right
// here are now, without waiting for any other code to finish, thanks
// to our constraint in canLoadGameStateCurrently() and to having
// enterNewRoom() called right after we exit from here.
return loadSavegameData(slot, this);
}
bool DraciEngine::canLoadGameStateCurrently(Common::U32String *msg) {
return (_game->getLoopStatus() == kStatusOrdinary) &&
(_game->getLoopSubstatus() == kOuterLoop);
}
Common::Error DraciEngine::saveGameState(int slot, const Common::String &desc, bool isAutosave) {
return saveSavegameData(slot, desc, *this);
}
bool DraciEngine::canSaveGameStateCurrently(Common::U32String *msg) {
return (_game->getLoopStatus() == kStatusOrdinary) &&
(_game->getLoopSubstatus() == kOuterLoop);
}
void DraciEngine::sayText(const Common::String &text, bool isSubtitle) {
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
// _previousSaid is used to prevent the TTS from looping when sayText is called inside a loop,
// for example when the cursor stays on a dialog option. Without it when the text ends it would speak
// the same text again.
// _previousSaid is cleared when appropriate to allow for repeat requests
bool speak = (!isSubtitle && ConfMan.getBool("tts_enabled_objects") && _previousSaid != text) ||
(isSubtitle && (ConfMan.getBool("tts_enabled_speech") || ConfMan.getBool("tts_enabled_missing_voice")));
if (ttsMan != nullptr && speak) {
#ifdef USE_TTS
ttsMan->say(convertText(text), Common::TextToSpeechManager::INTERRUPT);
#endif
_previousSaid = text;
}
}
void DraciEngine::stopTextToSpeech() {
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
if (ttsMan != nullptr && (ConfMan.getBool("tts_enabled_objects") || ConfMan.getBool("tts_enabled_speech") || ConfMan.getBool("tts_enabled_missing_voice")) &&
ttsMan->isSpeaking()) {
ttsMan->stop();
_previousSaid.clear();
setTTSVoice(kBertID);
}
}
void DraciEngine::setTTSVoice(int characterID) const {
#ifdef USE_TTS
Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
if (ttsMan != nullptr && (ConfMan.getBool("tts_enabled_objects") || ConfMan.getBool("tts_enabled_speech") || ConfMan.getBool("tts_enabled_missing_voice"))) {
Common::Array<int> voices;
int pitch = 0;
Common::TTSVoice::Gender gender;
if (characterDialogData[characterID].male) {
voices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::MALE);
gender = Common::TTSVoice::MALE;
} else {
voices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::FEMALE);
gender = Common::TTSVoice::FEMALE;
}
// If no voice is available for the necessary gender, set the voice to default
if (voices.empty()) {
ttsMan->setVoice(0);
} else {
int voiceIndex = characterDialogData[characterID].voiceID % voices.size();
ttsMan->setVoice(voices[voiceIndex]);
}
// If no voices are available for this gender, alter the pitch to mimic a voice
// of the other gender
if (ttsMan->getVoice().getGender() != gender) {
if (gender == Common::TTSVoice::MALE) {
pitch -= 50;
} else {
pitch += 50;
}
}
ttsMan->setPitch(pitch);
}
#endif
}
#ifdef USE_TTS
Common::U32String DraciEngine::convertText(const Common::String &text) const {
const uint16 *translationTable;
if (getLanguage() == Common::PL_POL) {
translationTable = polishEncodingTable;
} else {
translationTable = kamenickyEncodingTable;
}
const byte *bytes = (const byte *)text.c_str();
byte *convertedBytes = new byte[text.size() * 2 + 1];
int i = 0;
for (const byte *b = bytes; *b; ++b) {
if (*b == 0x7c) { // Convert | to a space
convertedBytes[i] = 0x20;
i++;
continue;
}
if (*b < 0x80 || *b > 0xab) {
convertedBytes[i] = *b;
i++;
continue;
}
bool inTable = false;
for (int j = 0; translationTable[j]; ++j) {
if (*b - 0x80 == j) {
int convertedValue = translationTable[j];
if (translationTable[j] == 0xc3b4 && getLanguage() == Common::DE_DEU) {
// German encoding replaces ô with ß
convertedValue = 0xc39f;
} else if ((getLanguage() == Common::EN_ANY || getLanguage() == Common::DE_DEU) &&
translationTable[j] != czechCharacterConversionTable[j]) {
// Replace certain Czech characters for English and German TTS with close alternatives
// in those languages, for better TTS
convertedValue = czechCharacterConversionTable[j];
}
if (convertedValue <= 0xff) {
convertedBytes[i] = convertedValue;
i++;
} else {
convertedBytes[i] = (convertedValue >> 8) & 0xff;
convertedBytes[i + 1] = convertedValue & 0xff;
i += 2;
}
inTable = true;
break;
}
}
if (!inTable) {
convertedBytes[i] = *b;
i++;
}
}
convertedBytes[i] = 0;
Common::U32String result((char *)convertedBytes);
delete[] convertedBytes;
return result;
}
#endif
} // End of namespace Draci

202
engines/draci/draci.h Normal file
View File

@@ -0,0 +1,202 @@
/* 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 DRACI_DRACI_H
#define DRACI_DRACI_H
#include "engines/engine.h"
#include "common/random.h"
#include "draci/console.h"
struct ADGameDescription;
class MidiDriver;
class OSystem;
/**
* This is the namespace of the Draci engine.
*
* Status of this engine: Complete
*
* Games using this engine:
* - Dragon History
*/
namespace Draci {
enum DRACIAction {
kActionNone,
kActionEscape,
kActionMap,
kActionShowWalkMap,
kActionToggleWalkSpeed,
kActionInventory,
kActionOpenMainMenu,
kActionTogglePointerItem,
kActionInvRotatePrevious,
kActionInvRotateNext
};
#ifdef USE_TTS
struct CharacterDialogData {
uint8 voiceID;
bool male;
};
static const CharacterDialogData characterDialogData[] = {
{ 0, true }, // Bert
{ 1, true }, // Narrator
{ 2, true }, // Giant
{ 3, true }, // Captured dwarf
{ 4, true }, // Troll
{ 0, false }, // Berta
{ 5, true }, // Herbert
{ 1, false }, // Evelyn
{ 1, false }, // Evelyn
{ 2, false }, // Karmela
{ 6, true }, // King
{ 7, true }, // Wind
{ 8, true }, // Worm
{ 9, true }, // Pub dwarf
{ 10, true }, // Card player 2
{ 11, true }, // Card player 1
{ 12, true }, // Barkeeper
{ 13, true }, // Lazy man
{ 14, true }, // Goblin 1
{ 15, true }, // Goblin 2
{ 3, false }, // Chronicle
{ 16, true }, // Beggar
{ 17, true }, // Horn-blower
{ 18, true }, // Herbert
{ 19, true }, // Wizard
{ 20, true }, // Comedian
{ 21, true }, // Darter
{ 4, true }, // Troll
{ 21, true }, // Darter
{ 0, true }, // Skull
{ 22, true }, // Canary
{ 23, true }, // Oak-tree
{ 24, true }, // Beech-tree
{ 4, false }, // Agatha
{ 5, false }, // Eulanie
{ 25, true }, // Knight
{ 1, false }, // Evelyn
{ 20, true }, // Comedian
{ 1, true }, // Narrator
};
#endif
static const int kBertID = 0;
static const int kNarratorID = 1;
class Screen;
class Mouse;
class Game;
class Script;
class AnimationManager;
class Sound;
class MusicPlayer;
class Font;
class BArchive;
class SoundArchive;
class DraciEngine : public Engine {
public:
DraciEngine(OSystem *syst, const ADGameDescription *gameDesc);
~DraciEngine() override;
int init();
Common::Error run() override;
Common::Language getLanguage() const;
bool hasFeature(Engine::EngineFeature f) const override;
void pauseEngineIntern(bool pause) override;
void syncSoundSettings() override;
void handleEvents();
static Common::String getSavegameFile(int saveGameIdx);
Common::String getSaveStateName(int slot) const override { return getSavegameFile(slot); }
Common::Error loadGameState(int slot) override;
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
Common::Error saveGameState(int slot, const Common::String &desc, bool isAutosave = false) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override;
void sayText(const Common::String &text, bool isSubtitle = false);
void stopTextToSpeech();
void setTTSVoice(int characterID) const;
#ifdef USE_TTS
Common::U32String convertText(const Common::String &text) const;
#endif
const ADGameDescription *_gameDescription;
Screen *_screen;
Mouse *_mouse;
Game *_game;
Script *_script;
AnimationManager *_anims;
Sound *_sound;
MusicPlayer *_music;
Font *_smallFont;
Font *_bigFont;
Common::String _previousSaid;
BArchive *_iconsArchive;
BArchive *_objectsArchive;
BArchive *_spritesArchive;
BArchive *_paletteArchive;
BArchive *_roomsArchive;
BArchive *_overlaysArchive;
BArchive *_animationsArchive;
BArchive *_walkingMapsArchive;
BArchive *_itemsArchive;
BArchive *_itemImagesArchive;
BArchive *_initArchive;
BArchive *_stringsArchive;
SoundArchive *_soundsArchive;
SoundArchive *_dubbingArchive;
bool _showWalkingMap;
Common::RandomSource _rnd;
int32 _pauseStartTime;
};
enum {
kDraciGeneralDebugLevel = 1,
kDraciBytecodeDebugLevel,
kDraciArchiverDebugLevel,
kDraciLogicDebugLevel,
kDraciAnimationDebugLevel,
kDraciSoundDebugLevel,
kDraciWalkingDebugLevel,
};
} // End of namespace Draci
#endif // DRACI_DRACI_H

330
engines/draci/font.cpp Normal file
View File

@@ -0,0 +1,330 @@
/* 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 "draci/draci.h"
#include "draci/font.h"
#include "draci/surface.h"
namespace Draci {
const char * const kFontSmall = "Small.fon";
const char * const kFontBig = "Big.fon";
Font::Font(const Common::Path &filename) {
_fontHeight = 0;
_maxCharWidth = 0;
_charWidths = nullptr;
_charData = nullptr;
loadFont(filename);
}
Font::~Font() {
freeFont();
}
/**
* @brief Loads fonts from a file
* @param path Path to font file
* @return true if the font was loaded successfully, false otherwise
*
* Loads fonts from a file into a Font instance. The original game uses two
* fonts (located inside files "Small.fon" and "Big.fon"). The characters in the
* font are indexed from the space character so an appropriate offset must be
* added to convert them to equivalent char values, i.e. kDraciIndexOffset.
* Characters in the higher range are non-ASCII and vary between different
* language versions of the game.
*
* font format: [1 byte] maximum character width
* [1 byte] font height
* [138 bytes] character widths of all 138 characters in the font
* [138 * fontHeight * maxWidth bytes] character data, stored row-wise
*/
bool Font::loadFont(const Common::Path &filename) {
// Free previously loaded font (if any)
freeFont();
Common::File f;
f.open(filename);
if (f.isOpen()) {
debugC(6, kDraciGeneralDebugLevel, "Opened font file %s",
filename.toString(Common::Path::kNativeSeparator).c_str());
} else {
debugC(6, kDraciGeneralDebugLevel, "Error opening font file %s",
filename.toString(Common::Path::kNativeSeparator).c_str());
return false;
}
_maxCharWidth = f.readByte();
_fontHeight = f.readByte();
// Read in the widths of the glyphs
_charWidths = new uint8[kCharNum];
for (uint i = 0; i < kCharNum; ++i) {
_charWidths[i] = f.readByte();
}
// Calculate size of font data
uint fontDataSize = kCharNum * _maxCharWidth * _fontHeight;
// Read in all glyphs
_charData = new byte[fontDataSize];
f.read(_charData, fontDataSize);
debugC(5, kDraciGeneralDebugLevel, "Font %s loaded", filename.toString(Common::Path::kNativeSeparator).c_str());
return true;
}
void Font::freeFont() {
delete[] _charWidths;
delete[] _charData;
}
uint8 Font::getCharWidth(uint8 chr) const {
// Safe-guard against incorrect strings containing localized characters
// with inaccessible codes. These strings do not exist in the original
// Czech version, but they do in the (never properly reviewed) English
// version.
return chr >= kCharIndexOffset && chr < kCharIndexOffset + kCharNum
? _charWidths[chr - kCharIndexOffset] : 0;
}
/**
* @brief Draw a char to a Draci::Surface
*
* @param dst Pointer to the destination surface
* @param chr Character to draw
* @param tx Horizontal offset on the surface
* @param ty Vertical offset on the surface
*/
void Font::drawChar(Surface *dst, uint8 chr, int tx, int ty, int with_color) const {
assert(dst != nullptr);
assert(tx >= 0);
assert(ty >= 0);
byte *ptr = (byte *)dst->getBasePtr(tx, ty);
const uint8 currentWidth = getCharWidth(chr);
if (currentWidth == 0) {
return;
}
const uint8 charIndex = chr - kCharIndexOffset;
const int charOffset = charIndex * _fontHeight * _maxCharWidth;
// Determine how many pixels to draw horizontally (to prevent overflow)
int xSpaceLeft = dst->w - tx - 1;
int xPixelsToDraw = (currentWidth < xSpaceLeft) ? currentWidth : xSpaceLeft;
// Determine how many pixels to draw vertically
int ySpaceLeft = dst->h - ty - 1;
int yPixelsToDraw = (_fontHeight < ySpaceLeft) ? _fontHeight : ySpaceLeft;
int _transparent = dst->getTransparentColor();
for (int y = 0; y < yPixelsToDraw; ++y) {
for (int x = 0; x <= xPixelsToDraw; ++x) {
int curr = y * _maxCharWidth + x;
int color = _charData[charOffset + curr];
// If pixel is transparent, skip it
if (color == _transparent)
continue;
// Replace color with font colors
switch (color) {
case 254:
color = with_color;
break;
case 253:
color = kFontColor2;
break;
case 252:
color = kFontColor3;
break;
case 251:
color = kFontColor4;
break;
default:
break;
}
// Paint the pixel
ptr[x] = color;
}
// Advance to next row
ptr += dst->pitch;
}
}
/**
* @brief Draw a string to a Draci::Surface
*
* @param dst Pointer to the destination surface
* @param str Buffer containing string data
* @param len Length of the data
* @param x Horizontal offset on the surface
* @param y Vertical offset on the surface
* @param spacing Space to leave between individual characters. Defaults to 0.
*/
void Font::drawString(Surface *dst, const byte *str, uint len,
int x, int y, int with_color, int spacing, bool markDirty) const {
drawString(dst, Common::String((const char *)str, len), x, y, with_color, spacing, markDirty);
}
/**
* @brief Draw a string to a Draci::Surface
*
* @param dst Pointer to the destination surface
* @param str String to draw
* @param x Horizontal offset on the surface
* @param y Vertical offset on the surface
* @param spacing Space to leave between individual characters. Defaults to 0.
*/
void Font::drawString(Surface *dst, const Common::String &str,
int x, int y, int with_color, int spacing, bool markDirty) const {
assert(dst != nullptr);
assert(x >= 0);
assert(y >= 0);
uint widest = getStringWidth(str, spacing);
int curx = x + (widest - getLineWidth(str, 0, spacing)) / 2;
int cury = y;
for (uint i = 0; i < str.size(); ++i) {
// If we encounter the '|' char (newline and end of string marker),
// skip it and go to the start of the next line
if (str[i] == '|') {
cury += getFontHeight();
curx = x + (widest - getLineWidth(str, i+1, spacing) - 1) / 2;
continue;
}
// Break early if there's no more space on the screen
if (curx >= dst->w - 1 || cury >= dst->h - 1) {
break;
}
drawChar(dst, str[i], curx, cury, with_color);
curx += getCharWidth(str[i]) + spacing;
}
if (markDirty) {
Common::Rect r(x, y, x + widest, y + getStringHeight(str));
dst->markDirtyRect(r);
}
}
/**
* @brief Calculate the width of a string when drawn in the current font
*
* @param str String to draw
* @param spacing Space to leave between individual characters. Defaults to 0.
*
* @return The calculated width of the string
*/
uint Font::getStringWidth(const Common::String &str, int spacing) const {
uint width = 0;
// Real length, including '|' separators
uint len = str.size();
for (uint i = 0, tmp = 0; i < len; ++i) {
if (str[i] != '|') {
tmp += getCharWidth(str[i]) + spacing;
}
// Newline char encountered, skip it and store the new length if it is greater.
// Also, all strings in the data files should end with '|' but not all do.
// This is why we check whether we are at the last char too.
if (str[i] == '|' || i == len - 1) {
if (tmp > width) {
width = tmp;
}
tmp = 0;
}
}
return width + 1;
}
uint Font::getLineWidth(const Common::String &str, uint startIndex, int spacing) const {
uint width = 0;
// If the index is greater or equal to the string size,
// the width of the line is 0
if (startIndex >= str.size())
return 0;
for (uint i = startIndex; i < str.size(); ++i) {
// EOL encountered
if (str[i] == '|')
break;
// Add width of the current char
width += getCharWidth(str[i]) + spacing;
}
return width;
}
/**
* @brief Calculate the height of a string by counting the number of '|' chars (which
* are used as newline characters and end-of-string markers)
*
* @param str String to draw
* @param spacing Space to leave between individual characters. Defaults to 0.
*
* @return The calculated height of the string
*/
uint Font::getStringHeight(const Common::String &str) const {
uint len = str.size();
int separators = 0;
for (uint i = 0; i < len; ++i) {
// All strings in the data files should end with '|' but not all do.
// This is why we check whether we are at the last char too.
if (str[i] == '|' || i == len - 1) {
++separators;
}
}
return separators * getFontHeight();
}
} // End of namespace Draci

100
engines/draci/font.h Normal file
View File

@@ -0,0 +1,100 @@
/* 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 DRACI_FONT_H
#define DRACI_FONT_H
#include "graphics/font.h"
#include "common/path.h"
namespace Draci {
extern const char * const kFontSmall;
extern const char * const kFontBig;
class Surface;
/**
* Default font colors. They all seem to remain constant except for the
* first one which varies depending on the character speaking.
* kOverFontColor is set to transparent.
*/
enum {
kFontColor1 = 2,
kFontColor2 = 0,
kFontColor3 = 3,
kFontColor4 = 4,
kOverFontColor = 255,
kTitleColor = 255,
kLineActiveColor = 254,
kLineInactiveColor = 255
};
/**
* Represents the game's fonts. See docs for setFont() for font format details.
*/
class Font {
public:
Font(const Common::Path &filename);
~Font();
bool loadFont(const Common::Path &filename);
uint8 getFontHeight() const { return _fontHeight; }
uint8 getMaxCharWidth() const { return _maxCharWidth; }
uint8 getCharWidth(byte chr) const;
void drawChar(Surface *dst, uint8 chr, int tx, int ty, int with_color) const;
void drawString(Surface *dst, const byte *str, uint len, int x, int y, int with_color,
int spacing, bool markDirty) const;
void drawString(Surface *dst, const Common::String &str,
int x, int y, int with_color, int spacing, bool markDirty) const;
uint getStringWidth(const Common::String &str, int spacing) const;
uint getStringHeight(const Common::String &str) const;
uint getLineWidth(const Common::String &str, uint startIndex, int spacing) const;
private:
uint8 _fontHeight;
uint8 _maxCharWidth;
/** Pointer to an array of individual char widths */
uint8 *_charWidths;
/** Pointer to a raw byte array representing font pixels stored row-wise */
byte *_charData;
/** Number of glyphs in the font */
static const uint kCharNum = 138;
/**
* Chars are indexed from the space character so this should be subtracted
* to get the index of a glyph
*/
static const uint kCharIndexOffset = 32;
/** Internal function for freeing fonts when destructing/loading another */
void freeFont();
};
} // End of namespace Draci
#endif // DRACI_FONT_H

1906
engines/draci/game.cpp Normal file

File diff suppressed because it is too large Load Diff

437
engines/draci/game.h Normal file
View File

@@ -0,0 +1,437 @@
/* 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 DRACI_GAME_H
#define DRACI_GAME_H
#include "common/str.h"
#include "draci/script.h"
#include "draci/walking.h"
namespace Common {
class Serializer;
}
namespace Draci {
class BArchive;
class DraciEngine;
enum {
kDragonObject = 0
};
enum {
kDialogueLines = 4
};
enum {
kBlackPalette = -1
};
enum {
kMouseEnableSwitching = -1,
kMouseDoNotSwitch = -2
};
// Constants tuned such that with ScummVM's default talkspeed kStandardSpeed, the speed
// computed by equation (kBaseSpeechDuration + kSpeechTimeUnit * #characters) /
// talkspeed is equal to the original game.
enum SpeechConstants {
kBaseSpeechDuration = 12000,
kSpeechTimeUnit = 2640,
kStandardSpeed = 60
};
enum FadeConstants {
// One fading phase called from the game scripts is 50ms.
kFadingTimeUnit = 50,
// Fading in/out when entering/leaving a location takes 15 iterations of (at least) 7ms each.
kBlackFadingIterations = 15,
kBlackFadingTimeUnit = 7
};
enum AnimationConstants {
kTimeUnit = 20
};
/** Inventory related magical constants */
enum InventoryConstants {
kInventoryItemWidth = 25,
kInventoryItemHeight = 25,
kInventoryColumns = 7,
kInventoryLines = 5,
kInventoryX = 70, ///< Used for positioning of the inventory sprite on the X axis
kInventoryY = 30, ///< Used for positioning of the inventory sprite on the Y axis
kInventorySlots = kInventoryLines * kInventoryColumns,
kStatusChangeTimeout = 500
};
static const int kCreditsMapID = 46;
class GameObject {
public:
int _absNum;
uint _init, _look, _use, _canUse;
bool _imInit, _imLook, _imUse;
int _walkDir;
byte _z;
uint _lookX, _lookY, _useX, _useY;
SightDirection _lookDir, _useDir;
GPL2Program _program;
Common::String _title;
int _location;
bool _visible;
Common::Array<Animation *> _anim;
int _playingAnim;
int getAnim(int animID) const;
int addAnim(Animation *anim);
int playingAnim() const { return _playingAnim; }
void playAnim(int i);
void stopAnim();
void deleteAnims();
void deleteAnimsFrom(int index);
void load(uint objNum, BArchive *archive);
};
struct GameInfo {
int _startRoom;
int _mapRoom;
uint _numObjects;
uint _numItems;
byte _numVariables;
byte _numPersons;
byte _numDialogues;
uint _maxItemWidth, _maxItemHeight;
uint _musicLength;
uint _crc[4];
uint _numDialogueBlocks;
};
class GameItem {
public:
int _absNum;
uint _init, _look, _use, _canUse;
bool _imInit, _imLook, _imUse;
GPL2Program _program;
Common::String _title;
Animation *_anim;
void load(int itemID, BArchive *archive);
};
struct Person {
uint _x, _y;
byte _fontColor;
};
struct Dialogue {
int _canLen;
byte *_canBlock;
Common::String _title;
GPL2Program _program;
};
class Room {
public:
int _roomNum;
byte _music;
int _mapID;
int _palette;
int _numOverlays;
int _init, _look, _use, _canUse;
bool _imInit, _imLook, _imUse;
bool _mouseOn, _heroOn;
double _pers0, _persStep;
int _escRoom;
byte _numGates;
Common::Array<int> _gates;
GPL2Program _program;
void load(int roomNum, BArchive *archive);
};
enum LoopStatus {
kStatusOrdinary, // normal game-play: everything allowed
kStatusGate, // during running init-scripts when entering a room: disable interactivity
kStatusInventory, // inventory is open: cannot change the room or go to map
kStatusDialogue // during a dialogue: cannot change the room, go to inventory
};
enum LoopSubstatus {
kOuterLoop, // outer loop: everything is allowed
kInnerWhileTalk, // playing a voice: inner loop will exit afterwards
kInnerWhileFade, // fading a palette: inner loop will exit when done
kInnerDuringDialogue, // selecting continuation block: inner block will exit afterwards
kInnerUntilExit // other inner loop: either immediately exiting or waiting for an animation to end (whose callback ends the loop)
};
class Game {
public:
Game(DraciEngine *vm);
~Game();
void init();
void start();
void loop(LoopSubstatus substatus, bool shouldExit);
// HACK: this is only for testing
int nextRoomNum() const {
int n = _currentRoom._roomNum;
n = n < 37 ? n+1 : n;
return n;
}
// HACK: same as above
int prevRoomNum() const {
int n = _currentRoom._roomNum;
n = n > 0 ? n-1 : n;
return n;
}
Common::Point findNearestWalkable(int x, int y) const { return _walkingMap.findNearestWalkable(x, y); }
void heroAnimationFinished() { _walkingState.heroAnimationFinished(); }
void stopWalking() { _walkingState.stopWalking(); } // and clear callback
void walkHero(int x, int y, SightDirection dir); // start walking and leave callback as is
void setHeroPosition(const Common::Point &p);
const Common::Point &getHeroPosition() const { return _hero; }
const Common::Point &getHeroLoadingPosition() const { return _heroLoading; }
void positionAnimAsHero(Animation *anim);
void positionHeroAsAnim(Animation *anim);
// Makes sure animation anim_index plays on the hero. If the hero's
// position has changed, it updates the animation position. If the new
// animation is different, it stops the old one and starts the new one,
// otherwise it just marks dirty rectangles for moving the position.
// Returns the current animation phase of the new animation (usually 0
// unless the animation hasn't changed).
int playHeroAnimation(int anim_index);
void loadOverlays();
void loadWalkingMap(int mapID); // but leaves _currentRoom._mapID untouched
void switchWalkingAnimations(bool enabled);
uint getNumObjects() const { return _info._numObjects; }
GameObject *getObject(uint objNum) { return _objects + objNum; }
const GameObject *getObjectWithAnimation(const Animation *anim) const;
void deleteObjectAnimations();
void deleteAnimationsAfterIndex(int lastAnimIndex);
int getVariable(int varNum) const { return _variables[varNum]; }
void setVariable(int varNum, int value) { _variables[varNum] = value; }
const Person *getPerson(int personID) const { return &_persons[personID]; }
int getRoomNum() const { return _currentRoom._roomNum; }
void setRoomNum(int num) { _currentRoom._roomNum = num; }
int getPreviousRoomNum() const { return _previousRoom; }
void rememberRoomNumAsPrevious() { _previousRoom = getRoomNum(); }
void scheduleEnteringRoomUsingGate(int room, int gate) { _newRoom = room; _newGate = gate; }
void pushNewRoom();
void popNewRoom();
double getPers0() const { return _currentRoom._pers0; }
double getPersStep() const { return _currentRoom._persStep; }
int getMusicTrack() const { return _currentRoom._music; }
void setMusicTrack(int num) { _currentRoom._music = num; }
int getItemStatus(int itemID) const { return _itemStatus[itemID]; }
void setItemStatus(int itemID, int status) { _itemStatus[itemID] = status; }
GameItem *getItem(int id) { return id >= 0 && id < (int)_info._numItems ? &_items[id] : NULL; }
GameItem *getCurrentItem() const { return _currentItem; }
void setCurrentItem(GameItem *item) { _currentItem = item; }
int getPreviousItemPosition() const { return _previousItemPosition; }
void setPreviousItemPosition(int pos) { _previousItemPosition = pos; }
void removeItem(GameItem *item);
void loadItemAnimation(GameItem *item);
void putItem(GameItem *item, int position);
void addItem(int itemID);
int getEscRoom() const { return _currentRoom._escRoom; }
int getMapRoom() const { return _info._mapRoom; }
int getMapID() const { return _currentRoom._mapID; }
/**
* The GPL command Mark sets the animation index (which specifies the
* order in which animations were loaded in) which is then used by the
* Release command to delete all animations that have an index greater
* than the one marked.
*/
int getMarkedAnimationIndex() const { return _markedAnimationIndex; }
void setMarkedAnimationIndex(int index) { _markedAnimationIndex = index; }
void setLoopStatus(LoopStatus status) { _loopStatus = status; }
void setLoopSubstatus(LoopSubstatus status) { _loopSubstatus = status; }
LoopStatus getLoopStatus() const { return _loopStatus; }
LoopSubstatus getLoopSubstatus() const { return _loopSubstatus; }
bool gameShouldQuit() const { return _shouldQuit; }
void setQuit(bool quit) { _shouldQuit = quit; }
bool shouldExitLoop() const { return _shouldExitLoop; }
void setExitLoop(bool exit) { _shouldExitLoop = exit; }
bool isReloaded() const { return _isReloaded; }
void setIsReloaded(bool value) { _isReloaded = value; }
bool isPositionLoaded() { return _isPositionLoaded; }
void setPositionLoaded(bool value) { _isPositionLoaded = value; }
void setSpeechTiming(uint tick, uint duration);
void shiftSpeechAndFadeTick(int delta);
void inventoryInit();
void inventoryDraw();
void inventoryDone();
void inventoryReload();
void inventorySwitch(int action);
void dialogueMenu(int dialogueID);
int dialogueDraw();
void dialogueInit(int dialogID);
void dialogueDone();
bool isDialogueBegin() const { return _dialogueBegin; }
bool shouldExitDialogue() const { return _dialogueExit; }
void setDialogueExit(bool exit) { _dialogueExit = exit; }
int getDialogueBlockNum() const { return _blockNum; }
int getDialogueVar(int dialogueID) const { return _dialogueVars[dialogueID]; }
void setDialogueVar(int dialogueID, int value) { _dialogueVars[dialogueID] = value; }
int getCurrentDialogue() const { return _currentDialogue; }
int getDialogueCurrentBlock() const { return _currentBlock; }
int getDialogueLastBlock() const { return _lastBlock; }
int getDialogueLinesNum() const { return _dialogueLinesNum; }
int getCurrentDialogueOffset() const { return _dialogueOffsets[_currentDialogue]; }
void schedulePalette(int paletteID) { _scheduledPalette = paletteID; }
int getScheduledPalette() const { return _scheduledPalette; }
void initializeFading(int phases);
void setEnableQuickHero(bool value) { _enableQuickHero = value; }
bool getEnableQuickHero() const { return _enableQuickHero; }
void setWantQuickHero(bool value) { _wantQuickHero = value; }
bool getWantQuickHero() const { return _wantQuickHero; }
void setEnableSpeedText(bool value) { _enableSpeedText = value; }
bool getEnableSpeedText() const { return _enableSpeedText; }
void synchronize(Common::Serializer &s, uint8 saveVersion);
private:
void updateOrdinaryCursor();
void updateInventoryCursor();
int inventoryPositionFromMouse() const;
void handleOrdinaryLoop(int x, int y);
void handleInventoryLoop();
void handleDialogueLoop();
void updateTitle(int x, int y);
void updateCursor();
void fadePalette(bool fading_out);
void advanceAnimationsAndTestLoopExit();
void handleStatusChangeByMouse();
void enterNewRoom();
void initWalkingOverlays();
void loadRoomObjects();
void redrawWalkingPath(Animation *anim, byte color, const WalkingPath &path);
DraciEngine *_vm;
GameInfo _info;
Common::Point _hero;
Common::Point _heroLoading;
Common::Point _lastTarget;
int *_variables;
Person *_persons;
GameObject *_objects;
byte *_itemStatus;
GameItem *_items;
GameItem *_currentItem;
GameItem *_itemUnderCursor;
// Last position in the inventory of the item currently in the hands, resp. of the item that
// was last in our hands.
int _previousItemPosition;
GameItem *_inventory[kInventorySlots];
Room _currentRoom;
int _newRoom;
int _newGate;
int _previousRoom;
int _pushedNewRoom; // used in GPL programs
int _pushedNewGate;
uint *_dialogueOffsets;
int _currentDialogue;
int *_dialogueVars;
BArchive *_dialogueArchive;
Dialogue *_dialogueBlocks;
bool _dialogueBegin;
bool _dialogueExit;
int _currentBlock;
int _lastBlock;
int _dialogueLinesNum;
int _blockNum;
int _lines[kDialogueLines];
Animation *_dialogueAnims[kDialogueLines];
LoopStatus _loopStatus;
LoopSubstatus _loopSubstatus;
bool _shouldQuit;
bool _shouldExitLoop;
bool _isReloaded;
bool _isPositionLoaded;
uint _speechTick;
uint _speechDuration;
const GameObject *_objUnderCursor;
const Animation *_animUnderCursor;
int _markedAnimationIndex; ///< Used by the Mark GPL command
int _scheduledPalette;
int _fadePhases;
int _fadePhase;
uint _fadeTick;
bool _isFadeOut;
int _mouseChangeTick;
bool _enableQuickHero;
bool _wantQuickHero;
bool _enableSpeedText;
WalkingMap _walkingMap;
WalkingState _walkingState;
Animation *_titleAnim;
Animation *_inventoryAnim;
Animation *_walkingMapOverlay;
Animation *_walkingShortestPathOverlay;
Animation *_walkingObliquePathOverlay;
};
} // End of namespace Draci
#endif // DRACI_GAME_H

View File

@@ -0,0 +1,279 @@
/* 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 "draci/draci.h"
#include "draci/saveload.h"
#include "draci/detection.h"
#include "backends/keymapper/action.h"
#include "backends/keymapper/keymapper.h"
#include "backends/keymapper/standard-actions.h"
#include "base/plugins.h"
#include "common/system.h"
#include "common/translation.h"
#include "engines/advancedDetector.h"
#include "engines/metaengine.h"
namespace Draci {
#ifdef USE_TTS
static const ADExtraGuiOptionsMap optionsList[] = {
{
GAMEOPTION_TTS_OBJECTS,
{
_s("Enable Text to Speech for Objects and Options"),
_s("Use TTS to read the descriptions (if TTS is available)"),
"tts_enabled_objects",
false,
0,
0
}
},
{
GAMEOPTION_TTS_SPEECH,
{
_s("Enable Text to Speech for Subtitles"),
_s("Use TTS to read the subtitles (if TTS is available)"),
"tts_enabled_speech",
false,
0,
0
}
},
{
GAMEOPTION_TTS_MISSING_VOICE,
{
_s("Enable Text to Speech for Missing Voiceovers"),
_s("Use TTS to read the subtitles of missing voiceovers (if TTS is available)"),
"tts_enabled_missing_voice",
false,
0,
0
}
},
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
#endif
Common::Language DraciEngine::getLanguage() const {
return _gameDescription->language;
}
} // End of namespace Draci
class DraciMetaEngine : public AdvancedMetaEngine<ADGameDescription> {
public:
const char *getName() const override {
return "draci";
}
#ifdef USE_TTS
const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
return Draci::optionsList;
}
#endif
bool hasFeature(MetaEngineFeature f) const override;
int getMaximumSaveSlot() const override { return 99; }
SaveStateList listSaves(const char *target) const override;
bool removeSaveState(const char *target, int slot) const override;
SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const override;
Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override;
Common::KeymapArray initKeymaps(const char *target) const override;
};
bool DraciMetaEngine::hasFeature(MetaEngineFeature f) const {
return
(f == kSupportsListSaves) ||
(f == kSupportsDeleteSave) ||
(f == kSavesSupportMetaInfo) ||
(f == kSavesSupportThumbnail) ||
(f == kSavesSupportCreationDate) ||
(f == kSavesSupportPlayTime) ||
(f == kSupportsLoadingDuringStartup);
}
SaveStateList DraciMetaEngine::listSaves(const char *target) const {
Common::SaveFileManager *saveFileMan = g_system->getSavefileManager();
Common::String pattern("draci.s##");
Common::StringArray filenames = saveFileMan->listSavefiles(pattern);
SaveStateList saveList;
for (const auto &filename : filenames) {
// Obtain the last 2 digits of the filename, since they correspond to the save slot
int slotNum = atoi(filename.c_str() + filename.size() - 2);
if (slotNum >= 0 && slotNum <= 99) {
Common::InSaveFile *in = saveFileMan->openForLoading(filename);
if (in) {
Draci::DraciSavegameHeader header;
if (Draci::readSavegameHeader(in, header)) {
saveList.push_back(SaveStateDescriptor(this, slotNum, header.saveName));
}
delete in;
}
}
}
// Sort saves based on slot number.
Common::sort(saveList.begin(), saveList.end(), SaveStateDescriptorSlotComparator());
return saveList;
}
bool DraciMetaEngine::removeSaveState(const char *target, int slot) const {
return g_system->getSavefileManager()->removeSavefile(Draci::DraciEngine::getSavegameFile(slot));
}
SaveStateDescriptor DraciMetaEngine::querySaveMetaInfos(const char *target, int slot) const {
Common::InSaveFile *f = g_system->getSavefileManager()->openForLoading(
Draci::DraciEngine::getSavegameFile(slot));
if (f) {
Draci::DraciSavegameHeader header;
if (!Draci::readSavegameHeader(f, header, false)) {
delete f;
return SaveStateDescriptor();
}
delete f;
// Create the return descriptor
SaveStateDescriptor desc(this, slot, header.saveName);
desc.setThumbnail(header.thumbnail);
int day = (header.date >> 24) & 0xFF;
int month = (header.date >> 16) & 0xFF;
int year = header.date & 0xFFFF;
desc.setSaveDate(year, month, day);
int hour = (header.time >> 8) & 0xFF;
int minutes = header.time & 0xFF;
desc.setSaveTime(hour, minutes);
desc.setPlayTime(header.playtime * 1000);
return desc;
}
return SaveStateDescriptor();
}
Common::Error DraciMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
*engine = new Draci::DraciEngine(syst, desc);
return Common::kNoError;
}
Common::KeymapArray DraciMetaEngine::initKeymaps(const char *target) const {
using namespace Common;
using namespace Draci;
Keymap *engineKeyMap = new Keymap(Keymap::kKeymapTypeGame, "draci-default", _("Default keymappings"));
Keymap *gameKeyMap = new Keymap(Keymap::kKeymapTypeGame, "game-shortcuts", _("Game keymappings"));
Action *act;
act = new Action(kStandardActionLeftClick, _("Left click"));
act->setLeftClickEvent();
act->addDefaultInputMapping("MOUSE_LEFT");
act->addDefaultInputMapping("JOY_A");
engineKeyMap->addAction(act);
act = new Action(kStandardActionRightClick, _("Right click"));
act->setRightClickEvent();
act->addDefaultInputMapping("MOUSE_RIGHT");
act->addDefaultInputMapping("JOY_B");
engineKeyMap->addAction(act);
act = new Action("ESCAPE", _("Skip intro / Exit map or inventory"));
act->setCustomEngineActionEvent(kActionEscape);
act->addDefaultInputMapping("ESCAPE");
act->addDefaultInputMapping("JOY_LEFT_SHOULDER");
gameKeyMap->addAction(act);
act = new Action("MAP", _("Open map"));
act->setCustomEngineActionEvent(kActionMap);
act->addDefaultInputMapping("m");
act->addDefaultInputMapping("JOY_A");
gameKeyMap->addAction(act);
// I18N: shows where the game actor is able to move
act = new Action("WALKMAP", _("Show walking map"));
act->setCustomEngineActionEvent(kActionShowWalkMap);
act->addDefaultInputMapping("w");
act->addDefaultInputMapping("JOY_LEFT_STICK");
gameKeyMap->addAction(act);
act = new Action("TOGGLEWALKSPEED", _("Toggle walk speed"));
act->setCustomEngineActionEvent(kActionToggleWalkSpeed);
act->addDefaultInputMapping("q");
act->addDefaultInputMapping("JOY_RIGHT_STICK");
gameKeyMap->addAction(act);
act = new Action("INV", _("Inventory"));
act->setCustomEngineActionEvent(kActionInventory);
act->addDefaultInputMapping("i");
act->addDefaultInputMapping("JOY_B");
gameKeyMap->addAction(act);
act = new Action("MAINMENU", _("Open main menu"));
act->setCustomEngineActionEvent(kActionOpenMainMenu);
act->addDefaultInputMapping("F5");
act->addDefaultInputMapping("JOY_RIGHT_SHOULDER");
gameKeyMap->addAction(act);
act = new Action("POINTERORITEM", _("Toggle between mouse pointer and the last game item"));
act->setCustomEngineActionEvent(kActionTogglePointerItem);
act->addDefaultInputMapping("SLASH");
act->addDefaultInputMapping("JOY_LEFT");
gameKeyMap->addAction(act);
act = new Action("PREVITEM", _("Previous item in inventory"));
act->setCustomEngineActionEvent(kActionInvRotatePrevious);
act->addDefaultInputMapping("COMMA");
act->addDefaultInputMapping("JOY_RIGHT");
gameKeyMap->addAction(act);
act = new Action("NEXTITEM", _("Next item in inventory"));
act->setCustomEngineActionEvent(kActionInvRotateNext);
act->addDefaultInputMapping("PERIOD");
act->addDefaultInputMapping("JOY_UP");
gameKeyMap->addAction(act);
KeymapArray keymaps(2);
keymaps[0] = engineKeyMap;
keymaps[1] = gameKeyMap;
return keymaps;
}
#if PLUGIN_ENABLED_DYNAMIC(DRACI)
REGISTER_PLUGIN_DYNAMIC(DRACI, PLUGIN_TYPE_ENGINE, DraciMetaEngine);
#else
REGISTER_PLUGIN_STATIC(DRACI, PLUGIN_TYPE_ENGINE, DraciMetaEngine);
#endif

30
engines/draci/module.mk Normal file
View File

@@ -0,0 +1,30 @@
MODULE := engines/draci
MODULE_OBJS := \
animation.o \
barchive.o \
console.o \
draci.o \
font.o \
game.o \
metaengine.o \
mouse.o \
music.o \
saveload.o \
screen.o \
script.o \
sound.o \
sprite.o \
surface.o \
walking.o
# This module can be built as a plugin
ifeq ($(ENABLE_DRACI), DYNAMIC_PLUGIN)
PLUGIN := 1
endif
# Include common rules
include $(srcdir)/rules.mk
# Detection objects
DETECT_OBJS += $(MODULE)/detection.o

128
engines/draci/mouse.cpp Normal file
View File

@@ -0,0 +1,128 @@
/* 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 "draci/draci.h"
#include "draci/game.h"
#include "draci/mouse.h"
#include "draci/barchive.h"
#include "draci/screen.h"
#include "draci/sprite.h"
#include "graphics/cursorman.h"
#include "common/system.h"
namespace Draci {
Mouse::Mouse(DraciEngine *vm) {
_x = 0;
_y = 0;
_lButton = false;
_rButton = false;
_cursorType = kUninitializedCursor;
_vm = vm;
}
void Mouse::handleEvent(Common::Event event) {
switch (event.type) {
case Common::EVENT_LBUTTONDOWN:
debugC(6, kDraciGeneralDebugLevel, "Left button down (x: %u y: %u)", _x, _y);
_lButton = true;
break;
case Common::EVENT_LBUTTONUP:
debugC(6, kDraciGeneralDebugLevel, "Left button up (x: %u y: %u)", _x, _y);
// Don't set _lButton to false, because some touchpads generate
// down and up at such a quick succession, that they will
// cancel each other in the same call of handleEvents(). Let
// the game clear this flag by calling lButtonSet() instead.
break;
case Common::EVENT_RBUTTONDOWN:
debugC(6, kDraciGeneralDebugLevel, "Right button down (x: %u y: %u)", _x, _y);
_rButton = true;
break;
case Common::EVENT_RBUTTONUP:
debugC(6, kDraciGeneralDebugLevel, "Right button up (x: %u y: %u)", _x, _y);
break;
case Common::EVENT_MOUSEMOVE:
debugC(6, kDraciGeneralDebugLevel, "Mouse move (x: %u y: %u)", _x, _y);
_x = (uint16) event.mouse.x;
_y = (uint16) event.mouse.y;
break;
default:
break;
}
}
void Mouse::cursorOn() {
CursorMan.showMouse(true);
}
void Mouse::cursorOff() {
CursorMan.showMouse(false);
}
bool Mouse::isCursorOn() const {
return CursorMan.isVisible();
}
void Mouse::setPosition(uint16 x, uint16 y) {
_vm->_system->warpMouse(x, y);
}
void Mouse::setCursorType(CursorType cur) {
if (cur == getCursorType()) {
return;
}
_cursorType = cur;
const BAFile *f;
f = _vm->_iconsArchive->getFile(cur);
Sprite sp(f->_data, f->_length, 0, 0, true);
CursorMan.replaceCursorPalette(_vm->_screen->getPalette(), 0, kNumColors);
CursorMan.replaceCursor(sp.getBuffer(), sp.getWidth(), sp.getHeight(),
sp.getWidth() / 2, sp.getHeight() / 2, 255);
}
void Mouse::loadItemCursor(const GameItem *item, bool highlighted) {
const int itemID = item->_absNum;
const int archiveIndex = 2 * itemID + (highlighted ? 1 : 0);
CursorType newCursor = static_cast<CursorType> (kItemCursor + archiveIndex);
if (newCursor == getCursorType()) {
return;
}
_cursorType = newCursor;
const BAFile *f;
f = _vm->_itemImagesArchive->getFile(archiveIndex);
Sprite sp(f->_data, f->_length, 0, 0, true);
CursorMan.replaceCursorPalette(_vm->_screen->getPalette(), 0, kNumColors);
CursorMan.replaceCursor(sp.getBuffer(), sp.getWidth(), sp.getHeight(),
sp.getWidth() / 2, sp.getHeight() / 2, 255);
}
} // End of namespace Draci

75
engines/draci/mouse.h Normal file
View File

@@ -0,0 +1,75 @@
/* 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 DRACI_MOUSE_H
#define DRACI_MOUSE_H
#include "common/events.h"
namespace Draci {
enum CursorType {
kNormalCursor,
kArrowCursor1,
kArrowCursor2,
kArrowCursor3,
kArrowCursor4,
kDialogueCursor,
kHighlightedCursor,
kMainMenuCursor,
kUninitializedCursor = 100,
kItemCursor // + the index in the BArchive
};
class DraciEngine;
class GameItem;
class Mouse {
public:
Mouse(DraciEngine *vm);
~Mouse() {}
void handleEvent(Common::Event event);
void cursorOn();
void cursorOff();
bool isCursorOn() const;
void setPosition(uint16 x, uint16 y);
CursorType getCursorType() const { return _cursorType; }
void setCursorType(CursorType cur);
void loadItemCursor(const GameItem *item, bool highlighted);
bool lButtonPressed() const { return _lButton; }
bool rButtonPressed() const { return _rButton; }
void lButtonSet(bool state) { _lButton = state; }
void rButtonSet(bool state) { _rButton = state; }
uint16 getPosX() const { return _x; }
uint16 getPosY() const { return _y; }
private:
uint16 _x, _y;
bool _lButton, _rButton;
CursorType _cursorType;
DraciEngine *_vm;
};
} // End of namespace Draci
#endif // DRACI_MOUSE_H

119
engines/draci/music.cpp Normal file
View File

@@ -0,0 +1,119 @@
/* 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/>.
*
*/
// MIDI and digital music class
#include "audio/mididrv.h"
#include "audio/midiparser.h"
#include "common/debug.h"
#include "common/file.h"
#include "draci/draci.h"
#include "draci/music.h"
namespace Draci {
MusicPlayer::MusicPlayer(const char *pathMask) : _pathMask(pathMask), _isGM(false), _track(-1) {
MidiPlayer::createDriver();
int ret = _driver->open();
if (ret == 0) {
if (_nativeMT32)
_driver->sendMT32Reset();
else
_driver->sendGMReset();
// TODO: Load cmf.ins with the instrument table. It seems that an
// interface for such an operation is supported for AdLib. Maybe for
// this card, setting instruments is necessary.
_driver->setTimerCallback(this, &timerCallback);
}
}
void MusicPlayer::sendToChannel(byte channel, uint32 b) {
if (!_channelsTable[channel]) {
_channelsTable[channel] = (channel == 15) ? _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])
_channelsTable[channel]->send(b);
}
void MusicPlayer::playSMF(int track, bool loop) {
Common::StackLock lock(_mutex);
if (_isPlaying && track == _track) {
debugC(2, kDraciSoundDebugLevel, "Already plaing track %d", track);
return;
}
stop();
_isGM = true;
// Load MIDI resource data
Common::File musicFile;
Common::String musicFileName = Common::String::format(_pathMask.c_str(), track);
musicFile.open(musicFileName.c_str());
if (!musicFile.isOpen()) {
debugC(2, kDraciSoundDebugLevel, "Cannot open track %d", track);
return;
}
int midiMusicSize = musicFile.size();
free(_midiData);
_midiData = (byte *)malloc(midiMusicSize);
musicFile.read(_midiData, midiMusicSize);
musicFile.close();
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();
_isLooping = loop;
_isPlaying = true;
_track = track;
debugC(2, kDraciSoundDebugLevel, "Playing track %d", track);
} else {
debugC(2, kDraciSoundDebugLevel, "Cannot play track %d", track);
delete parser;
}
}
void MusicPlayer::stop() {
Audio::MidiPlayer::stop();
debugC(2, kDraciSoundDebugLevel, "Stopping track %d", _track);
_track = -1;
}
} // End of namespace Draci

52
engines/draci/music.h Normal file
View 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/>.
*
*/
// Music class
#ifndef DRACI_MUSIC_H
#define DRACI_MUSIC_H
#include "audio/midiplayer.h"
namespace Draci {
// Taken from MADE, which took it from SAGA.
class MusicPlayer : public Audio::MidiPlayer {
public:
MusicPlayer(const char *pathMask);
void playSMF(int track, bool loop);
void stop() override;
// Overload Audio::MidiPlayer method
void sendToChannel(byte channel, uint32 b) override;
protected:
Common::String _pathMask;
bool _isGM;
int _track;
};
} // End of namespace Draci
#endif

153
engines/draci/saveload.cpp Normal file
View File

@@ -0,0 +1,153 @@
/* 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 "draci/draci.h"
#include "draci/game.h"
#include "draci/saveload.h"
#include "common/serializer.h"
#include "common/savefile.h"
#include "common/system.h"
#include "graphics/scaler.h"
#include "graphics/thumbnail.h"
namespace Draci {
static const char *const draciIdentString = "DRACI";
WARN_UNUSED_RESULT bool readSavegameHeader(Common::InSaveFile *in, DraciSavegameHeader &header, bool skipThumbnail) {
char saveIdentBuffer[6];
// Validate the header Id
in->read(saveIdentBuffer, 6);
if (strcmp(saveIdentBuffer, draciIdentString) != 0)
return false;
header.version = in->readByte();
// Version 1 is compatible with Version 2
if (header.version > DRACI_SAVEGAME_VERSION)
return false;
// Read in the string
header.saveName.clear();
char ch;
while ((ch = (char)in->readByte()) != '\0') header.saveName += ch;
header.date = in->readUint32LE();
header.time = in->readUint16LE();
header.playtime = in->readUint32LE();
// Get the thumbnail
if (!Graphics::loadThumbnail(*in, header.thumbnail, skipThumbnail)) {
return false;
}
return true;
}
void writeSavegameHeader(Common::OutSaveFile *out, const DraciSavegameHeader &header) {
// Write out a savegame header
out->write(draciIdentString, 6);
out->writeByte(DRACI_SAVEGAME_VERSION);
// Write savegame name
out->write(header.saveName.c_str(), header.saveName.size() + 1);
out->writeUint32LE(header.date);
out->writeUint16LE(header.time);
out->writeUint32LE(header.playtime);
// Create a thumbnail and save it
Graphics::saveThumbnail(*out);
}
Common::Error saveSavegameData(int saveGameIdx, const Common::String &saveName, DraciEngine &vm) {
Common::String filename = vm.getSavegameFile(saveGameIdx);
Common::SaveFileManager *saveMan = g_system->getSavefileManager();
Common::OutSaveFile *f = saveMan->openForSaving(filename);
if (f == nullptr)
return Common::kNoGameDataFoundError;
TimeDate curTime;
vm._system->getTimeAndDate(curTime);
// Save the savegame header
DraciSavegameHeader header;
header.saveName = saveName;
header.date = ((curTime.tm_mday & 0xFF) << 24) | (((curTime.tm_mon + 1) & 0xFF) << 16) | ((curTime.tm_year + 1900) & 0xFFFF);
header.time = ((curTime.tm_hour & 0xFF) << 8) | ((curTime.tm_min) & 0xFF);
header.playtime = vm.getTotalPlayTime() / 1000;
writeSavegameHeader(f, header);
if (f->err()) {
delete f;
saveMan->removeSavefile(filename);
return Common::kWritingFailed;
} else {
// Create the remainder of the savegame
Common::Serializer s(nullptr, f);
vm._game->synchronize(s, header.version);
f->finalize();
delete f;
return Common::kNoError;
}
}
Common::Error loadSavegameData(int saveGameIdx, DraciEngine *vm) {
Common::String saveName;
Common::SaveFileManager *saveMan = g_system->getSavefileManager();
Common::InSaveFile *f = saveMan->openForLoading(vm->getSavegameFile(saveGameIdx));
if (f == nullptr) {
return Common::kNoGameDataFoundError;
}
// Skip over the savegame header
DraciSavegameHeader header;
if (!readSavegameHeader(f, header)) {
return Common::kNoGameDataFoundError;
}
// Pre-processing
vm->_game->rememberRoomNumAsPrevious();
vm->_game->deleteObjectAnimations();
// Synchronise the remaining data of the savegame
Common::Serializer s(f, nullptr);
vm->_game->synchronize(s, header.version);
delete f;
// Post-processing
vm->_game->scheduleEnteringRoomUsingGate(vm->_game->getRoomNum(), 0);
vm->_game->setExitLoop(true);
vm->_game->setIsReloaded(true);
vm->_game->inventoryReload();
vm->setTotalPlayTime(header.playtime * 1000);
return Common::kNoError;
}
} // End of namespace Draci

51
engines/draci/saveload.h Normal file
View File

@@ -0,0 +1,51 @@
/* 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 DRACI_SAVELOAD_H
#define DRACI_SAVELOAD_H
#include "common/savefile.h"
#include "common/scummsys.h"
#include "graphics/surface.h"
namespace Draci {
#define DRACI_SAVEGAME_VERSION 2
struct DraciSavegameHeader {
uint8 version;
Common::String saveName;
uint32 date;
uint16 time;
uint32 playtime;
Graphics::Surface *thumbnail;
};
class DraciEngine;
WARN_UNUSED_RESULT bool readSavegameHeader(Common::InSaveFile *in, DraciSavegameHeader &header, bool skipThumbnail = true);
void writeSavegameHeader(Common::OutSaveFile *out, const DraciSavegameHeader &header);
Common::Error saveSavegameData(int saveGameIdx, const Common::String &saveName, DraciEngine &vm);
Common::Error loadSavegameData(int saveGameIdx, DraciEngine *vm);
} // End of namespace Draci
#endif

145
engines/draci/screen.cpp Normal file
View File

@@ -0,0 +1,145 @@
/* 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/memstream.h"
#include "common/system.h"
#include "graphics/paletteman.h"
#include "draci/draci.h"
#include "draci/screen.h"
#include "draci/surface.h"
#include "draci/sprite.h"
namespace Draci {
Screen::Screen(DraciEngine *vm) : _vm(vm) {
_surface = new Surface(kScreenWidth, kScreenHeight);
_palette = new byte[3 * kNumColors];
_blackPalette = new byte[3 * kNumColors];
for (int i = 0; i < 3 * kNumColors; ++i) {
_blackPalette[i] = 0;
}
setPalette(nullptr, 0, kNumColors);
this->clearScreen();
}
Screen::~Screen() {
delete _surface;
delete[] _palette;
delete[] _blackPalette;
}
/**
* @brief Sets a part of the palette
* @param data Pointer to a buffer containing new palette data
* start Index of the color where replacement should start
* num Number of colors to replace
*/
void Screen::setPalette(const byte *data, uint16 start, uint16 num) {
Common::MemoryReadStream pal(data ? data : _blackPalette, 3 * kNumColors);
pal.seek(start * 3);
// Copy the palette
for (uint16 i = start; i < start + num; ++i) {
_palette[i * 3] = pal.readByte();
_palette[i * 3 + 1] = pal.readByte();
_palette[i * 3 + 2] = pal.readByte();
}
// Shift the palette two bits to the left to make it brighter. The
// original game only uses 6-bit colors 0..63.
for (int i = start * 3; i < (start + num) * 3; ++i) {
_palette[i] <<= 2;
}
_vm->_system->getPaletteManager()->setPalette(_palette, start, num);
}
void Screen::interpolatePalettes(const byte *first, const byte *second, uint16 start, uint16 num, int index, int number) {
Common::MemoryReadStream firstPal(first ? first : _blackPalette, 3 * kNumColors);
Common::MemoryReadStream secondPal(second ? second : _blackPalette, 3 * kNumColors);
firstPal.seek(start * 3);
secondPal.seek(start * 3);
// Interpolate the palettes
for (uint16 i = start; i < start + num; ++i) {
_palette[i * 3] = interpolate(firstPal.readByte(), secondPal.readByte(), index, number);
_palette[i * 3 + 1] = interpolate(firstPal.readByte(), secondPal.readByte(), index, number);
_palette[i * 3 + 2] = interpolate(firstPal.readByte(), secondPal.readByte(), index, number);
}
// Shift the palette two bits to the left to make it brighter
for (int i = start * 3; i < (start + num) * 3; ++i) {
_palette[i] <<= 2;
}
_vm->_system->getPaletteManager()->setPalette(_palette, start, num);
}
int Screen::interpolate(int first, int second, int index, int number) {
return (second * index + first * (number - index)) / number;
}
/**
* @brief Copies the current memory screen buffer to the real screen
*/
void Screen::copyToScreen() {
const Common::List<Common::Rect> *dirtyRects = _surface->getDirtyRects();
// If a full update is needed, update the whole screen
if (_surface->needsFullUpdate()) {
byte *ptr = (byte *)_surface->getPixels();
_vm->_system->copyRectToScreen(ptr, kScreenWidth,
0, 0, kScreenWidth, kScreenHeight);
} else {
// Otherwise, update only the dirty rectangles
for (const auto &r : *dirtyRects) {
// Pointer to the upper left corner of the rectangle
byte *ptr = (byte *)_surface->getBasePtr(r.left, r.top);
_vm->_system->copyRectToScreen(ptr, kScreenWidth,
r.left, r.top, r.width(), r.height());
}
}
// Call the "real" updateScreen and mark the surface clean
_vm->_system->updateScreen();
_surface->markClean();
}
/**
* @brief Clears the screen
*
* Clears the screen and marks the whole screen dirty.
*/
void Screen::clearScreen() {
byte *ptr = (byte *)_surface->getPixels();
_surface->markDirty();
memset(ptr, 0, kScreenWidth * kScreenHeight);
}
} // End of namespace Draci

63
engines/draci/screen.h Normal file
View File

@@ -0,0 +1,63 @@
/* 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 DRACI_SCREEN_H
#define DRACI_SCREEN_H
#include "common/scummsys.h"
namespace Draci {
enum ScreenParameters {
kScreenWidth = 320,
kScreenHeight = 200,
kNumColors = 256,
kDefaultTransparent = 255
};
class DraciEngine;
class Surface;
class Screen {
public:
Screen(DraciEngine *vm);
~Screen();
void setPalette(const byte *data, uint16 start, uint16 num);
void interpolatePalettes(const byte *first, const byte *second, uint16 start, uint16 num, int index, int number);
const byte *getPalette() const { return _palette; }
void copyToScreen();
void clearScreen();
Surface *getSurface() { return _surface; }
private:
int interpolate(int first, int second, int index, int number);
Surface *_surface;
byte *_palette;
byte *_blackPalette;
DraciEngine *_vm;
};
} // End of namespace Draci
#endif // DRACI_SCREEN_H

1233
engines/draci/script.cpp Normal file

File diff suppressed because it is too large Load Diff

199
engines/draci/script.h Normal file
View File

@@ -0,0 +1,199 @@
/* 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 DRACI_SCRIPT_H
#define DRACI_SCRIPT_H
#include "common/array.h"
#include "common/stream.h"
namespace Common {
class ReadStream;
}
namespace Draci {
enum {
/** The maximum number of parameters for a GPL command */
kMaxParams = 3,
kNumCommands = 55
};
class DraciEngine;
class Script;
typedef void (Script::*GPLHandler)(const Common::Array<int> &);
typedef int (Script::*GPLOperatorHandler)(int, int) const;
typedef int (Script::*GPLFunctionHandler)(int) const;
/**
* Represents a single command in the GPL scripting language bytecode.
* Each command is represented in the bytecode by a command number and a
* subnumber.
*/
enum GPL2ParameterType {
kGPL2Num = 1,
kGPL2Str = 2,
kGPL2Ident = 3,
kGPL2Math = 4
};
struct GPL2Command {
byte _number;
byte _subNumber;
const char *_name;
uint16 _numParams;
GPL2ParameterType _paramTypes[kMaxParams];
GPLHandler _handler;
};
struct GPL2Operator {
GPLOperatorHandler _handler;
const char *_name;
};
struct GPL2Function {
GPLFunctionHandler _handler;
const char *_name;
};
/**
* A convenience data type that holds both the actual bytecode and the
* length of the bytecode. Passed to Script::run().
*/
struct GPL2Program {
GPL2Program() : _bytecode(NULL), _length(0) {}
byte *_bytecode;
uint16 _length;
};
class Animation;
class GameObject;
class Script {
public:
Script(DraciEngine *vm) : _vm(vm), _jump(0), _endProgram(false) { setupCommandList(); }
void run(const GPL2Program &program, uint16 offset);
void runWrapper(const GPL2Program &program, uint16 offset, bool disableCursor, bool releaseAnims);
bool testExpression(const GPL2Program &program, uint16 offset) const;
void endCurrentProgram(bool value) { _endProgram = value; }
bool shouldEndProgram() const { return _endProgram; }
private:
int _jump;
bool _endProgram;
/** List of all GPL commands. Initialized in the constructor. */
const GPL2Command *_commandList;
const GPL2Operator *_operatorList;
const GPL2Function *_functionList;
void c_If(const Common::Array<int> &params);
void c_Goto(const Common::Array<int> &params);
void c_Let(const Common::Array<int> &params);
void load(const Common::Array<int> &params);
void start(const Common::Array<int> &params);
void loadMusic(const Common::Array<int> &params);
void startMusic(const Common::Array<int> &params);
void stopMusic(const Common::Array<int> &params);
void mark(const Common::Array<int> &params);
void release(const Common::Array<int> &params);
void icoStat(const Common::Array<int> &params);
void objStat(const Common::Array<int> &params);
void objStatOn(const Common::Array<int> &params);
void execInit(const Common::Array<int> &params);
void execLook(const Common::Array<int> &params);
void execUse(const Common::Array<int> &params);
void stayOn(const Common::Array<int> &params);
void walkOn(const Common::Array<int> &params);
void walkOnPlay(const Common::Array<int> &params);
void play(const Common::Array<int> &params);
void startPlay(const Common::Array<int> &params);
void justTalk(const Common::Array<int> &params);
void justStay(const Common::Array<int> &params);
void newRoom(const Common::Array<int> &params);
void talk(const Common::Array<int> &params);
void loadMap(const Common::Array<int> &params);
void roomMap(const Common::Array<int> &params);
void disableQuickHero(const Common::Array<int> &params);
void enableQuickHero(const Common::Array<int> &params);
void disableSpeedText(const Common::Array<int> &params);
void enableSpeedText(const Common::Array<int> &params);
void dialogue(const Common::Array<int> &params);
void exitDialogue(const Common::Array<int> &params);
void resetDialogue(const Common::Array<int> &params);
void resetDialogueFrom(const Common::Array<int> &params);
void resetBlock(const Common::Array<int> &params);
void setPalette(const Common::Array<int> &params);
void blackPalette(const Common::Array<int> &params);
void fadePalette(const Common::Array<int> &params);
void fadePalettePlay(const Common::Array<int> &params);
void loadPalette(const Common::Array<int> &params);
void quitGame(const Common::Array<int> &params);
void pushNewRoom(const Common::Array<int> &params);
void popNewRoom(const Common::Array<int> &params);
int operAnd(int op1, int op2) const;
int operOr(int op1, int op2) const;
int operXor(int op1, int op2) const;
int operSub(int op1, int op2) const;
int operAdd(int op1, int op2) const;
int operDiv(int op1, int op2) const;
int operMul(int op1, int op2) const;
int operEqual(int op1, int op2) const;
int operNotEqual(int op1, int op2) const;
int operGreater(int op1, int op2) const;
int operLess(int op1, int op2) const;
int operGreaterOrEqual(int op1, int op2) const;
int operLessOrEqual(int op1, int op2) const;
int operMod(int op1, int op2) const;
int funcRandom(int n) const;
int funcNot(int n) const;
int funcIsIcoOn(int iconID) const;
int funcIcoStat(int iconID) const;
int funcActIco(int iconID) const;
int funcIsIcoAct(int iconID) const;
int funcIsObjOn(int objID) const;
int funcIsObjOff(int objID) const;
int funcIsObjAway(int objID) const;
int funcActPhase(int objID) const;
int funcObjStat(int objID) const;
int funcLastBlock(int blockID) const;
int funcAtBegin(int yesno) const;
int funcBlockVar(int blockID) const;
int funcHasBeen(int blockID) const;
int funcMaxLine(int lines) const;
void setupCommandList();
const GPL2Command *findCommand(byte num, byte subnum) const;
int handleMathExpression(Common::ReadStream *reader) const;
DraciEngine *_vm;
};
} // End of namespace Draci
#endif // DRACI_SCRIPT_H

447
engines/draci/sound.cpp Normal file
View File

@@ -0,0 +1,447 @@
/* 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/archive.h"
#include "common/config-manager.h"
#include "common/debug.h"
#include "common/file.h"
#include "common/str.h"
#include "common/substream.h"
#include "common/textconsole.h"
#include "common/memstream.h"
#include "common/compression/unzip.h"
#include "draci/sound.h"
#include "draci/draci.h"
#include "draci/game.h"
#include "audio/audiostream.h"
#include "audio/mixer.h"
#include "audio/decoders/raw.h"
#include "audio/decoders/mp3.h"
#include "audio/decoders/vorbis.h"
#include "audio/decoders/flac.h"
namespace Draci {
void LegacySoundArchive::openArchive(const char *path) {
// Close previously opened archive (if any)
closeArchive();
debugCN(1, kDraciArchiverDebugLevel, "Loading samples %s: ", path);
_f = new Common::File();
_f->open(path);
if (_f->isOpen()) {
debugC(1, kDraciArchiverDebugLevel, "Success");
} else {
debugC(1, kDraciArchiverDebugLevel, "Error");
delete _f;
_f = nullptr;
return;
}
// Save path for reading in files later on
_path = path;
// Read archive header
debugC(1, kDraciArchiverDebugLevel, "Loading header");
uint totalLength = _f->readUint32LE();
const uint kMaxSamples = 4095; // The no-sound file is exactly 16K bytes long, so don't fail on short reads
uint *sampleStarts = (uint *)malloc(kMaxSamples * sizeof(uint));
if (!sampleStarts)
error("[LegacySoundArchive::openArchive] Cannot allocate buffer for no-sound file");
for (uint i = 0; i < kMaxSamples; ++i) {
sampleStarts[i] = _f->readUint32LE();
}
// Fill the sample table
for (_sampleCount = 0; _sampleCount < kMaxSamples - 1; ++_sampleCount) {
int length = sampleStarts[_sampleCount + 1] - sampleStarts[_sampleCount];
if (length <= 0 && sampleStarts[_sampleCount] >= totalLength) // heuristics to detect the last sample
break;
}
if (_sampleCount > 0) {
debugC(1, kDraciArchiverDebugLevel, "Archive info: %d samples, %d total length",
_sampleCount, totalLength);
_samples = new SoundSample[_sampleCount];
for (uint i = 0; i < _sampleCount; ++i) {
_samples[i]._offset = sampleStarts[i];
_samples[i]._length = sampleStarts[i+1] - sampleStarts[i];
_samples[i]._frequency = 0; // set in getSample()
}
if (_samples[_sampleCount-1]._offset + _samples[_sampleCount-1]._length != totalLength &&
_samples[_sampleCount-1]._offset + _samples[_sampleCount-1]._length - _samples[0]._offset != totalLength) {
// WORKAROUND: the stored length is stored with the header for sounds and without the header for dubbing. Crazy.
debugC(1, kDraciArchiverDebugLevel, "Broken sound archive: %d != %d",
_samples[_sampleCount-1]._offset + _samples[_sampleCount-1]._length,
totalLength);
closeArchive();
free(sampleStarts);
return;
}
} else {
debugC(1, kDraciArchiverDebugLevel, "Archive info: empty");
}
free(sampleStarts);
// Indicate that the archive has been successfully opened
_opened = true;
}
/**
* @brief LegacySoundArchive close method
*
* Closes the currently opened archive. It can be called explicitly to
* free up memory.
*/
void LegacySoundArchive::closeArchive() {
clearCache();
delete _f;
_f = nullptr;
delete[] _samples;
_samples = nullptr;
_sampleCount = 0;
_path = "";
_opened = false;
}
/**
* Clears the cache of the open files inside the archive without closing it.
* If the files are subsequently accessed, they are read from the disk.
*/
void LegacySoundArchive::clearCache() {
// Delete all cached data
for (uint i = 0; i < _sampleCount; ++i) {
_samples[i].close();
}
}
/**
* @brief On-demand sound sample loader
* @param i Index of file inside an archive
* @return Pointer to a SoundSample coresponding to the opened file or NULL (on failure)
*
* Loads individual samples from an archive to memory on demand.
*/
SoundSample *LegacySoundArchive::getSample(int i, uint freq) {
// Check whether requested file exists
if (i < 0 || i >= (int)_sampleCount) {
return nullptr;
}
debugCN(2, kDraciArchiverDebugLevel, "Accessing sample %d from archive %s... ",
i, _path);
// Check if file has already been opened and return that
if (_samples[i]._data) {
debugC(2, kDraciArchiverDebugLevel, "Cached");
} else {
// It would be nice to unify the approach with ZipSoundArchive
// and allocate a MemoryReadStream with buffer stored inside it
// that playSoundBuffer() would just play. Unfortunately,
// streams are not thread-safe and the same sample couldn't
// thus be played more than once at the same time (this holds
// even if we create a SeekableSubReadStream from it as this
// just uses the parent). The only thread-safe solution is to
// share a read-only buffer and allocate separate
// MemoryReadStream's on top of it.
_samples[i]._data = new byte[_samples[i]._length];
_samples[i]._format = RAW;
// Read in the file (without the file header)
_f->seek(_samples[i]._offset);
_f->read(_samples[i]._data, _samples[i]._length);
debugC(2, kDraciArchiverDebugLevel, "Read sample %d from archive %s",
i, _path);
}
_samples[i]._frequency = freq ? freq : _defaultFreq;
return _samples + i;
}
void ZipSoundArchive::openArchive(const char *path, const char *extension, SoundFormat format, int raw_frequency) {
closeArchive();
if ((format == RAW || format == RAW80) && !raw_frequency) {
error("openArchive() expects frequency for RAW data");
return;
}
debugCN(1, kDraciArchiverDebugLevel, "Trying to open ZIP archive %s: ", path);
_archive = Common::makeZipArchive(path);
_path = path;
_extension = extension;
_format = format;
_defaultFreq = raw_frequency;
if (_archive) {
Common::ArchiveMemberList files;
_archive->listMembers(files);
_sampleCount = files.size();
// The sample files are in the form ####.mp3 but not all numbers are used so we need to
// iterate the archive and find the last file
for (auto &file : files) {
Common::String filename = file->getName();
filename.erase(filename.size() - 4); // remove .mp3 extension
uint file_number = atoi(filename.c_str());
if(file_number > _sampleCount) // finds the last file (numerically)
_sampleCount = file_number;
}
debugC(1, kDraciArchiverDebugLevel, "Capacity %d", _sampleCount);
} else {
debugC(1, kDraciArchiverDebugLevel, "Failed");
}
}
void ZipSoundArchive::closeArchive() {
clearCache();
delete _archive;
_archive = nullptr;
_path = _extension = nullptr;
_sampleCount = _defaultFreq = 0;
_format = RAW;
}
void ZipSoundArchive::clearCache() {
// Just deallocate the link-list of (very short) headers for each
// dubbed sentence played in the current location. If the callers have
// not called .close() on any of the items, call them now.
for (auto &sample : _cache) {
sample.close();
}
_cache.clear();
}
SoundSample *ZipSoundArchive::getSample(int i, uint freq) {
if (i < 0 || i >= (int)_sampleCount) {
return nullptr;
}
debugCN(2, kDraciArchiverDebugLevel, "Accessing sample %d.%s from archive %s (format %d@%d, capacity %d): ",
i, _extension, _path, static_cast<int> (_format), _defaultFreq, _sampleCount);
if (freq != 0 && (_format != RAW && _format != RAW80)) {
error("Cannot resample a sound in compressed format");
return nullptr;
}
// We cannot really cache anything, because createReadStreamForMember()
// returns the data as a ReadStream, which is not thread-safe. We thus
// read it again each time even if it has possibly been already cached
// a while ago. This is not such a problem for dubbing as for regular
// sound samples.
SoundSample sample;
sample._frequency = freq ? freq : _defaultFreq;
sample._format = _format;
// Read in the file (without the file header)
Common::Path filename(Common::String::format("%d.%s", i+1, _extension));
sample._stream = _archive->createReadStreamForMember(filename);
if (!sample._stream) {
debugC(2, kDraciArchiverDebugLevel, "Doesn't exist");
return nullptr;
} else {
debugC(2, kDraciArchiverDebugLevel, "Read");
_cache.push_back(sample);
// Return a pointer that we own and which we will deallocate
// including its contents.
return &_cache.back();
}
}
Sound::Sound(Audio::Mixer *mixer) : _mixer(mixer), _muteSound(false), _muteVoice(false),
_showSubtitles(true), _talkSpeed(kStandardSpeed) {
for (int i = 0; i < SOUND_HANDLES; i++)
_handles[i].type = kFreeHandle;
setVolume();
}
SndHandle *Sound::getHandle() {
for (int i = 0; i < SOUND_HANDLES; i++) {
if (_handles[i].type != kFreeHandle && !_mixer->isSoundHandleActive(_handles[i].handle)) {
debugC(5, kDraciSoundDebugLevel, "Handle %d has finished playing", i);
_handles[i].type = kFreeHandle;
}
}
for (int i = 0; i < SOUND_HANDLES; i++) {
if (_handles[i].type == kFreeHandle) {
debugC(5, kDraciSoundDebugLevel, "Allocated handle %d", i);
return &_handles[i];
}
}
error("Sound::getHandle(): Too many sound handles");
return nullptr; // for compilers that don't support NORETURN
}
uint Sound::playSoundBuffer(Audio::SoundHandle *handle, const SoundSample &buffer, int volume,
sndHandleType handleType, bool loop) {
if (!buffer._stream && !buffer._data) {
warning("Empty stream");
return 0;
}
// Create a new SeekableReadStream which will be automatically disposed
// after the sample stops playing. Do not dispose the original
// data/stream though.
// Beware that if the sample comes from an archive (i.e., is stored in
// buffer._stream), then you must NOT play it more than once at the
// same time, because streams are not thread-safe. Playing it
// repeatedly is OK. Currently this is ensured by that archives are
// only used for dubbing, which is only played from one place in
// script.cpp, which blocks until the dubbed sentence has finished
// playing.
Common::SeekableReadStream *stream;
const int skip = buffer._format == RAW80 ? 80 : 0;
if (buffer._stream) {
stream = new Common::SeekableSubReadStream(
buffer._stream, skip, buffer._stream->size() /* end */, DisposeAfterUse::NO);
} else {
stream = new Common::MemoryReadStream(
buffer._data + skip, buffer._length - skip /* length */, DisposeAfterUse::NO);
}
Audio::SeekableAudioStream *reader = nullptr;
switch (buffer._format) {
case RAW:
case RAW80:
reader = Audio::makeRawStream(stream, buffer._frequency, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES);
break;
#ifdef USE_MAD
case MP3:
reader = Audio::makeMP3Stream(stream, DisposeAfterUse::YES);
break;
#endif
#ifdef USE_VORBIS
case OGG:
reader = Audio::makeVorbisStream(stream, DisposeAfterUse::YES);
break;
#endif
#ifdef USE_FLAC
case FLAC:
reader = Audio::makeFLACStream(stream, DisposeAfterUse::YES);
break;
#endif
default:
error("Unsupported compression format %d", static_cast<int> (buffer._format));
delete stream;
return 0;
}
const uint length = reader->getLength().msecs();
const Audio::Mixer::SoundType soundType = (handleType == kVoiceHandle) ?
Audio::Mixer::kSpeechSoundType : Audio::Mixer::kSFXSoundType;
Audio::AudioStream *audio_stream = Audio::makeLoopingAudioStream(reader, loop ? 0 : 1);
_mixer->playStream(soundType, handle, audio_stream, -1, volume);
return length;
}
uint Sound::playSound(const SoundSample *buffer, int volume, bool loop) {
if (!buffer || _muteSound)
return 0;
SndHandle *handle = getHandle();
handle->type = kEffectHandle;
return playSoundBuffer(&handle->handle, *buffer, 2 * volume, handle->type, loop);
}
void Sound::pauseSound() {
for (int i = 0; i < SOUND_HANDLES; i++)
if (_handles[i].type == kEffectHandle)
_mixer->pauseHandle(_handles[i].handle, true);
}
void Sound::resumeSound() {
for (int i = 0; i < SOUND_HANDLES; i++)
if (_handles[i].type == kEffectHandle)
_mixer->pauseHandle(_handles[i].handle, false);
}
void Sound::stopSound() {
for (int i = 0; i < SOUND_HANDLES; i++)
if (_handles[i].type == kEffectHandle) {
_mixer->stopHandle(_handles[i].handle);
debugC(5, kDraciSoundDebugLevel, "Stopping effect handle %d", i);
_handles[i].type = kFreeHandle;
}
}
uint Sound::playVoice(const SoundSample *buffer) {
if (!buffer || _muteVoice)
return 0;
SndHandle *handle = getHandle();
handle->type = kVoiceHandle;
return playSoundBuffer(&handle->handle, *buffer, Audio::Mixer::kMaxChannelVolume, handle->type, false);
}
void Sound::pauseVoice() {
for (int i = 0; i < SOUND_HANDLES; i++)
if (_handles[i].type == kVoiceHandle)
_mixer->pauseHandle(_handles[i].handle, true);
}
void Sound::resumeVoice() {
for (int i = 0; i < SOUND_HANDLES; i++)
if (_handles[i].type == kVoiceHandle)
_mixer->pauseHandle(_handles[i].handle, false);
}
void Sound::stopVoice() {
for (int i = 0; i < SOUND_HANDLES; i++)
if (_handles[i].type == kVoiceHandle) {
_mixer->stopHandle(_handles[i].handle);
debugC(5, kDraciSoundDebugLevel, "Stopping voice handle %d", i);
_handles[i].type = kFreeHandle;
}
}
void Sound::setVolume() {
_showSubtitles = ConfMan.getBool("subtitles");
_talkSpeed = ConfMan.getInt("talkspeed");
if (_mixer->isReady()) {
_muteSound = ConfMan.getBool("sfx_mute");
_muteVoice = ConfMan.getBool("speech_mute");
} else {
_muteSound = _muteVoice = true;
}
if (ConfMan.getBool("mute")) {
_muteSound = _muteVoice = true;
}
_mixer->muteSoundType(Audio::Mixer::kSFXSoundType, _muteSound);
_mixer->muteSoundType(Audio::Mixer::kSpeechSoundType, _muteVoice);
_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, ConfMan.getInt("sfx_volume"));
_mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, ConfMan.getInt("speech_volume"));
}
} // End of namespace Draci

226
engines/draci/sound.h Normal file
View File

@@ -0,0 +1,226 @@
/* 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 DRACI_SOUND_H
#define DRACI_SOUND_H
#include "common/str.h"
#include "common/file.h"
#include "common/list.h"
#include "audio/mixer.h"
namespace Common {
class Archive;
class SeekableReadStream;
}
namespace Draci {
enum SoundFormat { RAW, RAW80, MP3, OGG, FLAC }; // RAW80 means skip the first 80 bytes
/**
* Represents individual files inside the archive.
*/
struct SoundSample {
uint _offset; // For internal use of LegacySoundArchive
uint _length;
uint _frequency; // Only when _format == RAW or RAW80
SoundFormat _format;
byte *_data; // At most one of these two pointer can be non-NULL
Common::SeekableReadStream* _stream;
SoundSample() : _offset(0), _length(0), _frequency(0), _format(RAW), _data(NULL), _stream(NULL) { }
// The standard copy constructor is good enough, since we only store numbers and pointers.
// Don't call close() automaticall in the destructor, otherwise copying causes SIGSEGV.
void close() {
delete[] _data;
delete _stream;
_data = NULL;
_stream = NULL;
}
};
/**
* An abstract wrapper around archives of sound samples or dubbing.
*/
class SoundArchive {
public:
SoundArchive() { }
virtual ~SoundArchive() { }
/**
* Returns the number of sound samples in the archive. Zero means that
* a fake empty archive has been opened and the caller may consider
* opening a different one, for example with compressed music.
*/
virtual uint size() const = 0;
/**
* Checks whether there is an archive opened. Should be called before reading
* from the archive to check whether opening of the archive has succeeded.
*/
virtual bool isOpen() const = 0;
/**
* Removes cached samples from memory.
*/
virtual void clearCache() = 0;
/**
* Caches a given sample into memory and returns a pointer into it. We
* own the returned pointer, but the user may call .close() on it,
* which deallocates the memory of the actual sample data. If freq is
* nonzero, then the sample is played at a different frequency (only
* works for uncompressed samples).
*/
virtual SoundSample *getSample(int i, uint freq) = 0;
};
/**
* Reads CD.SAM (with dubbing) and CD2.SAM (with sound samples) from the
* original game. Caches all read samples in a thread-safe manner.
*/
class LegacySoundArchive : public SoundArchive {
public:
LegacySoundArchive(const char *path, uint defaultFreq) :
_path(NULL), _samples(NULL), _sampleCount(0), _defaultFreq(defaultFreq), _opened(false), _f(NULL) {
openArchive(path);
}
~LegacySoundArchive() override { closeArchive(); }
void openArchive(const char *path);
void closeArchive();
uint size() const override { return _sampleCount; }
bool isOpen() const override { return _opened; }
void clearCache() override;
SoundSample *getSample(int i, uint freq) override;
private:
const char *_path; ///< Path to file
SoundSample *_samples; ///< Internal array of files
uint _sampleCount; ///< Number of files in archive
uint _defaultFreq; ///< The default sampling frequency of the archived samples
bool _opened; ///< True if the archive is opened, false otherwise
Common::File *_f; ///< Opened file
};
/**
* Reads ZIP archives with uncompressed files containing lossy-compressed
* samples in arbitrary format. Doesn't do any real caching and is
* thread-safe.
*/
class ZipSoundArchive : public SoundArchive {
public:
ZipSoundArchive() : _archive(NULL), _path(NULL), _extension(NULL), _format(RAW), _sampleCount(0), _defaultFreq(0), _cache() { }
~ZipSoundArchive() override { closeArchive(); }
void openArchive(const char *path, const char *extension, SoundFormat format, int raw_frequency = 0);
void closeArchive();
uint size() const override { return _sampleCount; }
bool isOpen() const override { return _archive != NULL; }
void clearCache() override;
SoundSample *getSample(int i, uint freq) override;
private:
Common::Archive *_archive;
const char *_path;
const char *_extension;
SoundFormat _format;
uint _sampleCount;
uint _defaultFreq;
// Since we typically play at most 1 dubbing at a time, we could get
// away with having just 1 record allocated and reusing it each time.
// However, that would be thread-unsafe if two samples were played.
// Although the player does not do that, it is nicer to allow for it in
// principle. For each dubbed sentence, we allocate a new record in
// the following link-list, which is cleared during each location
// change. The dubbed samples themselves are manually deallocated
// after they end.
Common::List<SoundSample> _cache;
};
#define SOUND_HANDLES 10
enum sndHandleType {
kFreeHandle,
kEffectHandle,
kVoiceHandle
};
struct SndHandle {
Audio::SoundHandle handle;
sndHandleType type;
};
// Taken from engines/saga/sound.h and simplified (in particular, removed
// decompression until we support compressed files too).
class Sound {
public:
Sound(Audio::Mixer *mixer);
~Sound() {}
uint playSound(const SoundSample *buffer, int volume, bool loop);
void pauseSound();
void resumeSound();
void stopSound();
bool isMutedSound() const { return _muteSound; }
uint playVoice(const SoundSample *buffer);
void pauseVoice();
void resumeVoice();
void stopVoice();
bool isMutedVoice() const { return _muteVoice; }
void stopAll() { stopVoice(); stopSound(); }
void setVolume();
bool showSubtitles() const { return _showSubtitles; }
int talkSpeed() const { return _talkSpeed; }
private:
// Returns the length of the sound sample in miliseconds.
uint playSoundBuffer(Audio::SoundHandle *handle, const SoundSample &buffer, int volume,
sndHandleType handleType, bool loop);
SndHandle *getHandle();
Audio::Mixer *_mixer;
bool _muteSound;
bool _muteVoice;
bool _showSubtitles;
int _talkSpeed;
SndHandle _handles[SOUND_HANDLES];
};
} // End of namespace Draci
#endif // DRACI_SOUND_H

350
engines/draci/sprite.cpp Normal file
View File

@@ -0,0 +1,350 @@
/* 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/memstream.h"
#include "draci/draci.h"
#include "draci/font.h"
#include "draci/sprite.h"
#include "draci/surface.h"
namespace Draci {
const Displacement kNoDisplacement = { 0, 0, 1.0, 1.0 };
/**
* @brief Transforms an image from column-wise to row-wise
* @param img pointer to the buffer containing the image data
* width width of the image in the buffer
* height height of the image in the buffer
*/
static void transformToRows(byte *img, uint16 width, uint16 height) {
byte *buf = new byte[(uint)(width * height)];
byte *tmp = buf;
memcpy(buf, img, (uint)(width * height));
for (uint16 i = 0; i < width; ++i) {
for (uint16 j = 0; j < height; ++j) {
img[j * width + i] = *tmp++;
}
}
delete[] buf;
}
/**
* Constructor for loading sprites from a raw data buffer, one byte per pixel.
*/
Sprite::Sprite(uint16 width, uint16 height, byte *raw_data, int x, int y, bool columnwise)
: _ownsData(true), _data(raw_data), _mirror(false) {
_width = width;
_height = height;
_scaledWidth = _width;
_scaledHeight = _height;
_x = x;
_y = y;
_delay = 0;
if (columnwise) {
transformToRows(raw_data, _width, _height);
}
}
/**
* Constructor for loading sprites from a sprite-formatted buffer, one byte per
* pixel.
*/
Sprite::Sprite(const byte *sprite_data, uint16 length, int x, int y, bool columnwise)
: _ownsData(false), _data(nullptr), _mirror(false) {
Common::MemoryReadStream reader(sprite_data, length);
_width = reader.readSint16LE();
_height = reader.readSint16LE();
_scaledWidth = _width;
_scaledHeight = _height;
_x = x;
_y = y;
_delay = 0;
if (!columnwise) {
_ownsData = false;
_data = sprite_data + 4;
} else {
_ownsData = true;
byte *data = new byte[_width * _height];
memcpy(data, sprite_data + 4, _width * _height);
transformToRows(data, _width, _height);
_data = data;
}
}
Sprite::~Sprite() {
if (_ownsData) {
delete[] _data;
}
}
int Sprite::getPixel(int x, int y, const Displacement &displacement) const {
Common::Rect rect = getRect(displacement);
int dy = y - rect.top;
int dx = x - rect.left;
// Calculate scaling factors
double scaleX = double(rect.width()) / _width;
double scaleY = double(rect.height()) / _height;
int sy = lround(dy / scaleY);
int sx = lround(dx / scaleX);
if (_mirror)
return _data[sy * _width + (_width - sx)];
else
return _data[sy * _width + sx];
}
void Sprite::drawReScaled(Surface *surface, bool markDirty, const Displacement &displacement) const {
const Common::Rect destRect(getRect(displacement));
Common::Rect clippedDestRect(0, 0, surface->w, surface->h);
clippedDestRect.clip(destRect);
// Calculate by how much we need to adjust the source rectangle to account for cropping
const Common::Point croppedBy(clippedDestRect.left - destRect.left, clippedDestRect.top - destRect.top);
// Get pointers to source and destination buffers
byte *dst = (byte *)surface->getBasePtr(clippedDestRect.left, clippedDestRect.top);
const int transparent = surface->getTransparentColor();
// Calculate how many rows and columns we need to draw
const int rows = clippedDestRect.height();
const int columns = clippedDestRect.width();
// Precalculate column indexes
int *columnIndices = new int[columns];
if (!_mirror) {
for (int j = 0; j < columns; ++j) {
columnIndices[j] = (j + croppedBy.x) * _width / destRect.width();
}
} else {
// Draw the sprite mirrored if the _mirror flag is set
for (int j = 0; j < columns; ++j) {
columnIndices[j] = _width - 1 - (j + croppedBy.x) * _width / destRect.width();
}
}
// Blit the sprite to the surface
for (int i = 0; i < rows; ++i) {
// Compute the index of current row to be drawn
const int row = (i + croppedBy.y) * _height / destRect.height();
const byte *row_data = _data + row * _width;
for (int j = 0; j < columns; ++j) {
// Fetch index of current column to be drawn
const byte src = row_data[columnIndices[j]];
// Don't blit if the pixel is transparent on the target surface
if (src != transparent) {
dst[j] = src;
}
}
// Advance to next row
dst += surface->pitch;
}
// Mark the sprite's rectangle dirty
if (markDirty) {
surface->markDirtyRect(clippedDestRect);
}
delete[] columnIndices;
}
/**
* @brief Draws the sprite to a Draci::Surface
* @param surface Pointer to a Draci::Surface
*
* Draws the sprite to a Draci::Surface and marks its rectangle on the surface as dirty.
* It is safe to call it for sprites that would overflow the surface.
*/
void Sprite::draw(Surface *surface, bool markDirty, int relX, int relY) const {
const Common::Rect destRect(_x + relX, _y + relY, _x + relX + _width, _y + relY + _height);
Common::Rect clippedDestRect(0, 0, surface->w, surface->h);
clippedDestRect.clip(destRect);
// Calculate by how much we need to adjust the source rectangle to account for cropping
const Common::Point croppedBy(clippedDestRect.left - destRect.left, clippedDestRect.top - destRect.top);
// Get pointers to source and destination buffers
byte *dst = (byte *)surface->getBasePtr(clippedDestRect.left, clippedDestRect.top);
const byte *src = _data + croppedBy.y * _width +
(!_mirror ? croppedBy.x : _width - 1 - croppedBy.x);
const int transparent = surface->getTransparentColor();
// Calculate how many rows and columns we need to draw
const int rows = clippedDestRect.height();
const int columns = clippedDestRect.width();
// Blit the sprite to the surface
for (int i = 0; i < rows; ++i) {
if (!_mirror) {
for (int j = 0; j < columns; ++j) {
if (src[j] != transparent) {
dst[j] = src[j];
}
}
} else {
for (int j = 0; j < columns; ++j) {
if (src[-j] != transparent) {
dst[j] = src[-j];
}
}
}
// Advance to next row
dst += surface->pitch;
src += _width;
}
// Mark the sprite's rectangle dirty
if (markDirty) {
surface->markDirtyRect(clippedDestRect);
}
}
Common::Rect Sprite::getRect(const Displacement &displacement) const {
return Common::Rect(_x + displacement.relX, _y + displacement.relY,
_x + displacement.relX + lround(_scaledWidth * displacement.extraScaleX),
_y + displacement.relY + lround(_scaledHeight * displacement.extraScaleY));
}
Text::Text(const Common::String &str, const Font *font, byte fontColor,
int x, int y, uint spacing) {
_x = x;
_y = y;
_delay = 0;
_text = str;
_length = 0;
for (uint i = 0; i < _text.size(); ++i) {
if (_text[i] != '|') {
++_length;
}
}
_spacing = spacing;
_color = fontColor;
_font = font;
_width = _font->getStringWidth(str, _spacing);
_height = _font->getStringHeight(str);
_scaledWidth = _width;
_scaledHeight = _height;
}
void Text::setText(const Common::String &str) {
_width = _font->getStringWidth(str, _spacing);
_height = _font->getStringHeight(str);
_text = str;
_length = 0;
for (uint i = 0; i < _text.size(); ++i) {
if (_text[i] != '|') {
++_length;
}
}
}
void Text::draw(Surface *surface, bool markDirty, int relX, int relY) const {
_font->drawString(surface, _text, _x + relX, _y + relY, _color, _spacing, true);
}
Common::Rect Text::getRect(const Displacement &displacement) const {
// Texts are never scaled
return Common::Rect(_x + displacement.relX, _y + displacement.relY, _x + displacement.relX + _width, _y + displacement.relY + _height);
}
void Text::setFont(const Font *font) {
if (font == _font) {
return;
}
_font = font;
_width = _font->getStringWidth(_text, _spacing);
_height = _font->getStringHeight(_text);
}
void Text::repeatedlySplitLongLines(uint maxWidth) {
while (_width > maxWidth) {
splitLinesLongerThan(maxWidth);
_width = _font->getStringWidth(_text, _spacing);
_height = _font->getStringHeight(_text);
}
}
void Text::splitLinesLongerThan(uint maxWidth) {
char *start = const_cast<char *> (_text.c_str()); // hacky
while (1) {
char *end = strchr(start, '|');
if (end) {
*end = 0;
}
uint lineWidth = _font->getStringWidth(start, _spacing);
if (lineWidth > maxWidth) {
int middle = end ? (end - start) / 2 : strlen(start) / 2;
for (int i = 0; ; ++i) {
if (start[middle + i] == ' ') {
start[middle + i] = '|';
break;
}
if (start[middle - i] == ' ') {
start[middle - i] = '|';
break;
}
}
debugC(2, kDraciGeneralDebugLevel, "Long line of width %d split into %s", lineWidth, start);
}
if (end) {
*end = '|';
start = end + 1;
} else {
break;
}
}
}
} // End of namespace Draci

171
engines/draci/sprite.h Normal file
View File

@@ -0,0 +1,171 @@
/* 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 DRACI_SPRITE_H
#define DRACI_SPRITE_H
#include "common/scummsys.h"
#include "common/rect.h"
namespace Draci {
enum DrawableType {
kDrawableText,
kDrawableSprite
};
struct Displacement {
int relX, relY;
double extraScaleX, extraScaleY;
};
extern const Displacement kNoDisplacement;
class Surface;
class Font;
class Drawable {
public:
virtual void draw(Surface *surface, bool markDirty, int relX, int relY) const = 0;
virtual void drawReScaled(Surface *surface, bool markDirty, const Displacement &displacement) const = 0;
virtual ~Drawable() {}
uint getWidth() const { return _width; }
uint getHeight() const { return _height; }
uint getScaledWidth() const { return _scaledWidth; }
uint getScaledHeight() const { return _scaledHeight; }
void setScaled(uint width, uint height) {
_scaledWidth = width;
_scaledHeight = height;
}
int getX() const { return _x; }
int getY() const { return _y; }
void setX(int x) { _x = x; }
void setY(int y) { _y = y; }
void setDelay(int delay) { _delay = delay; }
int getDelay() const { return _delay; }
virtual Common::Rect getRect(const Displacement &displacement) const = 0;
virtual DrawableType getType() const = 0;
protected:
uint _width; ///< Width of the sprite
uint _height; ///< Height of the sprite
uint _scaledWidth; ///< Scaled width of the sprite
uint _scaledHeight; ///< Scaled height of the sprite
int _x, _y; ///< Sprite coordinates
/** The time a drawable should stay on the screen
* before being replaced by another or deleted
*/
int _delay;
};
/**
* Represents a Draci Historie sprite. Supplies two constructors; one for
* loading a sprite from a raw data buffer and one for loading a sprite in
* the Draci sprite format. Supports loading the sprite from a column-wise
* format (transforming them to row-wise) since that is the way the sprites
* are stored in the original game files.
*
* Sprite format:
* [uint16LE] sprite width
* [uint16LE] sprite height
* [height * width bytes] image pixels stored column-wise, one byte per pixel
*/
class Sprite : public Drawable {
public:
// Takes ownership of raw_data.
Sprite(uint16 width, uint16 height, byte *raw_data, int x, int y, bool columnwise);
// It doesn't take ownership of sprite_data. If columnwise is false,
// it internally points to the original buffer (which has lifetime at
// least as long as this sprite, assumming that it comes from a cached
// BArchive file), otherwise it allocates its own buffer with the
// transposed image.
Sprite(const byte *sprite_data, uint16 length, int x, int y, bool columnwise);
~Sprite() override;
void draw(Surface *surface, bool markDirty, int relX, int relY) const override;
void drawReScaled(Surface *surface, bool markDirty, const Displacement &displacement) const override;
void setMirrorOn() { _mirror = true; }
void setMirrorOff() { _mirror = false; }
Common::Rect getRect(const Displacement &displacement) const override;
const byte *getBuffer() const { return _data; }
int getPixel(int x, int y, const Displacement &displacement) const;
DrawableType getType() const override { return kDrawableSprite; }
private:
bool _ownsData;
const byte *_data; ///< Pointer to a buffer containing raw sprite data (row-wise)
bool _mirror;
};
class Text : public Drawable {
public:
Text(const Common::String &str, const Font *font, byte fontColor,
int x, int y, uint spacing);
~Text() override {}
void setText(const Common::String &str);
void setColor(byte fontColor) { _color = fontColor; }
void setSpacing(uint spacing) { _spacing = spacing; }
void setFont(const Font *font);
void repeatedlySplitLongLines(uint maxWidth);
uint getLength() const { return _length; }
void draw(Surface *surface, bool markDirty, int relX, int relY) const override;
// drawReScaled just calls draw so that Text can be accessed through a Drawable pointer.
// Text scaling does not need to be handled.
void drawReScaled(Surface *surface, bool markDirty, const Displacement &displacement) const override { draw(surface, markDirty, displacement.relX, displacement.relY); }
Common::Rect getRect(const Displacement &displacement) const override;
DrawableType getType() const override { return kDrawableText; }
private:
void splitLinesLongerThan(uint maxWidth);
Common::String _text;
uint _length;
uint8 _color;
uint _spacing;
const Font *_font;
};
} // End of namespace Draci
#endif // DRACI_SPRITE_H

127
engines/draci/surface.cpp Normal file
View File

@@ -0,0 +1,127 @@
/* 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 "draci/screen.h"
#include "draci/surface.h"
namespace Draci {
Surface::Surface(int width, int height) {
this->create(width, height, Graphics::PixelFormat::createFormatCLUT8());
this->markClean();
_transparentColor = kDefaultTransparent;
}
Surface::~Surface() {
this->free();
}
/**
* @brief Marks a dirty rectangle on the surface
* @param r The rectangle to be marked dirty
*/
void Surface::markDirtyRect(Common::Rect r) {
Common::List<Common::Rect>::iterator it;
r.clip(w, h);
if (r.isEmpty())
return;
it = _dirtyRects.begin();
while (it != _dirtyRects.end()) {
if (it->contains(r))
return;
if (r.contains(*it))
it = _dirtyRects.erase(it);
else
++it;
}
_dirtyRects.push_back(r);
}
/**
* @brief Marks the whole surface dirty
*/
void Surface::markDirty() {
_fullUpdate = true;
}
/**
* @brief Marks the whole surface clean
*/
void Surface::markClean() {
_fullUpdate = false;
_dirtyRects.clear();
}
/**
* @brief Fills the surface with the specified color
*/
void Surface::fill(uint color) {
byte *ptr = (byte *)getPixels();
memset(ptr, color, (uint)(w * h));
}
/**
* @brief Calculates horizontal center of an object
*
* @param x The x coordinate of the center
* @param width The width of the object to be centered (in pixels)
*
* @return The centered x coordinate
*/
uint Surface::centerOnX(int x, int width) const {
int newX = x - width / 2;
if (newX + width > w)
newX = w - width;
if (newX < 0)
newX = 0;
return newX;
}
/**
* @brief Calculates vertical center of an object
*
* @param y The y coordinate of the center
* @param height The height of the object to be centered (in pixels)
*
* @return The centered y coordinate
*/
uint Surface::putAboveY(int y, int height) const {
int newY = y - height;
if (newY + height > h)
newY = h - height;
if (newY < 0)
newY = 0;
return newY;
}
} // End of namespace Draci

67
engines/draci/surface.h Normal file
View File

@@ -0,0 +1,67 @@
/* 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 DRACI_SURFACE_H
#define DRACI_SURFACE_H
#include "common/list.h"
#include "common/rect.h"
#include "graphics/surface.h"
namespace Draci {
class Surface : public Graphics::Surface {
public:
Surface(int width, int height);
~Surface();
void markDirtyRect(Common::Rect r);
const Common::List<Common::Rect> *getDirtyRects() const { return &_dirtyRects; }
void clearDirtyRects() { _dirtyRects.clear(); }
void markDirty();
void markClean();
bool needsFullUpdate() const { return _fullUpdate; }
uint getTransparentColor() const { return _transparentColor; }
void setTransparentColor(uint color) { _transparentColor = color; }
void fill(uint color);
uint putAboveY(int y, int height) const;
uint centerOnX(int x, int width) const;
Common::Rect getDimensions() const { return Common::Rect(w, h); }
private:
/** The current transparent color of the surface. See getTransparentColor() and
* setTransparentColor().
*/
uint _transparentColor;
/** Set when the surface is scheduled for a full update.
* See markDirty(), markClean(). Accessed via needsFullUpdate().
*/
bool _fullUpdate;
Common::List<Common::Rect> _dirtyRects; ///< List of currently dirty rectangles
};
} // End of namespace Draci
#endif // DRACI_SURFACE_H

772
engines/draci/walking.cpp Normal file
View File

@@ -0,0 +1,772 @@
/* 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/memstream.h"
#include "draci/draci.h"
#include "draci/animation.h"
#include "draci/game.h"
#include "draci/walking.h"
#include "draci/sprite.h"
namespace Draci {
void WalkingMap::load(const byte *data, uint length) {
Common::MemoryReadStream mapReader(data, length);
_realWidth = mapReader.readUint16LE();
_realHeight = mapReader.readUint16LE();
_deltaX = mapReader.readUint16LE();
_deltaY = mapReader.readUint16LE();
_mapWidth = mapReader.readUint16LE();
_mapHeight = mapReader.readUint16LE();
_byteWidth = mapReader.readUint16LE();
// Set the data pointer to raw map data
_data = data + mapReader.pos();
}
bool WalkingMap::getPixel(int x, int y) const {
const byte *pMapByte = _data + _byteWidth * y + x / 8;
return *pMapByte & (1 << x % 8);
}
bool WalkingMap::isWalkable(const Common::Point &p) const {
// Convert to map pixels
return getPixel(p.x / _deltaX, p.y / _deltaY);
}
Sprite *WalkingMap::newOverlayFromMap(byte color) const {
// HACK: Create a visible overlay from the walking map so we can test it
byte *wlk = new byte[_realWidth * _realHeight];
memset(wlk, 255, _realWidth * _realHeight);
for (int i = 0; i < _mapWidth; ++i) {
for (int j = 0; j < _mapHeight; ++j) {
if (getPixel(i, j)) {
drawOverlayRectangle(Common::Point(i, j), color, wlk);
}
}
}
Sprite *ov = new Sprite(_realWidth, _realHeight, wlk, 0, 0, false);
// ov has taken the ownership of wlk.
return ov;
}
/**
* @brief For a given point, find a nearest walkable point on the walking map
*
* @param startX x coordinate of the point
* @param startY y coordinate of the point
*
* @return A Common::Point representing the nearest walkable point
*/
Common::Point WalkingMap::findNearestWalkable(int startX, int startY) const {
// The dimension of the screen.
const Common::Rect searchRect(0, 0, _realWidth, _realHeight);
// Consider circles with radii gradually rising from 0 to the length of
// the longest edge on the screen. For each radius, probe all points
// on the circle and return the first walkable one. Go through angles
// [0, 45 degrees] and probe all 8 reflections of each point.
for (int radius = 0; radius < searchRect.width() + searchRect.height(); radius += _deltaX) {
// The position of the point on the circle.
int x = 0;
int y = radius;
// Variables for computing the points on the circle
int prediction = 1 - radius;
int dx = 3;
int dy = 2 * radius - 2;
// Walk until we reach the 45-degree angle.
while (x <= y) {
// The place where, eventually, the result coordinates will be stored
Common::Point finalPos;
// Auxiliary array of multiplicative coefficients for reflecting points.
static const int kSigns[] = { 1, -1 };
// Check all 8 reflections of the basic point.
for (uint i = 0; i < 2; ++i) {
finalPos.y = startY + y * kSigns[i];
for (uint j = 0; j < 2; ++j) {
finalPos.x = startX + x * kSigns[j];
// If the current point is walkable, return it
if (searchRect.contains(finalPos.x, finalPos.y) && isWalkable(finalPos)) {
return finalPos;
}
}
}
for (uint i = 0; i < 2; ++i) {
finalPos.y = startY + x * kSigns[i];
for (uint j = 0; j < 2; ++j) {
finalPos.x = startX + y * kSigns[j];
// If the current point is walkable, return it
if (searchRect.contains(finalPos.x, finalPos.y) && isWalkable(finalPos)) {
return finalPos;
}
}
}
// Walk along the circle to the next point: the
// X-coordinate moves to the right, and the
// Y-coordinate may move to the bottom if the predictor
// says so.
if (prediction >= 0) {
prediction -= dy;
dy -= 2 * _deltaX;
y -= _deltaX;
}
prediction += dx;
dx += 2 * _deltaX;
x += _deltaX;
}
}
// The destination point is unreachable.
return Common::Point(-1, -1);
}
// We don't use Common::Point due to using static initialization.
const int WalkingMap::kDirections[][2] = { {0, -1}, {0, +1}, {-1, 0}, {+1, 0} };
bool WalkingMap::findShortestPath(Common::Point p1, Common::Point p2, WalkingPath *path) const {
// Round the positions to map squares.
p1.x /= _deltaX;
p2.x /= _deltaX;
p1.y /= _deltaY;
p2.y /= _deltaY;
// Allocate buffers for breadth-first search. The buffer of points for
// exploration should be large enough.
const int bufSize = 4 * _realHeight;
int8 *cameFrom = new int8[_mapWidth * _mapHeight];
Common::Point *toSearch = new Common::Point[bufSize];
// Insert the starting point as a single seed.
int toRead = 0, toWrite = 0;
memset(cameFrom, -1, _mapWidth * _mapHeight); // -1 = not found yet
cameFrom[p1.y * _mapWidth + p1.x] = 0;
toSearch[toWrite++] = p1;
// Search until we empty the whole buffer (not found) or find the
// destination point.
while (toRead != toWrite) {
const Common::Point &here = toSearch[toRead];
const int from = cameFrom[here.y * _mapWidth + here.x];
if (here == p2) {
break;
}
// Look into all 4 directions in a particular order depending
// on the direction we came to this point from. This is to
// ensure that among many paths of the same length, the one
// with the smallest number of turns is preferred.
for (int addDir = 0; addDir < 4; ++addDir) {
const int probeDirection = (from + addDir) % 4;
const int x = here.x + kDirections[probeDirection][0];
const int y = here.y + kDirections[probeDirection][1];
if (x < 0 || x >= _mapWidth || y < 0 || y >= _mapHeight) {
continue;
}
// If this point is walkable and we haven't seen it
// yet, record how we have reached it and insert it
// into the round buffer for exploration.
if (getPixel(x, y) && cameFrom[y * _mapWidth + x] == -1) {
cameFrom[y * _mapWidth + x] = probeDirection;
toSearch[toWrite++] = Common::Point(x, y);
toWrite %= bufSize;
}
}
++toRead;
toRead %= bufSize;
}
// The path doesn't exist.
if (toRead == toWrite) {
delete[] cameFrom;
delete[] toSearch;
return false;
}
// Trace the path back and store it. Count the path length, resize the
// output array, and then track the pack from the end.
path->clear();
for (int pass = 0; pass < 2; ++pass) {
Common::Point p = p2;
int index = 0;
while (1) {
++index;
if (pass == 1) {
(*path)[path->size() - index] = p;
}
if (p == p1) {
break;
}
const int from = cameFrom[p.y * _mapWidth + p.x];
p.x -= kDirections[from][0];
p.y -= kDirections[from][1];
}
if (pass == 0) {
path->resize(index);
}
}
delete[] cameFrom;
delete[] toSearch;
return true;
}
void WalkingMap::obliquePath(const WalkingPath& path, WalkingPath *obliquedPath) {
// Prune the path to only contain vertices where the direction is changing.
obliquedPath->clear();
if (path.empty()) {
return;
}
obliquedPath->push_back(path[0]);
uint index = 1;
while (index < path.size()) {
// index1 points to the last vertex inserted into the
// simplified path.
uint index1 = index - 1;
// Probe the vertical direction. Notice that the shortest path
// never turns by 180 degrees and therefore it is sufficient to
// test that the X coordinates are equal.
while (index < path.size() && path[index].x == path[index1].x) {
++index;
}
if (index - 1 > index1) {
index1 = index - 1;
obliquedPath->push_back(path[index1]);
}
// Probe the horizontal direction.
while (index < path.size() && path[index].y == path[index1].y) {
++index;
}
if (index - 1 > index1) {
index1 = index - 1;
obliquedPath->push_back(path[index1]);
}
}
// Repeatedly oblique the path until it cannot be improved. This
// process is finite, because after each success the number of vertices
// goes down.
while (managedToOblique(obliquedPath)) {}
}
Sprite *WalkingMap::newOverlayFromPath(const WalkingPath &path, byte color) const {
// HACK: Create a visible overlay from the walking map so we can test it
byte *wlk = new byte[_realWidth * _realHeight];
memset(wlk, 255, _realWidth * _realHeight);
for (uint segment = 1; segment < path.size(); ++segment) {
const Common::Point &v1 = path[segment-1];
const Common::Point &v2 = path[segment];
const int steps = pointsBetween(v1, v2);
// Draw only points in the interval [v1, v2). These half-open
// half-closed intervals connect all the way to the last point.
for (int step = 0; step < steps; ++step) {
drawOverlayRectangle(interpolate(v1, v2, step, steps), color, wlk);
}
}
// Draw the last point. This works also when the path has no segment,
// but just one point.
if (path.size() > 0) {
const Common::Point &vLast = path[path.size()-1];
drawOverlayRectangle(vLast, color, wlk);
}
Sprite *ov = new Sprite(_realWidth, _realHeight, wlk, 0, 0, false);
// ov has taken the ownership of wlk.
return ov;
}
void WalkingMap::drawOverlayRectangle(const Common::Point &p, byte color, byte *buf) const {
for (int i = 0; i < _deltaX; ++i) {
for (int j = 0; j < _deltaY; ++j) {
buf[(p.y * _deltaY + j) * _realWidth + (p.x * _deltaX + i)] = color;
}
}
}
int WalkingMap::pointsBetween(const Common::Point &p1, const Common::Point &p2) {
return MAX(ABS(p2.x - p1.x), ABS(p2.y - p1.y));
}
Common::Point WalkingMap::interpolate(const Common::Point &p1, const Common::Point &p2, int i, int n) {
const int x = (p1.x * (n-i) + p2.x * i + n/2) / n;
const int y = (p1.y * (n-i) + p2.y * i + n/2) / n;
return Common::Point(x, y);
}
bool WalkingMap::lineIsCovered(const Common::Point &p1, const Common::Point &p2) const {
const int steps = pointsBetween(p1, p2);
for (int step = 0; step <= steps; ++step) {
Common::Point p = interpolate(p1, p2, step, steps);
if (!getPixel(p.x, p.y)) {
return false;
}
}
return true;
}
bool WalkingMap::managedToOblique(WalkingPath *path) const {
bool improved = false;
// Making the path oblique works as follows. If the path has at least
// 3 remaining vertices, we try to oblique the L-shaped path between
// them. First we try to connect the 1st and 3rd vertex directly (if
// all points on the line between them are walkable) and then we try to
// walk on both edges towards the 2nd vertex in parallel and try to
// find a shortcut (replacing the 2nd vertex by this mid-point). If
// either of those attempts succeeds, we have shortned the path. We
// update the path vertices and continue with the next segment.
for (uint head = 2; head < path->size(); ++head) {
const Common::Point &v1 = (*path)[head-2];
const Common::Point &v2 = (*path)[head-1];
const Common::Point &v3 = (*path)[head];
const int points12 = pointsBetween(v1, v2);
const int points32 = pointsBetween(v3, v2);
// Find the first point p on each edge [v1, v2] and [v3, v2]
// such that the edge [p, the third vertex] is covered.
// Ideally we would like p \in {v1, v3} and the closer the
// better. The last point p = v2 should always succeed.
int first12, first32;
for (first12 = 0; first12 < points12; ++first12) {
Common::Point midPoint = interpolate(v1, v2, first12, points12);
if (lineIsCovered(midPoint, v3)) {
break;
}
}
if (first12 == 0) {
// Can completely remove the vertex. Head stays the
// same after -- and ++.
path->remove_at(--head);
improved = true;
continue;
}
for (first32 = 0; first32 < points32; ++first32) {
Common::Point midPoint = interpolate(v3, v2, first32, points32);
if (lineIsCovered(midPoint, v1)) {
break;
}
}
if (first12 < points12 && first32 >= points32 + MIN(first12 - points12, 0)) {
// There is such a point on the first edge and the
// second edge has either not succeeded or we gain more
// by cutting this edge than the other one.
(*path)[head-1] = interpolate(v1, v2, first12, points12);
// After replacing the 2nd vertex, let head move on.
} else if (first32 < points32) {
(*path)[head-1] = interpolate(v3, v2, first32, points32);
}
}
return improved;
}
void WalkingState::stopWalking() {
_path.clear();
_callback = nullptr;
}
void WalkingState::startWalking(const Common::Point &p1, const Common::Point &p2,
const Common::Point &mouse, SightDirection dir,
const Common::Point &delta, const WalkingPath& path) {
_path = path;
_mouse = mouse;
_dir = dir;
if (!_path.size()) {
_path.push_back(p1);
}
if (_path.size() == 1 && p2 != p1) {
// Although the first and last point belong to the same
// rectangle and therefore the computed path is of length 1,
// they are different pixels.
_path.push_back(p2);
}
debugC(2, kDraciWalkingDebugLevel, "Starting walking [%d,%d] -> [%d,%d] with %d vertices",
p1.x, p1.y, p2.x, p2.y, _path.size());
// The first and last point are available with pixel accurracy.
_path[0] = p1;
_path[_path.size() - 1] = p2;
// The intermediate points are given with map granularity; convert them
// to pixels.
for (uint i = 1; i < _path.size() - 1; ++i) {
_path[i].x *= delta.x;
_path[i].y *= delta.y;
}
// Remember the initial dragon's direction.
const GameObject *dragon = _vm->_game->getObject(kDragonObject);
_startingDirection = static_cast<Movement> (dragon->playingAnim());
// Going to start with the first segment.
_segment = 0;
_lastAnimPhase = -1;
_turningFinished = false;
turnForTheNextSegment();
}
void WalkingState::setCallback(const GPL2Program *program, uint16 offset) {
_callback = _callbackLast = program;
_callbackOffset = _callbackOffsetLast = offset;
}
void WalkingState::callback() {
if (!_callback) {
return;
}
debugC(2, kDraciWalkingDebugLevel, "Calling walking callback");
const GPL2Program &originalCallback = *_callback;
_callback = nullptr;
_vm->_script->runWrapper(originalCallback, _callbackOffset, true, false);
_callbackLast = nullptr;
_callbackOffset = 0;
}
void WalkingState::callbackLast() {
setCallback(_callbackLast, _callbackOffsetLast);
}
bool WalkingState::continueWalkingOrClearPath() {
const bool stillWalking = continueWalking();
if (!stillWalking) {
_path.clear();
}
return stillWalking;
}
bool WalkingState::continueWalking() {
const GameObject *dragon = _vm->_game->getObject(kDragonObject);
const Movement movement = static_cast<Movement> (dragon->playingAnim());
if (_turningFinished) {
// When a turning animation has finished, heroAnimationFinished() callback
// gets called, which sets this flag to true. It's important
// to not start walking right away in the callback, because
// that would disturb the data structures of the animation
// manager.
_turningFinished = false;
return walkOnNextEdge();
}
// If the current segment is the last one, we have reached the
// destination and are already facing in the right direction ===>
// return false. The code should, however, get here only if the path
// has just 1 vertex and startWalking() leaves the path open.
// Finishing and nontrivial path will get caught earlier.
if (_segment >= _path.size()) {
return false;
}
// Read the dragon's animation's current phase. Determine if it has
// changed from the last time. If not, wait until it has.
Animation *anim = dragon->_anim[movement];
const int animPhase = anim->currentFrameNum();
const bool wasUpdated = animPhase != _lastAnimPhase;
if (!wasUpdated) {
debugC(4, kDraciWalkingDebugLevel, "Waiting for an animation phase change: still %d", animPhase);
return true;
}
if (isTurningMovement(movement)) {
// If the current animation is a turning animation, wait a bit more.
debugC(3, kDraciWalkingDebugLevel, "Continuing turning for edge %d with phase %d", _segment, animPhase);
_lastAnimPhase = animPhase;
return true;
}
// We are walking in the middle of an edge. The animation phase has
// just changed.
// Read the position of the hero from the animation object, and adjust
// it to the current edge.
const Common::Point prevHero = _vm->_game->getHeroPosition();
_vm->_game->positionHeroAsAnim(anim);
const Common::Point curHero = _vm->_game->getHeroPosition();
Common::Point adjustedHero = curHero;
const bool reachedEnd = alignHeroToEdge(_path[_segment-1], _path[_segment], prevHero, &adjustedHero);
if (reachedEnd && _segment >= _path.size() - 1) {
// We don't want the dragon to jump around if we repeatedly
// click on the same pixel. Let him always end where desired.
debugC(2, kDraciWalkingDebugLevel, "Adjusting position to the final node");
adjustedHero = _path[_segment];
}
debugC(3, kDraciWalkingDebugLevel, "Continuing walking on edge %d: phase %d and position+=[%d,%d]->[%d,%d] adjusted to [%d,%d]",
_segment-1, animPhase, curHero.x - prevHero.x, curHero.y - prevHero.y, curHero.x, curHero.y, adjustedHero.x, adjustedHero.y);
// Update the hero position to the adjusted one. The animation number
// is not changing, so this will just move the sprite and return the
// current frame number.
_vm->_game->setHeroPosition(adjustedHero);
_lastAnimPhase = _vm->_game->playHeroAnimation(movement);
// If the hero has reached the end of the edge, start transition to the
// next phase. This will increment _segment, either immediately (if no
// transition is needed) or in the callback (after the transition is
// done). If the hero has arrived at a slightly different point due to
// animated sprites, adjust the path so that the animation can smoothly
// continue.
if (reachedEnd) {
if (adjustedHero != _path[_segment]) {
debugC(2, kDraciWalkingDebugLevel, "Adjusting node %d of the path [%d,%d]->[%d,%d]",
_segment, _path[_segment].x, _path[_segment].y, adjustedHero.x, adjustedHero.y);
_path[_segment] = adjustedHero;
}
return turnForTheNextSegment();
}
return true;
}
bool WalkingState::alignHeroToEdge(const Common::Point &p1, const Common::Point &p2, const Common::Point &prevHero, Common::Point *hero) {
const Movement movement = animationForDirection(p1, p2);
const Common::Point p2Diff(p2.x - p1.x, p2.y - p1.y);
if (p2Diff.x == 0 && p2Diff.y == 0) {
debugC(2, kDraciWalkingDebugLevel, "Adjusted walking edge has zero length");
// Due to changing the path vertices on the fly, this can happen.
return true;
}
bool reachedEnd;
if (movement == kMoveLeft || movement == kMoveRight) {
if (p2Diff.x == 0) {
error("Wrong value for horizontal movement");
}
reachedEnd = movement == kMoveLeft ? hero->x <= p2.x : hero->x >= p2.x;
hero->y += hero->x * p2Diff.y / p2Diff.x - prevHero.x * p2Diff.y / p2Diff.x;
} else {
if (p2Diff.y == 0) {
error("Wrong value for vertical movement");
}
reachedEnd = movement == kMoveUp ? hero->y <= p2.y : hero->y >= p2.y;
hero->x += hero->y * p2Diff.x / p2Diff.y - prevHero.y * p2Diff.x / p2Diff.y;
}
return reachedEnd;
}
bool WalkingState::turnForTheNextSegment() {
const GameObject *dragon = _vm->_game->getObject(kDragonObject);
const Movement currentAnim = static_cast<Movement> (dragon->playingAnim());
const Movement wantAnim = directionForNextPhase();
Movement transition = transitionBetweenAnimations(currentAnim, wantAnim);
debugC(2, kDraciWalkingDebugLevel, "Turning for edge %d", _segment);
if (transition == kMoveUndefined) {
// Start the next segment right away as if the turning has just finished.
return walkOnNextEdge();
} else {
// Otherwise start the transition and wait until the Animation
// class calls heroAnimationFinished() as a callback, leading
// to calling walkOnNextEdge() in the next phase.
assert(isTurningMovement(transition));
_lastAnimPhase = _vm->_game->playHeroAnimation(transition);
Animation *anim = dragon->_anim[transition];
anim->registerCallback(&Animation::tellWalkingState);
debugC(2, kDraciWalkingDebugLevel, "Starting turning animation %d with phase %d", transition, _lastAnimPhase);
return true;
}
}
void WalkingState::heroAnimationFinished() {
debugC(2, kDraciWalkingDebugLevel, "Turning callback called");
_turningFinished = true;
// We don't need to clear the callback to safer doNothing, because
// nobody ever plays this animation directly. It is only played by
// turnForTheNextSegment() and then the same callback needs to be
// activated again.
}
bool WalkingState::walkOnNextEdge() {
// The hero is turned well for the next line segment or for facing the
// target direction. It is also standing on the right spot thanks to
// the entry condition for turnForTheNextSegment().
// Start the desired next animation and retrieve the current animation
// phase.
// Don't use any callbacks, because continueWalking() will decide the
// end on its own and after walking is done callbacks shouldn't be
// called either. It wouldn't make much sense anyway, since the
// walking/staying/talking animations are cyclic.
Movement nextAnim = directionForNextPhase();
_lastAnimPhase = _vm->_game->playHeroAnimation(nextAnim);
debugC(2, kDraciWalkingDebugLevel, "Turned for edge %d, starting animation %d with phase %d", _segment, nextAnim, _lastAnimPhase);
if (++_segment < _path.size()) {
// We are on an edge: track where the hero is on this edge.
int length = WalkingMap::pointsBetween(_path[_segment-1], _path[_segment]);
debugC(2, kDraciWalkingDebugLevel, "Next edge %d has length %d", _segment-1, length);
return true;
} else {
// Otherwise we are done. continueWalking() will return false next time.
debugC(2, kDraciWalkingDebugLevel, "We have walked the whole path");
return false;
}
}
Movement WalkingState::animationForDirection(const Common::Point &here, const Common::Point &there) {
const int dx = there.x - here.x;
const int dy = there.y - here.y;
if (ABS(dx) >= ABS(dy)) {
return dx >= 0 ? kMoveRight : kMoveLeft;
} else {
return dy >= 0 ? kMoveDown : kMoveUp;
}
}
Movement WalkingState::directionForNextPhase() const {
if (_segment >= _path.size() - 1) {
return animationForSightDirection(_dir, _path[_path.size()-1], _mouse, _path, _startingDirection);
} else {
return animationForDirection(_path[_segment], _path[_segment+1]);
}
}
Movement WalkingState::transitionBetweenAnimations(Movement previous, Movement next) {
switch (next) {
case kMoveUp:
switch (previous) {
case kMoveLeft:
case kStopLeft:
case kSpeakLeft:
return kMoveLeftUp;
case kMoveRight:
case kStopRight:
case kSpeakRight:
return kMoveRightUp;
default:
return kMoveUndefined;
}
case kMoveDown:
switch (previous) {
case kMoveLeft:
case kStopLeft:
case kSpeakLeft:
return kMoveLeftDown;
case kMoveRight:
case kStopRight:
case kSpeakRight:
return kMoveRightDown;
default:
return kMoveUndefined;
}
case kMoveLeft:
switch (previous) {
case kMoveDown:
return kMoveDownLeft;
case kMoveUp:
return kMoveUpLeft;
case kMoveRight:
case kStopRight:
case kSpeakRight:
return kMoveRightLeft;
default:
return kMoveUndefined;
}
case kMoveRight:
switch (previous) {
case kMoveDown:
return kMoveDownRight;
case kMoveUp:
return kMoveUpRight;
case kMoveLeft:
case kStopLeft:
case kSpeakLeft:
return kMoveLeftRight;
default:
return kMoveUndefined;
}
case kStopLeft:
switch (previous) {
case kMoveUp:
return kMoveUpStopLeft;
case kMoveRight:
case kStopRight:
case kSpeakRight:
return kMoveRightLeft;
default:
return kMoveUndefined;
}
case kStopRight:
switch (previous) {
case kMoveUp:
return kMoveUpStopRight;
case kMoveLeft:
case kStopLeft:
case kSpeakLeft:
return kMoveLeftRight;
default:
return kMoveUndefined;
}
default:
return kMoveUndefined;
}
}
Movement WalkingState::animationForSightDirection(SightDirection dir, const Common::Point &hero, const Common::Point &mouse, const WalkingPath &path, Movement startingDirection) {
switch (dir) {
case kDirectionLeft:
return kStopLeft;
case kDirectionRight:
return kStopRight;
case kDirectionMouse:
if (mouse.x < hero.x) {
return kStopLeft;
} else if (mouse.x > hero.x) {
return kStopRight;
}
// fall through
default: {
// Find the last horizontal direction on the path.
int i = path.size() - 1;
while (i >= 0 && path[i].x == hero.x) {
--i;
}
if (i >= 0) {
return path[i].x < hero.x ? kStopRight : kStopLeft;
} else {
// Avoid changing the direction when no walking has
// been done. Preserve the original direction.
return (startingDirection == kMoveLeft || startingDirection == kStopLeft || startingDirection == kSpeakLeft)
? kStopLeft : kStopRight;
}
}
}
}
} // End of namespace Draci

201
engines/draci/walking.h Normal file
View File

@@ -0,0 +1,201 @@
/* 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 DRACI_WALKING_H
#define DRACI_WALKING_H
#include "common/array.h"
#include "common/rect.h"
namespace Draci {
class Sprite;
typedef Common::Array<Common::Point> WalkingPath;
class WalkingMap {
public:
WalkingMap() : _realWidth(0), _realHeight(0), _deltaX(1), _deltaY(1),
_mapWidth(0), _mapHeight(0), _byteWidth(0), _data(NULL) { }
void load(const byte *data, uint length);
bool getPixel(int x, int y) const;
bool isWalkable(const Common::Point &p) const;
Sprite *newOverlayFromMap(byte color) const;
Common::Point findNearestWalkable(int x, int y) const;
bool findShortestPath(Common::Point p1, Common::Point p2, WalkingPath *path) const;
void obliquePath(const WalkingPath& path, WalkingPath *obliquedPath);
Sprite *newOverlayFromPath(const WalkingPath &path, byte color) const;
Common::Point getDelta() const { return Common::Point(_deltaX, _deltaY); }
static int pointsBetween(const Common::Point &p1, const Common::Point &p2);
static Common::Point interpolate(const Common::Point &p1, const Common::Point &p2, int i, int n);
private:
int _realWidth, _realHeight;
int _deltaX, _deltaY;
int _mapWidth, _mapHeight;
int _byteWidth;
// We don't own the pointer. It points to the BArchive cache for this room.
const byte *_data;
// 4 possible directions to walk from a pixel.
static const int kDirections[][2];
void drawOverlayRectangle(const Common::Point &p, byte color, byte *buf) const;
bool lineIsCovered(const Common::Point &p1, const Common::Point &p2) const;
// Returns true if the number of vertices on the path was decreased.
bool managedToOblique(WalkingPath *path) const;
};
/*
* Enumerates the directions the dragon can look into when arrived.
*/
enum SightDirection {
kDirectionLast, kDirectionMouse, kDirectionUnknown,
kDirectionRight, kDirectionLeft, kDirectionIntelligent
};
/**
* Enumerates the animations for the dragon's movement.
*/
enum Movement {
kMoveUndefined = -1,
kMoveDown, kMoveUp, kMoveRight, kMoveLeft,
kFirstTurning,
kMoveRightDown = kFirstTurning, kMoveRightUp, kMoveLeftDown, kMoveLeftUp,
kMoveDownRight, kMoveUpRight, kMoveDownLeft, kMoveUpLeft,
kMoveLeftRight, kMoveRightLeft, kMoveUpStopLeft, kMoveUpStopRight,
kLastTurning = kMoveUpStopRight,
kSpeakRight, kSpeakLeft, kStopRight, kStopLeft,
kFirstTemporaryAnimation
};
class DraciEngine;
struct GPL2Program;
class WalkingState {
public:
explicit WalkingState(DraciEngine *vm) : _vm(vm) {
_dir = kDirectionLast;
_startingDirection = kMoveUndefined;
_segment = 0;
_lastAnimPhase = 0;
_turningFinished = 0;
_callbackOffset = 0;
_callbackOffsetLast = 0;
_callbackLast = 0;
stopWalking();
}
~WalkingState() {}
void stopWalking();
void startWalking(const Common::Point &p1, const Common::Point &p2,
const Common::Point &mouse, SightDirection dir,
const Common::Point &delta, const WalkingPath& path);
const WalkingPath& getPath() const { return _path; }
void setCallback(const GPL2Program *program, uint16 offset);
void callback();
void callbackLast();
bool isActive() const { return _path.size() > 0; }
// Advances the hero along the path and changes animation accordingly.
// Walking MUST be active when calling this method. When the hero has
// arrived to the target, returns false, but leaves the callback
// untouched (the caller must call it).
// The second variant also clears the path when returning false.
bool continueWalking();
bool continueWalkingOrClearPath();
// Called when the hero's turning animation has finished.
void heroAnimationFinished();
// Returns the hero's animation corresponding to looking into given
// direction. The direction can be smart and in that case this
// function needs to know the whole last path, the current position of
// the hero, or the mouse position.
static Movement animationForSightDirection(SightDirection dir, const Common::Point &hero, const Common::Point &mouse, const WalkingPath &path, Movement startingDirection);
private:
DraciEngine *_vm;
WalkingPath _path;
Common::Point _mouse;
SightDirection _dir;
Movement _startingDirection;
uint _segment; // Index of the path vertex we are currently going to / rotation on
int _lastAnimPhase;
bool _turningFinished;
const GPL2Program *_callback;
const GPL2Program *_callbackLast;
uint16 _callbackOffset;
uint16 _callbackOffsetLast;
// Initiates turning of the dragon into the direction for the next
// segment / after walking. Returns false when there is nothing left
// to do and walking is done.
bool turnForTheNextSegment();
// Starts walking on the next edge. Returns false if we are already at
// the final vertex and walking is done.
bool walkOnNextEdge();
// Return one of the 4 animations kMove{Down,Up,Right,Left}
// corresponding to walking from here to there.
static Movement animationForDirection(const Common::Point &here, const Common::Point &there);
// Returns the desired facing direction to begin the next phase of the
// walk. It's either a direction for the given edge or the desired
// final direction.
Movement directionForNextPhase() const;
// Returns either animation that needs to be played between given two
// animations (e.g., kMoveRightDown after kMoveRight and before
// kMoveDown), or kMoveUndefined if none animation is to be played.
static Movement transitionBetweenAnimations(Movement previous, Movement next);
static bool isTurningMovement(Movement m) {
return m >= kFirstTurning && m <= kLastTurning;
}
// Projects hero to the given edge. Returns true when hero has reached
// at least p2. prevHero is passed so that we can compute how much to
// adjust in the other-than-walking direction.
static bool alignHeroToEdge(const Common::Point &p1, const Common::Point &p2, const Common::Point &prevHero, Common::Point *hero);
};
} // End of namespace Draci
#endif // DRACI_WALKING_H