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

View File

@@ -0,0 +1,3 @@
engines/alcachofa/detection.cpp
engines/alcachofa/graphics-opengl.cpp
engines/alcachofa/metaengine.cpp

View File

@@ -0,0 +1,493 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/config-manager.h"
#include "common/debug-channels.h"
#include "common/events.h"
#include "common/savefile.h"
#include "common/system.h"
#include "engines/util.h"
#include "graphics/paletteman.h"
#include "graphics/framelimiter.h"
#include "graphics/thumbnail.h"
#include "graphics/managed_surface.h"
#include "image/png.h"
#include "video/avi_decoder.h"
#include "video/mpegps_decoder.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/metaengine.h"
#include "alcachofa/console.h"
#include "alcachofa/detection.h"
#include "alcachofa/player.h"
#include "alcachofa/rooms.h"
#include "alcachofa/script.h"
#include "alcachofa/global-ui.h"
#include "alcachofa/menu.h"
#include "alcachofa/debug.h"
#include "alcachofa/game.h"
using namespace Math;
using namespace Graphics;
namespace Alcachofa {
constexpr uint kDefaultFramerate = 100; // the original target framerate, not critical
AlcachofaEngine *g_engine;
AlcachofaEngine::AlcachofaEngine(OSystem *syst, const AlcachofaGameDescription *gameDesc)
: Engine(syst)
, _gameDescription(gameDesc)
, _eventLoopSemaphore("engine") {
assert(gameDesc != nullptr);
g_engine = this;
}
AlcachofaEngine::~AlcachofaEngine() {
// do not delete, this is done by `Engine::~Engine` with `delete _debugger;`
_console = nullptr; //-V773
}
uint32 AlcachofaEngine::getFeatures() const {
return _gameDescription->desc.flags;
}
Common::String AlcachofaEngine::getGameId() const {
return _gameDescription->desc.gameId;
}
Common::Error AlcachofaEngine::run() {
g_system->showMouse(false);
setDebugger(_console);
_game.reset(Game::createForMovieAdventure());
_renderer.reset(IRenderer::createOpenGLRenderer(Common::Point(1024, 768)));
_drawQueue.reset(new DrawQueue(_renderer.get()));
_world.reset(new World());
_script.reset(new Script());
_player.reset(new Player());
_globalUI.reset(new GlobalUI());
_menu.reset(new Menu());
setMillis(0);
game().onLoadedGameFiles();
if (!tryLoadFromLauncher()) {
_script->createProcess(MainCharacterKind::None, "CREDITOS_INICIALES");
_scheduler.run();
// we run once to set the initial room, otherwise we could run into currentRoom == nullptr
}
Common::Event e;
Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
while (!shouldQuit()) {
_input.nextFrame();
while (g_system->getEventManager()->pollEvent(e)) {
if (_input.handleEvent(e))
continue;
if (e.type == EVENT_CUSTOM_ENGINE_ACTION_START &&
e.customType == (CustomEventType)EventAction::LoadFromMenu)
menu().triggerLoad();
}
_sounds.update();
_renderer->begin();
_drawQueue->clear();
_camera.shake() = Vector2d();
_player->preUpdate();
if (_player->currentRoom() != nullptr)
_player->currentRoom()->update();
if (_player->currentRoom() != nullptr) // update() might clear currentRoom
_player->currentRoom()->draw();
_player->postUpdate();
if (_debugHandler != nullptr)
_debugHandler->update();
_renderer->end();
// Delay for a bit. All events loops should have a delay
// to prevent the system being unduly loaded
if (!_renderer->hasOutput()) {
limiter.delayBeforeSwap();
g_system->updateScreen();
}
// else we just rendered to some surface and will use it in the next frame
// no need to update the screen or wait
limiter.startFrame();
}
return Common::kNoError;
}
void AlcachofaEngine::playVideo(int32 videoId) {
if (game().isKnownBadVideo(videoId)) {
warning("Skipping known bad video %d", videoId);
return;
}
// Video files are either MPEG PS or AVI
FakeLock lock("playVideo", _eventLoopSemaphore);
File *file = new File();
if (!file->open(Path(Common::String::format("Data/DATA%02d.BIN", videoId + 1)))) {
delete file;
game().invalidVideo(videoId, "open file");
return;
}
char magic[4];
if (file->read(magic, sizeof(magic)) != sizeof(magic) || !file->seek(0)) {
delete file;
game().invalidVideo(videoId, "read magic");
return;
}
ScopedPtr<Video::VideoDecoder> decoder;
if (memcmp(magic, "RIFF", sizeof(magic)) == 0)
decoder.reset(new Video::AVIDecoder());
else
decoder.reset(new Video::MPEGPSDecoder());
if (!decoder->loadStream(file)) {
game().invalidVideo(videoId, "decode video");
return;
}
decoder->setOutputPixelFormat(g_engine->renderer().getPixelFormat());
Vector2d texMax(1.0f, 1.0f);
int16 texWidth = decoder->getWidth(), texHeight = decoder->getHeight();
ManagedSurface tmpSurface;
if (_renderer->requiresPoTTextures() &&
(!isPowerOfTwo(texWidth) || !isPowerOfTwo(texHeight))) {
texWidth = nextHigher2(texWidth);
texHeight = nextHigher2(texHeight);
texMax = {
decoder->getWidth() / (float)texWidth,
decoder->getHeight() / (float)texHeight,
};
tmpSurface.create(texWidth, texHeight, _renderer->getPixelFormat());
}
auto texture = _renderer->createTexture(texWidth, texHeight, false);
Common::Event e;
_sounds.stopAll();
decoder->start();
while (!decoder->endOfVideo() && !shouldQuit()) {
if (decoder->needsUpdate()) {
auto surface = decoder->decodeNextFrame();
if (surface) {
if (tmpSurface.empty())
texture->update(*surface);
else {
tmpSurface.blitFrom(*surface);
texture->update(tmpSurface);
}
}
_renderer->begin();
_renderer->setBlendMode(BlendMode::Alpha);
_renderer->setLodBias(0.0f);
_renderer->setTexture(texture.get());
_renderer->quad(
{},
{ (float)g_system->getWidth(), (float)g_system->getHeight() },
kWhite,
{},
{},
texMax);
_renderer->end();
g_system->updateScreen();
}
_input.nextFrame();
while (g_system->getEventManager()->pollEvent(e)) {
if (_input.handleEvent(e))
continue;
}
if (_input.wasAnyMouseReleased() || _input.wasMenuKeyPressed())
break;
g_system->delayMillis(decoder->getTimeToNextFrame());
}
decoder->stop();
}
void AlcachofaEngine::fadeExit() {
constexpr uint kFadeOutDuration = 1000;
FakeLock lock("fadeExit", _eventLoopSemaphore);
Event e;
Graphics::FrameLimiter limiter(g_system, kDefaultFramerate, false);
uint32 startTime = g_system->getMillis();
Room *room = g_engine->player().currentRoom();
_renderer->end(); // we were in a frame, let's exit
while (g_system->getMillis() - startTime < kFadeOutDuration && !shouldQuit()) {
_input.nextFrame();
while (g_system->getEventManager()->pollEvent(e)) {
if (_input.handleEvent(e))
continue;
}
_renderer->begin();
_drawQueue->clear();
float t = ((float)(g_system->getMillis() - startTime)) / kFadeOutDuration;
_drawQueue->add<FadeDrawRequest>(FadeType::ToBlack, t, -kForegroundOrderCount);
if (room != nullptr)
room->draw(); // this executes the drawQueue as well
_renderer->end();
limiter.delayBeforeSwap();
g_system->updateScreen();
limiter.startFrame();
}
quitGame();
player().changeRoom("SALIR", false); // this skips some update steps along the way
}
void AlcachofaEngine::setDebugMode(DebugMode mode, int32 param) {
switch (mode) {
case DebugMode::ClosestFloorPoint:
_debugHandler.reset(new ClosestFloorPointDebugHandler(param));
break;
case DebugMode::FloorIntersections:
_debugHandler.reset(new FloorIntersectionsDebugHandler(param));
break;
case DebugMode::TeleportCharacter:
_debugHandler.reset(new TeleportCharacterDebugHandler(param));
break;
case DebugMode::FloorAlpha:
_debugHandler.reset(FloorColorDebugHandler::create(param, false));
break;
case DebugMode::FloorColor:
_debugHandler.reset(FloorColorDebugHandler::create(param, true));
break;
default:
_debugHandler.reset(nullptr);
break;
}
_input.toggleDebugInput(isDebugModeActive());
}
uint32 AlcachofaEngine::getMillis() const {
// Time is stored in savestate at various points e.g. to persist animation progress
// We wrap the system-provided time to offset it to the expected game-time
// This would also double as playtime
return g_system->getMillis() - _timeNegOffset + _timePosOffset;
}
void AlcachofaEngine::setMillis(uint32 newMillis) {
const uint32 sysMillis = g_system->getMillis();
if (newMillis > sysMillis) {
_timeNegOffset = 0;
_timePosOffset = newMillis - sysMillis;
} else {
_timeNegOffset = sysMillis - newMillis;
_timePosOffset = 0;
}
}
void AlcachofaEngine::pauseEngineIntern(bool pause) {
// Audio::Mixer also implements recursive pausing,
// so ScummVM pausing and Menu pausing will not conflict
_sounds.pauseAll(pause);
if (pause)
_timeBeforePause = getMillis();
else
setMillis(_timeBeforePause);
}
bool AlcachofaEngine::canLoadGameStateCurrently(U32String *msg) {
if (!_eventLoopSemaphore.isReleased())
return false;
return
(menu().isOpen() && menu().interactionSemaphore().isReleased()) ||
player().isAllowedToOpenMenu();
}
Common::String AlcachofaEngine::getSaveStatePattern() {
return getMetaEngine()->getSavegameFilePattern(_targetName.c_str());
}
Common::Error AlcachofaEngine::syncGame(MySerializer &s) {
if (!s.syncVersion((Serializer::Version)kCurrentSaveVersion))
return { kUnknownError, "Gamestate version is higher than expected" };
Graphics::ManagedSurface *thumbnail = nullptr;
if (s.isSaving()) {
thumbnail = new Graphics::ManagedSurface();
getSavegameThumbnail(*thumbnail->surfacePtr());
}
bool couldSyncThumbnail = syncThumbnail(s, thumbnail);
if (thumbnail != nullptr)
delete thumbnail;
if (!couldSyncThumbnail)
return { kUnknownError, "Could not read thumbnail" };
uint32 millis = menu().isOpen()
? menu().millisBeforeMenu()
: getMillis();
s.syncAsUint32LE(millis);
if (s.isLoading())
setMillis(millis);
/* Some notes about the order:
* 1. The scheduler should prepare due to our FakeSemaphores
* By destructing all previous processes we also release all locks and
* can assert that the semaphores are released on loading.
* 2. The player should come late as it changes the room
* 3. With the room current, the tasks can now better find the referenced objects
* 4. Redundant: The world has to be synced before the tasks to reset the semaphores to 0
*/
scheduler().prepareSyncGame(s);
world().syncGame(s);
camera().syncGame(s);
script().syncGame(s);
globalUI().syncGame(s);
player().syncGame(s);
scheduler().syncGame(s);
if (s.isLoading()) {
menu().resetAfterLoad();
sounds().stopAll();
sounds().setMusicToRoom(player().currentRoom()->musicID());
}
return Common::kNoError;
}
static constexpr uint32 kNoThumbnailMagicValue = 0xBADBAD;
bool AlcachofaEngine::syncThumbnail(MySerializer &s, Graphics::ManagedSurface *thumbnail) {
if (s.isLoading()) {
auto prevPosition = s.readStream().pos();
Image::PNGDecoder pngDecoder;
if (pngDecoder.loadStream(s.readStream()) && pngDecoder.getSurface() != nullptr) {
if (thumbnail != nullptr) {
thumbnail->free();
thumbnail->copyFrom(*pngDecoder.getSurface());
}
} else {
// If we do not get a thumbnail, maybe we get at least the marker that there is no thumbnail
s.readStream().seek(prevPosition, SEEK_SET);
uint32 magicValue = s.readStream().readUint32LE();
if (magicValue != kNoThumbnailMagicValue)
return false; // the savegame is not valid
else // this is not an error, just a pity
warning("No thumbnail stored in in-game savestate");
}
} else {
if (thumbnail == nullptr ||
thumbnail->getPixels() == nullptr ||
!Image::writePNG(s.writeStream(), *thumbnail)) {
// We were not able to get a thumbnail, save a value that denotes that situation
warning("Could not save in-game thumbnail");
s.writeStream().writeUint32LE(kNoThumbnailMagicValue);
}
}
return true;
}
void AlcachofaEngine::getSavegameThumbnail(Graphics::Surface &thumbnail) {
thumbnail.free();
auto *bigThumbnail = g_engine->menu().getBigThumbnail();
if (bigThumbnail != nullptr) {
// we still have a one from the in-game menu opening, reuse that
thumbnail.copyFrom(*bigThumbnail);
return;
}
// otherwise we have to rerender
thumbnail.create(kBigThumbnailWidth, kBigThumbnailHeight, g_engine->renderer().getPixelFormat());
if (g_engine->player().currentRoom() == nullptr)
return; // but without a room we would render only black anyway
g_engine->drawQueue().clear();
g_engine->renderer().begin();
g_engine->renderer().setOutput(thumbnail);
g_engine->player().currentRoom()->draw(); // drawQueue is drawn here as well
g_engine->renderer().end();
// we should be within the event loop. as such it is quite safe to mess with the drawQueue or renderer
}
bool AlcachofaEngine::tryLoadFromLauncher() {
int saveSlot = ConfMan.getInt("save_slot");
if (!ConfMan.hasKey("save_slot") || saveSlot < 0)
return false;
auto *saveFile = g_system->getSavefileManager()->openForLoading(getSaveStateName(saveSlot));
if (saveFile == nullptr)
return false;
bool result = loadGameStream(saveFile).getCode() == kNoError;
delete saveFile;
return result;
}
Config::Config() {
loadFromScummVM();
}
void Config::loadFromScummVM() {
_musicVolume = (uint8)CLIP(ConfMan.getInt("music_volume"), 0, 255);
_speechVolume = (uint8)CLIP(ConfMan.getInt("speech_volume"), 0, 255);
_subtitles = ConfMan.getBool("subtitles");
_highQuality = ConfMan.getBool("high_quality");
_bits32 = ConfMan.getBool("32_bits");
}
void Config::saveToScummVM() {
ConfMan.setBool("subtitles", _subtitles);
ConfMan.setBool("high_quality", _highQuality);
ConfMan.setBool("32_bits", _bits32);
ConfMan.setInt("music_volume", _musicVolume);
ConfMan.setInt("speech_volume", _speechVolume);
ConfMan.setInt("sfx_volume", _speechVolume);
ConfMan.flushToDisk();
// ^ a bit unfortunate, that means if you change in-game it overrides.
// if you set it in ScummVMs dialog it sticks
}
DelayTask::DelayTask(Process &process, uint32 millis)
: Task(process)
, _endTime(millis) {}
DelayTask::DelayTask(Process &process, Serializer &s)
: Task(process) {
DelayTask::syncGame(s);
}
TaskReturn DelayTask::run() {
TASK_BEGIN;
_endTime += g_engine->getMillis();
while (g_engine->getMillis() < _endTime)
TASK_YIELD(1);
TASK_END;
}
void DelayTask::debugPrint() {
uint32 remaining = g_engine->getMillis() <= _endTime ? _endTime - g_engine->getMillis() : 0;
g_engine->getDebugger()->debugPrintf("Delay for further %ums\n", remaining);
}
void DelayTask::syncGame(Serializer &s) {
Task::syncGame(s);
s.syncAsUint32LE(_endTime);
}
DECLARE_TASK(DelayTask)
} // End of namespace Alcachofa

View File

@@ -0,0 +1,203 @@
/* 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 ALCACHOFA_H
#define ALCACHOFA_H
#include "common/system.h"
#include "common/error.h"
#include "common/fs.h"
#include "common/hash-str.h"
#include "common/random.h"
#include "common/serializer.h"
#include "common/util.h"
#include "engines/engine.h"
#include "engines/savestate.h"
#include "graphics/screen.h"
#include "alcachofa/detection.h"
#include "alcachofa/camera.h"
#include "alcachofa/input.h"
#include "alcachofa/sounds.h"
#include "alcachofa/player.h"
#include "alcachofa/scheduler.h"
#include "alcachofa/console.h"
#include "alcachofa/game.h"
namespace Alcachofa {
class IDebugHandler;
class IRenderer;
class DrawQueue;
class World;
class Script;
class GlobalUI;
class Menu;
class Game;
struct AlcachofaGameDescription;
constexpr int16 kSmallThumbnailWidth = 160; // for ScummVM
constexpr int16 kSmallThumbnailHeight = 120;
static constexpr int16 kBigThumbnailWidth = 341; // for in-game
static constexpr int16 kBigThumbnailHeight = 256;
enum class SaveVersion : Common::Serializer::Version {
Initial = 0
};
static constexpr SaveVersion kCurrentSaveVersion = SaveVersion::Initial;
class MySerializer : public Common::Serializer {
public:
MySerializer(Common::SeekableReadStream *in, Common::WriteStream *out) : Common::Serializer(in, out) {}
Common::SeekableReadStream &readStream() {
assert(isLoading() && _loadStream != nullptr);
return *_loadStream;
}
Common::WriteStream &writeStream() {
assert(isSaving() && _saveStream != nullptr);
return *_saveStream;
}
};
class Config {
public:
Config();
inline bool &subtitles() { return _subtitles; }
inline bool &highQuality() { return _highQuality; }
inline bool &bits32() { return _bits32; }
inline uint8 &musicVolume() { return _musicVolume; }
inline uint8 &speechVolume() { return _speechVolume; }
void loadFromScummVM();
void saveToScummVM();
private:
bool
_subtitles = true,
_highQuality = true,
_bits32 = true;
uint8
_musicVolume = 255,
_speechVolume = 255;
};
class AlcachofaEngine : public Engine {
protected:
// Engine APIs
Common::Error run() override;
public:
AlcachofaEngine(OSystem *syst, const AlcachofaGameDescription *gameDesc);
~AlcachofaEngine() override;
inline EngineVersion version() const { return gameDescription().engineVersion; }
inline bool isV1() const { return gameDescription().isVersionBetween(10, 19); }
inline bool isV2() const { return gameDescription().isVersionBetween(20, 29); }
inline bool isV3() const { return gameDescription().isVersionBetween(30, 39); }
inline const AlcachofaGameDescription &gameDescription() const { return *_gameDescription; }
inline IRenderer &renderer() { return *_renderer; }
inline DrawQueue &drawQueue() { return *_drawQueue; }
inline Camera &camera() { return _camera; }
inline Input &input() { return _input; }
inline Sounds &sounds() { return _sounds; }
inline Player &player() { return *_player; }
inline World &world() { return *_world; }
inline Script &script() { return *_script; }
inline GlobalUI &globalUI() { return *_globalUI; }
inline Menu &menu() { return *_menu; }
inline Scheduler &scheduler() { return _scheduler; }
inline Console &console() { return *_console; }
inline Game &game() { return *_game; }
inline Config &config() { return _config; }
inline bool isDebugModeActive() const { return _debugHandler != nullptr; }
uint32 getMillis() const;
void setMillis(uint32 newMillis);
void pauseEngineIntern(bool pause) override;
void playVideo(int32 videoId);
void fadeExit();
void setDebugMode(DebugMode debugMode, int32 param);
uint32 getFeatures() const;
Common::String getGameId() const;
bool hasFeature(EngineFeature f) const override {
return
(f == kSupportsLoadingDuringRuntime) ||
(f == kSupportsSavingDuringRuntime) ||
(f == kSupportsReturnToLauncher);
};
bool canLoadGameStateCurrently(Common::U32String *msg = nullptr) override;
bool canSaveGameStateCurrently(Common::U32String *msg = nullptr) override {
return canLoadGameStateCurrently(msg);
}
Common::String getSaveStatePattern();
Common::Error syncGame(MySerializer &s);
Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave = false) override {
assert(stream != nullptr);
MySerializer s(nullptr, stream);
return syncGame(s);
}
Common::Error loadGameStream(Common::SeekableReadStream *stream) override {
assert(stream != nullptr);
MySerializer s(stream, nullptr);
return syncGame(s);
}
bool syncThumbnail(MySerializer &s, Graphics::ManagedSurface *thumbnail);
void getSavegameThumbnail(Graphics::Surface &thumbnail);
private:
bool tryLoadFromLauncher();
const AlcachofaGameDescription *_gameDescription;
Console *_console = new Console();
Common::ScopedPtr<IDebugHandler> _debugHandler;
Common::ScopedPtr<IRenderer> _renderer;
Common::ScopedPtr<DrawQueue> _drawQueue;
Common::ScopedPtr<World> _world;
Common::ScopedPtr<Script> _script;
Common::ScopedPtr<Player> _player;
Common::ScopedPtr<GlobalUI> _globalUI;
Common::ScopedPtr<Menu> _menu;
Common::ScopedPtr<Game> _game;
Camera _camera;
Input _input;
Sounds _sounds;
Scheduler _scheduler;
Config _config;
FakeSemaphore _eventLoopSemaphore; // for special states like playVideo and fadeExit
uint32 _timeNegOffset = 0, _timePosOffset = 0;
uint32 _timeBeforePause = 0;
};
extern AlcachofaEngine *g_engine;
#define SHOULD_QUIT ::Alcachofa::g_engine->shouldQuit()
} // End of namespace Alcachofa
#endif // ALCACHOFA_H

View File

@@ -0,0 +1,671 @@
/* 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 "alcachofa/camera.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/script.h"
#include "common/system.h"
#include "math/vector4d.h"
using namespace Common;
using namespace Math;
namespace Alcachofa {
void Camera::resetRotationAndScale() {
_cur._scale = 1;
_cur._rotation = 0;
_cur._usedCenter.z() = 0;
}
void Camera::setRoomBounds(Point bgSize, int16 bgScale) {
float scaleFactor = 1 - bgScale * kInvBaseScale;
_roomMin = Vector2d(
g_system->getWidth() / 2 * scaleFactor,
g_system->getHeight() / 2 * scaleFactor);
_roomMax = _roomMin + Vector2d(
bgSize.x * bgScale * kInvBaseScale,
bgSize.y * bgScale * kInvBaseScale);
_roomScale = bgScale;
}
void Camera::setFollow(WalkingCharacter *target, bool catchUp) {
_cur._isFollowingTarget = target != nullptr;
_followTarget = target;
_lastUpdateTime = g_engine->getMillis();
_catchUp = catchUp;
if (target == nullptr)
_isChanging = false;
}
void Camera::setPosition(Vector2d v) {
setPosition({ v.getX(), v.getY(), _cur._usedCenter.z() });
}
void Camera::setPosition(Vector3d v) {
_cur._usedCenter = v;
setFollow(nullptr);
}
void Camera::backup(uint slot) {
assert(slot < kStateBackupCount);
_backups[slot] = _cur;
}
void Camera::restore(uint slot) {
assert(slot < kStateBackupCount);
auto backupState = _backups[slot];
_backups[slot] = _cur;
_cur = backupState;
}
static Matrix4 scale2DMatrix(float scale) {
Matrix4 m;
m(0, 0) = scale;
m(1, 1) = scale;
return m;
}
void Camera::setupMatricesAround(Vector3d center) {
Matrix4 matTemp;
matTemp.buildAroundZ(_cur._rotation);
_mat3Dto2D.setToIdentity();
_mat3Dto2D.translate(-center);
_mat3Dto2D = matTemp * _mat3Dto2D;
_mat3Dto2D = scale2DMatrix(_cur._scale) * _mat3Dto2D;
_mat2Dto3D.setToIdentity();
_mat2Dto3D.translate(center);
matTemp.buildAroundZ(-_cur._rotation);
matTemp = matTemp * scale2DMatrix(1 / _cur._scale);
_mat2Dto3D = _mat2Dto3D * matTemp;
}
void minmax(Vector3d &min, Vector3d &max, Vector3d val) {
min.set(
MIN(min.x(), val.x()),
MIN(min.y(), val.y()),
MIN(min.z(), val.z()));
max.set(
MAX(max.x(), val.x()),
MAX(max.y(), val.y()),
MAX(max.z(), val.z()));
}
Vector3d Camera::setAppliedCenter(Vector3d center) {
setupMatricesAround(center);
if (g_engine->game().shouldClipCamera()) {
const float screenW = g_system->getWidth(), screenH = g_system->getHeight();
Vector3d min, max;
min = max = transform2Dto3D(Vector3d(0, 0, _roomScale));
minmax(min, max, transform2Dto3D(Vector3d(screenW, 0, _roomScale)));
minmax(min, max, transform2Dto3D(Vector3d(screenW, screenH, _roomScale)));
minmax(min, max, transform2Dto3D(Vector3d(0, screenH, _roomScale)));
center.x() += MAX(0.0f, _roomMin.getX() - min.x());
center.y() += MAX(0.0f, _roomMin.getY() - min.y());
center.x() -= MAX(0.0f, max.x() - _roomMax.getX());
center.y() -= MAX(0.0f, max.y() - _roomMax.getY());
setupMatricesAround(center);
}
return _appliedCenter = center;
}
Vector3d Camera::transform2Dto3D(Vector3d v2d) const {
// if this looks like normal 3D math to *someone* please contact.
Vector4d vh;
vh.w() = 1.0f;
vh.z() = v2d.z() - _cur._usedCenter.z();
vh.y() = (v2d.y() - g_system->getHeight() * 0.5f) * vh.z() * kInvBaseScale;
vh.x() = (v2d.x() - g_system->getWidth() * 0.5f) * vh.z() * kInvBaseScale;
vh = _mat2Dto3D * vh;
return Vector3d(vh.x(), vh.y(), 0.0f);
}
Vector3d Camera::transform3Dto2D(Vector3d v3d) const {
// I swear there is a better way than this. This is stupid. But it is original.
float depthScale = v3d.z() * kInvBaseScale;
Vector4d vh;
vh.x() = v3d.x() * depthScale + (1 - depthScale) * g_system->getWidth() * 0.5f;
vh.y() = v3d.y() * depthScale + (1 - depthScale) * g_system->getHeight() * 0.5f;
vh.z() = v3d.z();
vh.w() = 1.0f;
vh = _mat3Dto2D * vh;
return Vector3d(
g_system->getWidth() * 0.5f + vh.x() * kBaseScale / vh.z(),
g_system->getHeight() * 0.5f + vh.y() * kBaseScale / vh.z(),
_cur._scale * kBaseScale / vh.z());
}
Point Camera::transform3Dto2D(Point p3d) const {
auto v2d = transform3Dto2D({ (float)p3d.x, (float)p3d.y, kBaseScale });
return { (int16)v2d.x(), (int16)v2d.y() };
}
void Camera::update() {
// original would be some smoothing of delta times, let's not.
uint32 now = g_engine->getMillis();
float deltaTime = (now - _lastUpdateTime) / 1000.0f;
deltaTime = MAX(0.001f, MIN(0.5f, deltaTime));
_lastUpdateTime = now;
if (_catchUp) {
for (int i = 0; i < 4; i++)
updateFollowing(50.0f);
_catchUp = false;
} else
updateFollowing(deltaTime);
setAppliedCenter(_cur._usedCenter + Vector3d(_shake.getX(), _shake.getY(), 0.0f));
}
void Camera::updateFollowing(float deltaTime) {
if (!_cur._isFollowingTarget || _followTarget == nullptr)
return;
const float resolutionFactor = g_system->getWidth() * 0.00125f;
const float acceleration = 460 * resolutionFactor;
const float baseDeadZoneSize = 25 * resolutionFactor;
const float minSpeed = 20 * resolutionFactor;
const float maxSpeed = this->_cur._maxSpeedFactor * resolutionFactor;
const float depthScale = _followTarget->graphic()->depthScale();
const auto characterPolygon = _followTarget->shape()->at(0);
const float halfHeight = ABS(characterPolygon._points[0].y - characterPolygon._points[2].y) / 2.0f;
Vector3d targetCenter = setAppliedCenter({
_shake.getX() + _followTarget->position().x,
_shake.getY() + _followTarget->position().y - depthScale * 85,
_cur._usedCenter.z() });
targetCenter.y() -= halfHeight;
float distanceToTarget = as2D(_cur._usedCenter - targetCenter).getMagnitude();
float moveDistance = _followTarget->stepSizeFactor() * _cur._speed * deltaTime;
float deadZoneSize = baseDeadZoneSize / _cur._scale;
if (_followTarget->isWalking() && depthScale > 0.8f)
deadZoneSize = (baseDeadZoneSize + (depthScale - 0.8f) * 200) / _cur._scale;
bool isFarAway = false;
if (ABS(targetCenter.x() - _cur._usedCenter.x()) > deadZoneSize ||
ABS(targetCenter.y() - _cur._usedCenter.y()) > deadZoneSize) {
isFarAway = true;
_cur._isBraking = false;
_isChanging = true;
}
if (_cur._isBraking) {
_cur._speed -= acceleration * 0.9f * deltaTime;
_cur._speed = MAX(_cur._speed, minSpeed);
}
if (_isChanging && !_cur._isBraking) {
_cur._speed += acceleration * deltaTime;
_cur._speed = MIN(_cur._speed, maxSpeed);
if (!isFarAway)
_cur._isBraking = true;
}
if (_isChanging) {
if (distanceToTarget <= moveDistance) {
_cur._usedCenter = targetCenter;
_isChanging = false;
_cur._isBraking = false;
} else {
Vector3d deltaCenter = targetCenter - _cur._usedCenter;
deltaCenter.z() = 0.0f;
_cur._usedCenter += deltaCenter * moveDistance / distanceToTarget;
}
}
}
static void syncMatrix(Serializer &s, Matrix4 &m) {
float *data = m.getData();
for (int i = 0; i < 16; i++)
s.syncAsFloatLE(data[i]);
}
static void syncVector(Serializer &s, Vector3d &v) {
s.syncAsFloatLE(v.x());
s.syncAsFloatLE(v.y());
s.syncAsFloatLE(v.z());
}
void Camera::State::syncGame(Serializer &s) {
syncVector(s, _usedCenter);
s.syncAsFloatLE(_scale);
s.syncAsFloatLE(_speed);
s.syncAsFloatLE(_maxSpeedFactor);
float rotationDegs = _rotation.getDegrees();
s.syncAsFloatLE(rotationDegs);
_rotation.setDegrees(rotationDegs);
s.syncAsByte(_isBraking);
s.syncAsByte(_isFollowingTarget);
}
void Camera::syncGame(Serializer &s) {
syncMatrix(s, _mat3Dto2D);
syncMatrix(s, _mat2Dto3D);
syncVector(s, _appliedCenter);
s.syncAsUint32LE(_lastUpdateTime);
s.syncAsByte(_isChanging);
_cur.syncGame(s);
for (uint i = 0; i < kStateBackupCount; i++)
_backups[i].syncGame(s);
// originally the follow object is also searched for before changing the room
// so that would practically mean only the main characters could be reasonably found
// instead we fall back to global search
String name;
if (_followTarget != nullptr)
name = _followTarget->name();
s.syncString(name);
if (s.isLoading()) {
if (name.empty())
_followTarget = nullptr;
else {
_followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByName(name.c_str()));
if (_followTarget == nullptr)
_followTarget = dynamic_cast<WalkingCharacter *>(g_engine->world().getObjectByNameFromAnyRoom(name.c_str()));
if (_followTarget == nullptr)
warning("Camera follow target from savestate was not found: %s", name.c_str());
}
}
}
struct CamLerpTask : public Task {
CamLerpTask(Process &process, uint32 duration = 0, EasingType easingType = EasingType::Linear)
: Task(process)
, _camera(g_engine->camera())
, _duration(duration)
, _easingType(easingType) {}
TaskReturn run() override {
TASK_BEGIN;
_startTime = g_engine->getMillis();
while (g_engine->getMillis() - _startTime < _duration) {
update(ease((g_engine->getMillis() - _startTime) / (float)_duration, _easingType));
_camera._isChanging = true;
TASK_YIELD(1);
}
update(1.0f);
TASK_END;
}
void debugPrint() override {
uint32 remaining = g_engine->getMillis() - _startTime <= _duration
? _duration - (g_engine->getMillis() - _startTime)
: 0;
g_engine->console().debugPrintf("%s camera with %ums remaining\n", taskName(), remaining);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
s.syncAsUint32LE(_startTime);
s.syncAsUint32LE(_duration);
syncEnum(s, _easingType);
}
protected:
virtual void update(float t) = 0;
Camera &_camera;
uint32 _startTime = 0, _duration;
EasingType _easingType;
};
struct CamLerpPosTask final : public CamLerpTask {
CamLerpPosTask(Process &process, Vector3d targetPos, int32 duration, EasingType easingType)
: CamLerpTask(process, duration, easingType)
, _fromPos(_camera._appliedCenter)
, _deltaPos(targetPos - _camera._appliedCenter) {}
CamLerpPosTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
syncVector(s, _fromPos);
syncVector(s, _deltaPos);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera.setPosition(_fromPos + _deltaPos * t);
}
Vector3d _fromPos, _deltaPos;
};
DECLARE_TASK(CamLerpPosTask)
struct CamLerpScaleTask final : public CamLerpTask {
CamLerpScaleTask(Process &process, float targetScale, int32 duration, EasingType easingType)
: CamLerpTask(process, duration, easingType)
, _fromScale(_camera._cur._scale)
, _deltaScale(targetScale - _camera._cur._scale) {}
CamLerpScaleTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
s.syncAsFloatLE(_fromScale);
s.syncAsFloatLE(_deltaScale);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera._cur._scale = _fromScale + _deltaScale * t;
}
float _fromScale = 0, _deltaScale = 0;
};
DECLARE_TASK(CamLerpScaleTask)
struct CamLerpPosScaleTask final : public CamLerpTask {
CamLerpPosScaleTask(Process &process,
Vector3d targetPos, float targetScale,
int32 duration,
EasingType moveEasingType, EasingType scaleEasingType)
: CamLerpTask(process, duration, EasingType::Linear) // linear as we need different ones per component
, _fromPos(_camera._appliedCenter)
, _deltaPos(targetPos - _camera._appliedCenter)
, _fromScale(_camera._cur._scale)
, _deltaScale(targetScale - _camera._cur._scale)
, _moveEasingType(moveEasingType)
, _scaleEasingType(scaleEasingType) {}
CamLerpPosScaleTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
syncVector(s, _fromPos);
syncVector(s, _deltaPos);
s.syncAsFloatLE(_fromScale);
s.syncAsFloatLE(_deltaScale);
syncEnum(s, _moveEasingType);
syncEnum(s, _scaleEasingType);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera.setPosition(_fromPos + _deltaPos * ease(t, _moveEasingType));
_camera._cur._scale = _fromScale + _deltaScale * ease(t, _scaleEasingType);
}
Vector3d _fromPos, _deltaPos;
float _fromScale = 0, _deltaScale = 0;
EasingType _moveEasingType = {}, _scaleEasingType = {};
};
DECLARE_TASK(CamLerpPosScaleTask)
struct CamLerpRotationTask final : public CamLerpTask {
CamLerpRotationTask(Process &process, float targetRotation, int32 duration, EasingType easingType)
: CamLerpTask(process, duration, easingType)
, _fromRotation(_camera._cur._rotation.getDegrees())
, _deltaRotation(targetRotation - _camera._cur._rotation.getDegrees()) {}
CamLerpRotationTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
s.syncAsFloatLE(_fromRotation);
s.syncAsFloatLE(_deltaRotation);
}
const char *taskName() const override;
protected:
void update(float t) override {
_camera._cur._rotation = Angle(_fromRotation + _deltaRotation * t);
}
float _fromRotation = 0, _deltaRotation = 0;
};
DECLARE_TASK(CamLerpRotationTask)
static void syncVector(Serializer &s, Vector2d &v) {
float *data = v.getData();
s.syncAsFloatLE(data[0]);
s.syncAsFloatLE(data[1]);
}
struct CamShakeTask final : public CamLerpTask {
CamShakeTask(Process &process, Vector2d amplitude, Vector2d frequency, int32 duration)
: CamLerpTask(process, duration, EasingType::Linear)
, _amplitude(amplitude)
, _frequency(frequency) {}
CamShakeTask(Process &process, Serializer &s)
: CamLerpTask(process) {
syncGame(s);
}
void syncGame(Serializer &s) override {
CamLerpTask::syncGame(s);
syncVector(s, _amplitude);
syncVector(s, _frequency);
}
const char *taskName() const override;
protected:
void update(float t) override {
const Vector2d phase = _frequency * t * (float)M_PI * 2.0f;
const float amplTimeFactor = 1.0f / expf(t * 5.0f); // a curve starting at 1, depreciating towards 0
_camera.shake() = {
sinf(phase.getX()) * _amplitude.getX() * amplTimeFactor,
sinf(phase.getY()) * _amplitude.getY() * amplTimeFactor
};
}
Vector2d _amplitude, _frequency;
};
DECLARE_TASK(CamShakeTask)
struct CamWaitToStopTask final : public Task {
CamWaitToStopTask(Process &process)
: Task(process)
, _camera(g_engine->camera()) {}
CamWaitToStopTask(Process &process, Serializer &s)
: Task(process)
, _camera(g_engine->camera()) {
syncGame(s);
}
TaskReturn run() override {
return _camera._isChanging
? TaskReturn::yield()
: TaskReturn::finish(1);
}
void debugPrint() override {
g_engine->console().debugPrintf("Wait for camera to stop moving\n");
}
const char *taskName() const override;
private:
Camera &_camera;
};
DECLARE_TASK(CamWaitToStopTask)
struct CamSetInactiveAttributeTask final : public Task {
enum Attribute {
kPosZ,
kScale,
kRotation
};
CamSetInactiveAttributeTask(Process &process, Attribute attribute, float value, int32 delay)
: Task(process)
, _camera(g_engine->camera())
, _attribute(attribute)
, _value(value)
, _delay(delay) {}
CamSetInactiveAttributeTask(Process &process, Serializer &s)
: Task(process)
, _camera(g_engine->camera()) {
syncGame(s);
}
TaskReturn run() override {
if (_delay > 0) {
uint32 delay = (uint32)_delay;
_delay = 0;
return TaskReturn::waitFor(new DelayTask(process(), delay));
}
auto &state = _camera._backups[0];
switch (_attribute) {
case kPosZ:
state._usedCenter.z() = _value;
break;
case kScale:
state._scale = _value;
break;
case kRotation:
state._rotation = _value;
break;
default:
g_engine->game().unknownCamSetInactiveAttribute((int)_attribute);
break;
}
return TaskReturn::finish(0);
}
void debugPrint() override {
const char *attributeName;
switch (_attribute) {
case kPosZ:
attributeName = "PosZ";
break;
case kScale:
attributeName = "Scale";
break;
case kRotation:
attributeName = "Rotation";
break;
default:
attributeName = "<unknown>";
break;
}
g_engine->console().debugPrintf("Set inactive camera %s to %f after %dms\n", attributeName, _value, _delay);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
syncEnum(s, _attribute);
s.syncAsFloatLE(_value);
s.syncAsSint32LE(_delay);
}
const char *taskName() const override;
private:
Camera &_camera;
Attribute _attribute = {};
float _value = 0;
int32 _delay = 0;
};
DECLARE_TASK(CamSetInactiveAttributeTask)
Task *Camera::lerpPos(Process &process,
Vector2d targetPos,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new DelayTask(process, duration); // lerpPos does not handle inactive players
}
Vector3d targetPos3d(targetPos.getX(), targetPos.getY(), _appliedCenter.z());
return new CamLerpPosTask(process, targetPos3d, duration, easingType);
}
Task *Camera::lerpPos(Process &process,
Vector3d targetPos,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new DelayTask(process, duration); // lerpPos does not handle inactive players
}
setFollow(nullptr); // 3D position lerping is the only task that resets following
return new CamLerpPosTask(process, targetPos, duration, easingType);
}
Task *Camera::lerpPosZ(Process &process,
float targetPosZ,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kPosZ, targetPosZ, duration);
}
Vector3d targetPos(_appliedCenter.x(), _appliedCenter.y(), targetPosZ);
return new CamLerpPosTask(process, targetPos, duration, easingType);
}
Task *Camera::lerpScale(Process &process,
float targetScale,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kScale, targetScale, duration);
}
return new CamLerpScaleTask(process, targetScale, duration, easingType);
}
Task *Camera::lerpRotation(Process &process,
float targetRotation,
int32 duration, EasingType easingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kRotation, targetRotation, duration);
}
return new CamLerpRotationTask(process, targetRotation, duration, easingType);
}
Task *Camera::lerpPosScale(Process &process,
Vector3d targetPos, float targetScale,
int32 duration,
EasingType moveEasingType, EasingType scaleEasingType) {
if (!process.isActiveForPlayer()) {
return new CamSetInactiveAttributeTask(process, CamSetInactiveAttributeTask::kScale, targetScale, duration);
}
return new CamLerpPosScaleTask(process, targetPos, targetScale, duration, moveEasingType, scaleEasingType);
}
Task *Camera::waitToStop(Process &process) {
return new CamWaitToStopTask(process);
}
Task *Camera::shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration) {
if (!process.isActiveForPlayer()) {
return new DelayTask(process, (uint32)duration);
}
return new CamShakeTask(process, amplitude, frequency, duration);
}
} // namespace Alcachofa

122
engines/alcachofa/camera.h Normal file
View File

@@ -0,0 +1,122 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALCACHOFA_CAMERA_H
#define ALCACHOFA_CAMERA_H
#include "alcachofa/common.h"
#include "math/matrix4.h"
namespace Alcachofa {
class WalkingCharacter;
class Process;
struct Task;
static constexpr const int16_t kBaseScale = 300;
static constexpr const float kInvBaseScale = 1.0f / kBaseScale;
class Camera {
public:
inline Math::Angle rotation() const { return _cur._rotation; }
inline Math::Vector2d &shake() { return _shake; }
inline WalkingCharacter *followTarget() { return _followTarget; }
void update();
Math::Vector3d transform2Dto3D(Math::Vector3d v) const;
Math::Vector3d transform3Dto2D(Math::Vector3d v) const;
Common::Point transform3Dto2D(Common::Point p) const;
void resetRotationAndScale();
void setRoomBounds(Common::Point bgSize, int16 bgScale);
void setFollow(WalkingCharacter *target, bool catchUp = false);
void setPosition(Math::Vector2d v);
void setPosition(Math::Vector3d v);
void backup(uint slot);
void restore(uint slot);
void syncGame(Common::Serializer &s);
Task *lerpPos(Process &process,
Math::Vector2d targetPos,
int32 duration, EasingType easingType);
Task *lerpPos(Process &process,
Math::Vector3d targetPos,
int32 duration, EasingType easingType);
Task *lerpPosZ(Process &process,
float targetPosZ,
int32 duration, EasingType easingType);
Task *lerpScale(Process &process,
float targetScale,
int32 duration, EasingType easingType);
Task *lerpRotation(Process &process,
float targetRotation,
int32 duration, EasingType easingType);
Task *lerpPosScale(Process &process,
Math::Vector3d targetPos, float targetScale,
int32 duration, EasingType moveEasingType, EasingType scaleEasingType);
Task *waitToStop(Process &process);
Task *shake(Process &process, Math::Vector2d amplitude, Math::Vector2d frequency, int32 duration);
private:
friend struct CamLerpTask;
friend struct CamLerpPosTask;
friend struct CamLerpScaleTask;
friend struct CamLerpPosScaleTask;
friend struct CamLerpRotationTask;
friend struct CamShakeTask;
friend struct CamWaitToStopTask;
friend struct CamSetInactiveAttributeTask;
Math::Vector3d setAppliedCenter(Math::Vector3d center);
void setupMatricesAround(Math::Vector3d center);
void updateFollowing(float deltaTime);
struct State {
Math::Vector3d _usedCenter = Math::Vector3d(512, 384, 0);
float
_scale = 1.0f,
_speed = 0.0f,
_maxSpeedFactor = 230.0f;
Math::Angle _rotation;
bool _isBraking = false;
bool _isFollowingTarget = false;
void syncGame(Common::Serializer &s);
};
static constexpr uint kStateBackupCount = 2;
State _cur, _backups[kStateBackupCount];
WalkingCharacter *_followTarget = nullptr;
uint32 _lastUpdateTime = 0;
bool _isChanging = false,
_catchUp = false;
float _roomScale = 1.0f;
Math::Vector2d
_roomMin = Math::Vector2d(-10000, -10000),
_roomMax = Math::Vector2d(10000, 10000),
_shake;
Math::Vector3d _appliedCenter;
Math::Matrix4
_mat3Dto2D,
_mat2Dto3D;
};
}
#endif // ALCACHOFA_CAMERA_H

View File

@@ -0,0 +1,203 @@
/* 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 "alcachofa/common.h"
#include "alcachofa/detection.h"
using namespace Common;
using namespace Math;
namespace Alcachofa {
bool isPowerOfTwo(int16 x) {
return (x & (x - 1)) == 0;
}
float ease(float t, EasingType type) {
switch (type) {
case EasingType::Linear:
return t;
case EasingType::InOut:
return (1 - cosf(t * M_PI)) * 0.5f;
case EasingType::In:
return 1 - cosf(t * M_PI * 0.5f);
case EasingType::Out:
return sinf(t * M_PI * 0.5f);
default:
return 0.0f;
}
}
String reencode(const String &string, CodePage from, CodePage to) {
// Some spanish releases contain special characters in paths but Path does not support U32String
// Instead we convert to UTF8 and let the filesystem backend choose the native target encoding
auto it = Common::find_if(string.begin(), string.end(), [] (const char v) { return v < 0; });
if (it == string.end())
return string; // no need to reencode
return string.decode(from).encode(to);
}
FakeSemaphore::FakeSemaphore(const char *name, uint initialCount)
: _name(name)
, _counter(initialCount) {}
FakeSemaphore::~FakeSemaphore() {
assert(_counter == 0);
}
void FakeSemaphore::sync(Serializer &s, FakeSemaphore &semaphore) {
// if we are still holding locks during loading these locks will
// try to decrease the counter which will fail, let's find this out already here
assert(s.isSaving() || semaphore.isReleased());
(void)s;
(void)semaphore;
// We should not actually serialize the counter, just make sure it is empty
// When the locks are loaded, they will increase the counter themselves
}
FakeLock::FakeLock() {}
FakeLock::FakeLock(const char *name, FakeSemaphore &semaphore)
: _name(name)
, _semaphore(&semaphore) {
_semaphore->_counter++;
debug("ctor");
}
FakeLock::FakeLock(const FakeLock &other)
: _name(other._name)
, _semaphore(other._semaphore) {
assert(_semaphore != nullptr);
_semaphore->_counter++;
debug("copy");
}
FakeLock::FakeLock(FakeLock &&other)
: _name(other._name)
, _semaphore(other._semaphore) {
other._name = "<moved>";
other._semaphore = nullptr;
if (_semaphore != nullptr)
debug("move-ctor");
}
FakeLock::~FakeLock() {
release();
}
void FakeLock::operator= (FakeLock &&other) {
release();
_name = other._name;
_semaphore = other._semaphore;
other._name = "<moved>";
other._semaphore = nullptr;
debug("move-assign");
}
FakeLock &FakeLock::operator= (const FakeLock &other) {
release();
_name = other._name;
_semaphore = other._semaphore;
if (_semaphore != nullptr)
_semaphore->_counter++;
debug("copy-assign");
return *this;
}
void FakeLock::release() {
if (_semaphore == nullptr)
return;
assert(_semaphore->_counter > 0);
_semaphore->_counter--;
debug("release");
_semaphore = nullptr;
}
void FakeLock::debug(const char *action) {
const char *myName = _name == nullptr ? "<null>" : _name;
if (_semaphore == nullptr)
debugC(kDebugSemaphores, "Lock %s %s nullptr", myName, action);
else
debugC(kDebugSemaphores, "Lock %s %s %s at %u", myName, action, _semaphore->_name, _semaphore->_counter);
}
Vector3d as3D(const Vector2d &v) {
return Vector3d(v.getX(), v.getY(), 0.0f);
}
Vector3d as3D(Common::Point p) {
return Vector3d((float)p.x, (float)p.y, 0.0f);
}
Vector2d as2D(const Vector3d &v) {
return Vector2d(v.x(), v.y());
}
Vector2d as2D(Point p) {
return Vector2d((float)p.x, (float)p.y);
}
bool readBool(ReadStream &stream) {
return stream.readByte() != 0;
}
Point readPoint(ReadStream &stream) {
return { (int16)stream.readSint32LE(), (int16)stream.readSint32LE() };
}
static uint32 readVarInt(ReadStream &stream) {
uint32 length = stream.readByte();
if (length != 0xFF)
return length;
length = stream.readUint16LE();
if (length != 0xFFFF)
return length;
return stream.readUint32LE();
}
String readVarString(ReadStream &stream) {
uint32 length = readVarInt(stream);
if (length == 0)
return Common::String();
char *buffer = new char[length];
if (buffer == nullptr) //-V668
error("Out of memory in readVarString");
if (stream.read(buffer, length) != length)
error("Could not read all %u bytes in readVarString", length);
String result(buffer, buffer + length);
delete[] buffer;
return result;
}
void skipVarString(SeekableReadStream &stream) {
stream.skip(readVarInt(stream));
}
void syncPoint(Serializer &serializer, Point &point) {
serializer.syncAsSint32LE(point.x);
serializer.syncAsSint32LE(point.y);
}
}

171
engines/alcachofa/common.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 ALCACHOFA_COMMON_H
#define ALCACHOFA_COMMON_H
#include "common/rect.h"
#include "common/serializer.h"
#include "common/stream.h"
#include "common/str-enc.h"
#include "common/stack.h"
#include "math/vector2d.h"
#include "math/vector3d.h"
namespace Alcachofa {
enum class CursorType {
Point,
LeaveUp,
LeaveRight,
LeaveDown,
LeaveLeft,
WalkTo
};
enum class Direction {
Up,
Right,
Down,
Left,
Invalid = -1
};
enum class MainCharacterKind {
None,
Mortadelo,
Filemon
};
enum class EasingType {
Linear,
InOut,
In,
Out
};
constexpr const int32 kDirectionCount = 4;
constexpr const int8 kOrderCount = 70;
constexpr const int8 kForegroundOrderCount = 10;
struct Color {
uint8 r, g, b, a;
};
static constexpr const Color kWhite = { 255, 255, 255, 255 };
static constexpr const Color kBlack = { 0, 0, 0, 255 };
static constexpr const Color kClear = { 0, 0, 0, 0 };
static constexpr const Color kDebugRed = { 250, 0, 0, 70 };
static constexpr const Color kDebugGreen = { 0, 255, 0, 85 };
static constexpr const Color kDebugBlue = { 0, 0, 255, 110 };
static constexpr const Color kDebugLightBlue = { 80, 80, 255, 190 };
/**
* @brief This *fake* semaphore does not work in multi-threaded scenarios
* It is used as a safer option for a simple "isBusy" counter
*/
struct FakeSemaphore {
FakeSemaphore(const char *name, uint initialCount = 0);
~FakeSemaphore();
inline bool isReleased() const { return _counter == 0; }
inline uint counter() const { return _counter; }
static void sync(Common::Serializer &s, FakeSemaphore &semaphore);
private:
friend struct FakeLock;
const char *const _name;
uint _counter = 0;
};
struct FakeLock {
FakeLock();
FakeLock(const char *name, FakeSemaphore &semaphore);
FakeLock(const FakeLock &other);
FakeLock(FakeLock &&other);
~FakeLock();
void operator= (FakeLock &&other);
FakeLock &operator= (const FakeLock &other);
void release();
inline bool isReleased() const { return _semaphore == nullptr; }
private:
void debug(const char *action);
const char *_name = "<uninitialized>";
FakeSemaphore *_semaphore = nullptr;
};
bool isPowerOfTwo(int16 x);
float ease(float t, EasingType type);
Common::String reencode(
const Common::String &string,
Common::CodePage from = Common::CodePage::kISO8859_1, // "Western European", used for the spanish special characters
Common::CodePage to = Common::CodePage::kUtf8);
Math::Vector3d as3D(const Math::Vector2d &v);
Math::Vector3d as3D(Common::Point p);
Math::Vector2d as2D(const Math::Vector3d &v);
Math::Vector2d as2D(Common::Point p);
bool readBool(Common::ReadStream &stream);
Common::Point readPoint(Common::ReadStream &stream);
Common::String readVarString(Common::ReadStream &stream);
void skipVarString(Common::SeekableReadStream &stream);
void syncPoint(Common::Serializer &serializer, Common::Point &point);
template<typename T>
inline void syncArray(Common::Serializer &serializer, Common::Array<T> &array, void (*serializeFunction)(Common::Serializer &, T &)) {
auto size = array.size();
serializer.syncAsUint32LE(size);
array.resize(size);
serializer.syncArray(array.data(), size, serializeFunction);
}
template<typename T>
inline void syncStack(Common::Serializer &serializer, Common::Stack<T> &stack, void (*serializeFunction)(Common::Serializer &, T &)) {
auto size = stack.size();
serializer.syncAsUint32LE(size);
if (serializer.isLoading()) {
for (uint i = 0; i < size; i++) {
T value;
serializeFunction(serializer, value);
stack.push(value);
}
} else {
for (uint i = 0; i < size; i++)
serializeFunction(serializer, stack[i]);
}
}
template<typename T>
inline void syncEnum(Common::Serializer &serializer, T &enumValue) {
// syncAs does not have a cast for saving
int32 intValue = static_cast<int32>(enumValue);
serializer.syncAsSint32LE(intValue);
enumValue = static_cast<T>(intValue);
}
}
#endif // ALCACHOFA_COMMON_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]
add_engine alcachofa "Alcachofa" yes "" "" "highres mpeg2 3d" "tinygl"

View File

@@ -0,0 +1,297 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef USE_TEXT_CONSOLE_FOR_DEBUGGER
#include "gui/console.h"
#endif
#include "alcachofa/console.h"
#include "alcachofa/script.h"
#include "alcachofa/alcachofa.h"
using namespace Common;
namespace Alcachofa {
Console::Console() : GUI::Debugger() {
registerVar("showGraphics", &_showGraphics);
registerVar("showInteractables", &_showInteractables);
registerVar("showCharacters", &_showCharacters);
registerVar("showFloorShape", &_showFloor);
registerVar("showFloorEdges", &_showFloorEdges);
registerVar("showFloorColor", &_showFloorColor);
registerCmd("var", WRAP_METHOD(Console, cmdVar));
registerCmd("processes", WRAP_METHOD(Console, cmdProcesses));
registerCmd("room", WRAP_METHOD(Console, cmdRoom));
registerCmd("rooms", WRAP_METHOD(Console, cmdRooms));
registerCmd("changeRoom", WRAP_METHOD(Console, cmdChangeRoom));
registerCmd("disableDebugDraw", WRAP_METHOD(Console, cmdDisableDebugDraw));
registerCmd("pickup", WRAP_METHOD(Console, cmdItem));
registerCmd("drop", WRAP_METHOD(Console, cmdItem));
registerCmd("debugMode", WRAP_METHOD(Console, cmdDebugMode));
registerCmd("tp", WRAP_METHOD(Console, cmdTeleport));
registerCmd("toggleRoomFloor", WRAP_METHOD(Console, cmdToggleRoomFloor));
registerCmd("playVideo", WRAP_METHOD(Console, cmdPlayVideo));
}
Console::~Console() {}
bool Console::isAnyDebugDrawingOn() const {
return
g_engine->isDebugModeActive() ||
_showGraphics ||
_showInteractables ||
_showCharacters ||
_showFloor ||
_showFloorEdges ||
_showFloorColor;
}
bool Console::cmdVar(int argc, const char **args) {
auto &script = g_engine->script();
if (argc < 2 || argc > 3)
debugPrintf("usage: %s <name> [<value>]\n", args[0]);
else if (argc == 3) {
char *end = nullptr;
int32 value = (int32)strtol(args[2], &end, 10);
if (end == nullptr || *end != '\0')
debugPrintf("Invalid variable value: %s", args[2]);
else if (!script.hasVariable(args[1]))
debugPrintf("Invalid variable name: %s", args[1]);
else
script.variable(args[1]) = value;
} else if (argc == 2) {
bool hadSomeMatch = false;
for (auto it = script.beginVariables(); it != script.endVariables(); it++) {
if (matchString(it->_key.c_str(), args[1], true)) {
hadSomeMatch = true;
debugPrintf(" %32s = %d\n", it->_key.c_str(), script.variable(it->_key.c_str()));
}
}
if (!hadSomeMatch)
debugPrintf("Could not find any variable with pattern: %s\n", args[1]);
}
return true;
}
bool Console::cmdProcesses(int argc, const char **args) {
g_engine->scheduler().debugPrint();
return true;
}
bool Console::cmdRoom(int argc, const char **args) {
if (argc > 2) {
debugPrintf("usage: %s [<name>]\n", args[0]);
return true;
}
Room *room = nullptr;
if (argc == 1) {
room = g_engine->player().currentRoom();
if (room == nullptr) {
debugPrintf("Player is currently in no room, cannot print details\n");
return true;
}
} else {
room = g_engine->world().getRoomByName(args[1]);
if (room == nullptr) {
debugPrintf("Could not find room with exact name: %s\n", args[1]);
return cmdRooms(argc, args);
}
}
room->debugPrint(true);
return true;
}
bool Console::cmdRooms(int argc, const char **args) {
if (argc != 2) {
debugPrintf("usage: %s <pattern>\n", args[0]);
return true;
}
bool hadSomeMatch = false;
for (auto it = g_engine->world().beginRooms(); it != g_engine->world().endRooms(); it++) {
if ((*it)->name().matchString(args[1], true)) {
hadSomeMatch = true;
(*it)->debugPrint(false);
}
}
if (!hadSomeMatch)
debugPrintf("Could not find any room with pattern: %s\n", args[1]);
return true;
}
bool Console::cmdChangeRoom(int argc, const char **args) {
if (argc > 2)
debugPrintf("usage: %s <name>\n", args[0]);
else if (argc == 1) {
Room *current = g_engine->player().currentRoom();
debugPrintf("Current room: %s\n", current == nullptr ? "<null>" : current->name().c_str());
} else if (g_engine->world().getRoomByName(args[1]) == nullptr)
debugPrintf("Invalid room name: %s\n", args[1]);
else {
g_engine->player().changeRoom(args[1], true);
return false;
}
return true;
}
bool Console::cmdDisableDebugDraw(int argc, const char **args) {
_showInteractables = _showCharacters = _showFloor = _showFloorColor = false;
return true;
}
bool Console::cmdItem(int argc, const char **args) {
auto &inventory = g_engine->world().inventory();
auto &mortadelo = g_engine->world().mortadelo();
auto &filemon = g_engine->world().filemon();
auto *active = g_engine->player().activeCharacter();
if (argc < 2 || argc > 3) {
debugPrintf("usage: %s [Mortadelo/Filemon] [<item>]\n\n", args[0]);
debugPrintf("%20s%10s%10s\n", "Item", "Mortadelo", "Filemon");
for (auto itItem = inventory.beginObjects(); itItem != inventory.endObjects(); ++itItem) {
if (dynamic_cast<const Item *>(*itItem) == nullptr)
continue;
debugPrintf("%20s%10s%10s\n",
(*itItem)->name().c_str(),
mortadelo.hasItem((*itItem)->name()) ? "YES" : "no",
filemon.hasItem((*itItem)->name()) ? "YES" : "no");
}
return true;
}
if (argc == 2 && active == nullptr) {
debugPrintf("No character is active, name has to be specified\n");
return true;
}
const char *itemName = args[1];
if (argc == 3) {
itemName = args[2];
if (scumm_stricmp(args[1], "mortadelo") == 0 || scumm_stricmp(args[1], "m") == 0)
active = &mortadelo;
else if (scumm_stricmp(args[1], "filemon") == 0 || scumm_stricmp(args[1], "f") == 0)
active = &filemon;
else {
debugPrintf("Invalid character name \"%s\", has to be either \"mortadelo\" or \"filemon\"\n", args[1]);
return true;
}
}
bool hasMatchedSomething = false;
for (auto itItem = inventory.beginObjects(); itItem != inventory.endObjects(); ++itItem) {
if (dynamic_cast<const Item *>(*itItem) == nullptr ||
!(*itItem)->name().matchString(itemName, true))
continue;
hasMatchedSomething = true;
if (args[0][0] == 'p')
active->pickup((*itItem)->name(), false);
else
active->drop((*itItem)->name());
}
if (!hasMatchedSomething)
debugPrintf("Cannot find any item matching \"%s\"\n", itemName);
return true;
}
bool Console::cmdDebugMode(int argc, const char **args) {
if (argc < 2 || argc > 3) {
debugPrintf("usage: debugMode <mode> [<param>]\n");
debugPrintf("modes:\n");
debugPrintf(" 0 - None, disables debug mode\n");
debugPrintf(" 1 - Closest floor point, param limits to polygon\n");
debugPrintf(" 2 - Floor edge intersections, param limits to polygon\n");
debugPrintf(" 3 - Teleport character to mouse click, param selects character\n");
debugPrintf(" 4 - Show floor alpha, param selects index of floor color object\n");
debugPrintf(" 5 - Show floor color, param selects index of floor color object\n");
return true;
}
int32 param = -1;
if (argc > 2) {
char *end = nullptr;
param = (int32)strtol(args[2], &end, 10);
if (end == nullptr || *end != '\0') {
debugPrintf("Debug mode parameter can only be integers");
return true;
}
}
auto mode = (DebugMode)strtol(args[1], nullptr, 10);
g_engine->setDebugMode(mode, param);
return true;
}
bool Console::cmdTeleport(int argc, const char **args) {
if (argc < 1 || argc > 2) {
debugPrintf("usage: tp [<character>]\n");
debugPrintf("characters:\n");
debugPrintf(" 0 - Both\n");
debugPrintf(" 1 - Mortadelo\n");
debugPrintf(" 2 - Filemon\n");
}
int32 param = 0;
if (argc > 1) {
char *end = nullptr;
param = (int32)strtol(args[1], &end, 10);
if (end == nullptr || *end != '\0') {
debugPrintf("Character kind can only be an integer\n");
return true;
}
}
g_engine->setDebugMode(DebugMode::TeleportCharacter, param);
return false;
}
bool Console::cmdToggleRoomFloor(int argc, const char **args) {
auto room = g_engine->player().currentRoom();
if (room == nullptr) {
debugPrintf("No room is active");
return true;
}
room->toggleActiveFloor();
return false;
}
bool Console::cmdPlayVideo(int argc, const char **args) {
if (argc == 2) {
char *end = nullptr;
int32 videoId = (int32)strtol(args[1], &end, 10);
if (end == nullptr || *end != '\0') {
debugPrintf("Video ID can only be an integer\n");
return true;
}
#ifndef USE_TEXT_CONSOLE_FOR_DEBUGGER
// we have to close the console *now* to properly see the video
_debuggerDialog->close();
g_system->clearOverlay();
#endif
g_engine->playVideo(videoId);
return false;
} else
debugPrintf("usage: playVideo <id>\n");
return true;
}
} // End of namespace Alcachofa

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 ALCACHOFA_CONSOLE_H
#define ALCACHOFA_CONSOLE_H
#include "gui/debugger.h"
namespace Alcachofa {
enum class DebugMode {
None,
ClosestFloorPoint,
FloorIntersections,
TeleportCharacter,
FloorAlpha,
FloorColor
};
class Console : public GUI::Debugger {
public:
Console();
~Console() override;
inline bool showGraphics() const { return _showGraphics; }
inline bool showInteractables() const { return _showInteractables; }
inline bool showCharacters() const { return _showCharacters; }
inline bool showFloor() const { return _showFloor; }
inline bool showFloorEdges() const { return _showFloorEdges; }
inline bool showFloorColor() const { return _showFloorColor; }
bool isAnyDebugDrawingOn() const;
private:
bool cmdVar(int argc, const char **args);
bool cmdProcesses(int argc, const char **args);
bool cmdRoom(int argc, const char **args);
bool cmdRooms(int argc, const char **args);
bool cmdChangeRoom(int argc, const char **args);
bool cmdDisableDebugDraw(int argc, const char **args);
bool cmdItem(int argc, const char **args);
bool cmdDebugMode(int argc, const char **args);
bool cmdTeleport(int argc, const char **args);
bool cmdToggleRoomFloor(int argc, const char **args);
bool cmdPlayVideo(int argc, const char **args);
bool _showGraphics = false;
bool _showInteractables = false;
bool _showCharacters = false;
bool _showFloor = false;
bool _showFloorEdges = false;
bool _showFloorColor = false;
};
} // End of namespace Alcachofa
#endif // ALCACHOFA_CONSOLE_H

View File

@@ -0,0 +1,3 @@
begin_section("Alcachofa");
add_person("Hermann Noll", "Helco", "");
end_section();

230
engines/alcachofa/debug.h Normal file
View File

@@ -0,0 +1,230 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALCACHOFA_DEBUG_H
#define ALCACHOFA_DEBUG_H
#include "alcachofa/alcachofa.h"
using namespace Common;
namespace Alcachofa {
class IDebugHandler {
public:
virtual ~IDebugHandler() {}
virtual void update() = 0;
};
class ClosestFloorPointDebugHandler final : public IDebugHandler {
int32 _polygonI;
public:
ClosestFloorPointDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
void update() override {
auto mousePos2D = g_engine->input().debugInput().mousePos2D();
auto mousePos3D = g_engine->input().debugInput().mousePos3D();
auto floor = g_engine->player().currentRoom()->activeFloor();
auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
if (floor == nullptr || renderer == nullptr)
return;
Point target3D;
if (_polygonI < 0 || (uint)_polygonI >= floor->polygonCount())
target3D = floor->closestPointTo(mousePos3D);
else
target3D = floor->at((uint)_polygonI).closestPointTo(mousePos3D);
renderer->debugPolyline(mousePos2D, g_engine->camera().transform3Dto2D(target3D));
}
};
class FloorIntersectionsDebugHandler final : public IDebugHandler {
int32 _polygonI;
Point _fromPos3D;
public:
FloorIntersectionsDebugHandler(int32 polygonI) : _polygonI(polygonI) {}
void update() override {
auto floor = g_engine->player().currentRoom()->activeFloor();
auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
if (floor == nullptr || renderer == nullptr) {
g_engine->console().attach("Either the room has no floor or the renderer is not a debug renderer");
g_engine->setDebugMode(DebugMode::None, 0);
return;
}
if (g_engine->input().debugInput().wasMouseLeftPressed())
_fromPos3D = g_engine->input().debugInput().mousePos3D();
renderer->debugPolyline(
g_engine->camera().transform3Dto2D(_fromPos3D),
g_engine->input().debugInput().mousePos2D(),
kDebugRed);
if (_polygonI >= 0 && (uint)_polygonI < floor->polygonCount())
drawIntersectionsFor(floor->at((uint)_polygonI), renderer);
else {
for (uint i = 0; i < floor->polygonCount(); i++)
drawIntersectionsFor(floor->at(i), renderer);
}
}
private:
static constexpr float kMarkerLength = 16;
void drawIntersectionsFor(const Polygon &polygon, IDebugRenderer *renderer) {
auto &camera = g_engine->camera();
auto mousePos3D = g_engine->input().debugInput().mousePos3D();
for (uint i = 0; i < polygon._points.size(); i++) {
if (!polygon.intersectsEdge(i, _fromPos3D, mousePos3D))
continue;
auto a = camera.transform3Dto2D(polygon._points[i]);
auto b = camera.transform3Dto2D(polygon._points[(i + 1) % polygon._points.size()]);
auto mid = (a + b) / 2;
auto length = sqrtf(a.sqrDist(b));
auto normal = a - b;
normal = { normal.y, (int16)-normal.x };
auto inner = mid + normal * (kMarkerLength / length);
renderer->debugPolyline(a, b, kDebugGreen);
renderer->debugPolyline(mid, inner, kDebugGreen);
}
}
};
class TeleportCharacterDebugHandler final : public IDebugHandler {
MainCharacterKind _kind;
public:
TeleportCharacterDebugHandler(int32 kindI) : _kind((MainCharacterKind)kindI) {}
void update() override {
g_engine->drawQueue().clear();
g_engine->player().drawCursor(true);
g_engine->drawQueue().draw();
auto &input = g_engine->input().debugInput();
if (input.wasMouseRightPressed()) {
g_engine->setDebugMode(DebugMode::None, 0);
return;
}
if (!input.wasMouseLeftPressed())
return;
auto floor = g_engine->player().currentRoom()->activeFloor();
if (floor == nullptr || !floor->contains(input.mousePos3D()))
return;
if (_kind == MainCharacterKind::Filemon)
teleport(g_engine->world().filemon(), input.mousePos3D());
else if (_kind == MainCharacterKind::Mortadelo)
teleport(g_engine->world().mortadelo(), input.mousePos3D());
else {
teleport(g_engine->world().filemon(), input.mousePos3D());
teleport(g_engine->world().mortadelo(), input.mousePos3D());
}
g_engine->setDebugMode(DebugMode::None, 0);
}
private:
void teleport(MainCharacter &character, Point position) {
auto currentRoom = g_engine->player().currentRoom();
if (character.room() != currentRoom) {
character.resetTalking();
character.room() = currentRoom;
}
character.setPosition(position);
}
};
class FloorColorDebugHandler final : public IDebugHandler {
const FloorColorShape &_shape;
const bool _useColor;
Color _curColor = kDebugGreen;
bool _isOnFloor = false;
static constexpr size_t kBufferSize = 64;
char _buffer[kBufferSize] = { 0 };
FloorColorDebugHandler(const FloorColorShape &shape, bool useColor)
: _shape(shape)
, _useColor(useColor) {}
public:
static FloorColorDebugHandler *create(int32 objectI, bool useColor) {
const Room *room = g_engine->player().currentRoom();
uint floorCount = 0;
for (auto itObject = room->beginObjects(); itObject != room->endObjects(); ++itObject) {
FloorColor *floor = dynamic_cast<FloorColor *>(*itObject);
if (floor == nullptr)
continue;
if (objectI <= 0)
// dynamic_cast is not possible due to Shape not having virtual methods
return new FloorColorDebugHandler(*(FloorColorShape *)(floor->shape()), useColor);
floorCount++;
objectI--;
}
g_engine->console().debugPrintf("Invalid floor color index, there are %u floors in this room\n", floorCount);
return nullptr;
}
void update() override {
auto &input = g_engine->input().debugInput();
if (input.wasMouseRightPressed()) {
g_engine->setDebugMode(DebugMode::None, 0);
return;
}
if (input.isMouseLeftDown()) {
auto optColor = _shape.colorAt(input.mousePos3D());
_isOnFloor = optColor.first;
if (!_isOnFloor) {
uint8 roomAlpha = (uint)(g_engine->player().currentRoom()->characterAlphaTint() * 255 / 100);
optColor.second = Color { 255, 255, 255, roomAlpha };
}
_curColor = _useColor
? Color { optColor.second.r, optColor.second.g, optColor.second.b, 255 }
: Color { optColor.second.a, optColor.second.a, optColor.second.a, 255 };
g_engine->world().mortadelo().color() =
g_engine->world().filemon().color() =
_useColor ? optColor.second : Color { 255, 255, 255, optColor.second.a };
}
snprintf(_buffer, kBufferSize, "r:%3d g:%3d b:%3d a:%3d",
(int)_curColor.r, (int)_curColor.g, (int)_curColor.b, (int)_curColor.a);
auto *debugRenderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
g_engine->drawQueue().clear();
g_engine->player().drawCursor(true);
g_engine->renderer().setTexture(nullptr);
g_engine->renderer().quad({ 0, 0 }, { 50, 50 }, _isOnFloor ? _curColor : kDebugGreen);
g_engine->drawQueue().add<TextDrawRequest>(
g_engine->globalUI().dialogFont(), _buffer, Point { 70, 20 }, 500, false, kWhite, -kForegroundOrderCount + 1);
if (!_isOnFloor)
g_engine->renderer().quad({ 5, 5 }, { 40, 40 }, _curColor);
if (debugRenderer != nullptr)
debugRenderer->debugShape(_shape, kDebugBlue);
g_engine->drawQueue().draw();
}
};
}
#endif // ALCACHOFA_DEBUG_H

View File

@@ -0,0 +1,46 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "base/plugins.h"
#include "common/config-manager.h"
#include "common/file.h"
#include "common/md5.h"
#include "common/str-array.h"
#include "common/translation.h"
#include "common/util.h"
#include "alcachofa/detection.h"
#include "alcachofa/detection_tables.h"
const DebugChannelDef AlcachofaMetaEngineDetection::debugFlagList[] = {
{ Alcachofa::kDebugGraphics, "Graphics", "Graphics debug level" },
{ Alcachofa::kDebugScript, "Script", "Enable debug script dump" },
{ Alcachofa::kDebugGameplay, "Gameplay", "Gameplay-related tracing" },
{ Alcachofa::kDebugSounds, "Sounds", "Sound- and Music-related tracing" },
{ Alcachofa::kDebugSemaphores, "Semaphores", "Tracing operations on semaphores" },
DEBUG_CHANNEL_END
};
AlcachofaMetaEngineDetection::AlcachofaMetaEngineDetection() : AdvancedMetaEngineDetection<Alcachofa::AlcachofaGameDescription>(
Alcachofa::gameDescriptions, Alcachofa::alcachofaGames) {
_flags |= kADFlagMatchFullPaths;
}
REGISTER_PLUGIN_STATIC(ALCACHOFA_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, AlcachofaMetaEngineDetection);

View File

@@ -0,0 +1,90 @@
/* 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 ALCACHOFA_DETECTION_H
#define ALCACHOFA_DETECTION_H
#include "engines/advancedDetector.h"
namespace Alcachofa {
enum AlcachofaDebugChannels {
kDebugGraphics = 1,
kDebugScript,
kDebugGameplay,
kDebugSounds,
kDebugSemaphores
};
enum class EngineVersion {
V1_0 = 10, // edicion orginal, vaqueros and terror
V2_0 = 20, // the rest
V3_0 = 30, // Remastered movie adventure (used for original spanish release)
V3_1 = 31, // Remastered movie adventure (for german release and english/spanish steam release)
};
struct AlcachofaGameDescription {
AD_GAME_DESCRIPTION_HELPERS(desc);
ADGameDescription desc;
EngineVersion engineVersion;
inline bool isVersionBetween(int min, int max) const {
int intVersion = (int)engineVersion;
return intVersion >= min && intVersion <= max;
}
};
extern const PlainGameDescriptor alcachofaGames[];
extern const AlcachofaGameDescription gameDescriptions[];
#define GAMEOPTION_HIGH_QUALITY GUIO_GAMEOPTIONS1 // I should comment what this does, but I don't know
#define GAMEOPTION_32BITS GUIO_GAMEOPTIONS2
} // End of namespace Alcachofa
class AlcachofaMetaEngineDetection : public AdvancedMetaEngineDetection<Alcachofa::AlcachofaGameDescription> {
static const DebugChannelDef debugFlagList[];
public:
AlcachofaMetaEngineDetection();
~AlcachofaMetaEngineDetection() override {}
const char *getName() const override {
return "alcachofa";
}
const char *getEngineName() const override {
return "Alcachofa";
}
const char *getOriginalCopyright() const override {
return "Alcachofa Soft (C)";
}
const DebugChannelDef *getDebugChannels() const override {
return debugFlagList;
}
};
#endif // ALCACHOFA_DETECTION_H

View File

@@ -0,0 +1,120 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Alcachofa {
const PlainGameDescriptor alcachofaGames[] = {
{ "aventuradecine", "Mort & Phil: A Movie Adventure" },
{ 0, 0 }
};
const AlcachofaGameDescription gameDescriptions[] = {
//
// A Movie Adventure
//
{
{
"aventuradecine",
"Clever & Smart - A Movie Adventure",
AD_ENTRY2s(
"Textos/Objetos.nkr", "a2b1deff5ca7187f2ebf7f2ab20747e9", 17606,
"Data/DATA02.BIN", "ab6d8867585fbc0f555f5b13d8d1bdf3", 55906308
),
Common::DE_DEU,
Common::kPlatformWindows,
ADGF_USEEXTRAASTITLE | ADGF_REMASTERED,
GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
},
EngineVersion::V3_1
},
{
{
"aventuradecine",
"Clever & Smart - A Movie Adventure",
AD_ENTRY2s(
"Textos/Objetos.nkr", "a2b1deff5ca7187f2ebf7f2ab20747e9", 17606,
"Data/DATA02.BIN", "4693e52835bad0c6deab63b60ead81fb", 38273192
),
Common::DE_DEU,
Common::kPlatformWindows,
ADGF_USEEXTRAASTITLE | ADGF_REMASTERED | ADGF_PIRATED,
GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
},
EngineVersion::V3_1
},
{
{
"aventuradecine",
"Clever & Smart - A Movie Adventure",
AD_ENTRY1s("Textos/Objetos.nkr", "8dce25494470209d4882bf12f1a5ea42", 19208),
Common::DE_DEU,
Common::kPlatformWindows,
ADGF_USEEXTRAASTITLE | ADGF_REMASTERED | ADGF_DEMO,
GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
},
EngineVersion::V3_1
},
// The "english" version is just the spanish version with english subtitles...
{
{
"aventuradecine",
"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
AD_ENTRY1s("Textos/Objetos.nkr", "ad3cb78ad7a51cfe63ee6f84768c7e66", 15895),
Common::EN_ANY,
Common::kPlatformWindows,
ADGF_USEEXTRAASTITLE | ADGF_REMASTERED,
GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
},
EngineVersion::V3_1
},
// the spanish Steam variant
{
{
"aventuradecine",
"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
AD_ENTRY1s("Textos/Objetos.nkr", "93331e4cc8d2f8f8a0007bfb5140dff5", 16403),
Common::ES_ESP,
Common::kPlatformWindows,
ADGF_USEEXTRAASTITLE | ADGF_REMASTERED,
GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
},
EngineVersion::V3_1
},
// the spanish CD variant
{
{
"aventuradecine",
"Mortadelo y Filemón: Una Aventura de Cine - Edición Especial",
AD_ENTRY1s("Textos/Objetos.nkr", "8a8b23c04fdc4ced8070a7bccd0177bb", 24467),
Common::ES_ESP,
Common::kPlatformWindows,
ADGF_UNSTABLE | ADGF_USEEXTRAASTITLE | ADGF_REMASTERED | ADGF_CD,
GUIO2(GAMEOPTION_32BITS, GAMEOPTION_HIGH_QUALITY)
},
EngineVersion::V3_0
},
{ AD_TABLE_END_MARKER, EngineVersion::V1_0 }
};
} // End of namespace Alcachofa

View File

@@ -0,0 +1,468 @@
/* 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 "alcachofa/alcachofa.h"
#include "alcachofa/game.h"
#include "alcachofa/script.h"
using namespace Common;
namespace Alcachofa {
static constexpr const ScriptOp kScriptOpMap[] = {
ScriptOp::Nop,
ScriptOp::Dup,
ScriptOp::PushAddr,
ScriptOp::PushValue,
ScriptOp::Deref,
ScriptOp::Crash, ///< would crash original engine by writing to read-only memory
ScriptOp::PopN,
ScriptOp::Store,
ScriptOp::Crash,
ScriptOp::Crash,
ScriptOp::LoadString,
ScriptOp::LoadString, ///< exactly the same as LoadString
ScriptOp::Crash,
ScriptOp::ScriptCall,
ScriptOp::KernelCall,
ScriptOp::JumpIfFalse,
ScriptOp::JumpIfTrue,
ScriptOp::Jump,
ScriptOp::Negate,
ScriptOp::BooleanNot,
ScriptOp::Mul,
ScriptOp::Crash,
ScriptOp::Crash,
ScriptOp::Add,
ScriptOp::Sub,
ScriptOp::Less,
ScriptOp::Greater,
ScriptOp::LessEquals,
ScriptOp::GreaterEquals,
ScriptOp::Equals,
ScriptOp::NotEquals,
ScriptOp::BitAnd,
ScriptOp::BitOr,
ScriptOp::Crash,
ScriptOp::Crash,
ScriptOp::Crash,
ScriptOp::Crash,
ScriptOp::ReturnValue
};
static constexpr const ScriptKernelTask kScriptKernelTaskMapV30[] = {
ScriptKernelTask::Nop,
ScriptKernelTask::PlayVideo,
ScriptKernelTask::PlaySound,
ScriptKernelTask::PlayMusic,
ScriptKernelTask::StopMusic,
ScriptKernelTask::WaitForMusicToEnd,
ScriptKernelTask::ShowCenterBottomText,
ScriptKernelTask::StopAndTurn,
ScriptKernelTask::StopAndTurnMe,
ScriptKernelTask::ChangeCharacter,
ScriptKernelTask::SayText,
ScriptKernelTask::Nop,
ScriptKernelTask::Go,
ScriptKernelTask::Put,
ScriptKernelTask::ChangeCharacterRoom,
ScriptKernelTask::KillProcesses,
ScriptKernelTask::On,
ScriptKernelTask::Off,
ScriptKernelTask::Pickup,
ScriptKernelTask::CharacterPickup,
ScriptKernelTask::Drop,
ScriptKernelTask::CharacterDrop,
ScriptKernelTask::Delay,
ScriptKernelTask::HadNoMousePressFor,
ScriptKernelTask::Nop,
ScriptKernelTask::Fork,
ScriptKernelTask::Animate,
ScriptKernelTask::AnimateCharacter,
ScriptKernelTask::AnimateTalking,
ScriptKernelTask::ChangeRoom,
ScriptKernelTask::ToggleRoomFloor,
ScriptKernelTask::SetDialogLineReturn,
ScriptKernelTask::DialogMenu,
ScriptKernelTask::ClearInventory,
ScriptKernelTask::Nop,
ScriptKernelTask::FadeType0,
ScriptKernelTask::FadeType1,
ScriptKernelTask::LerpWorldLodBias,
ScriptKernelTask::FadeType2,
ScriptKernelTask::SetActiveTextureSet,
ScriptKernelTask::SetMaxCamSpeedFactor,
ScriptKernelTask::WaitCamStopping,
ScriptKernelTask::CamFollow,
ScriptKernelTask::CamShake,
ScriptKernelTask::LerpCamXY,
ScriptKernelTask::LerpCamZ,
ScriptKernelTask::LerpCamScale,
ScriptKernelTask::LerpCamToObjectWithScale,
ScriptKernelTask::LerpCamToObjectResettingZ,
ScriptKernelTask::LerpCamRotation,
ScriptKernelTask::FadeIn,
ScriptKernelTask::FadeOut,
ScriptKernelTask::FadeIn2,
ScriptKernelTask::FadeOut2,
ScriptKernelTask::LerpCamToObjectKeepingZ
};
// in V3.1 there is the LerpCharacterLodBias and LerpCamXYZ tasks, no other differences
static constexpr const ScriptKernelTask kScriptKernelTaskMapV31[] = {
ScriptKernelTask::Nop,
ScriptKernelTask::PlayVideo,
ScriptKernelTask::PlaySound,
ScriptKernelTask::PlayMusic,
ScriptKernelTask::StopMusic,
ScriptKernelTask::WaitForMusicToEnd,
ScriptKernelTask::ShowCenterBottomText,
ScriptKernelTask::StopAndTurn,
ScriptKernelTask::StopAndTurnMe,
ScriptKernelTask::ChangeCharacter,
ScriptKernelTask::SayText,
ScriptKernelTask::Nop,
ScriptKernelTask::Go,
ScriptKernelTask::Put,
ScriptKernelTask::ChangeCharacterRoom,
ScriptKernelTask::KillProcesses,
ScriptKernelTask::LerpCharacterLodBias,
ScriptKernelTask::On,
ScriptKernelTask::Off,
ScriptKernelTask::Pickup,
ScriptKernelTask::CharacterPickup,
ScriptKernelTask::Drop,
ScriptKernelTask::CharacterDrop,
ScriptKernelTask::Delay,
ScriptKernelTask::HadNoMousePressFor,
ScriptKernelTask::Nop,
ScriptKernelTask::Fork,
ScriptKernelTask::Animate,
ScriptKernelTask::AnimateCharacter,
ScriptKernelTask::AnimateTalking,
ScriptKernelTask::ChangeRoom,
ScriptKernelTask::ToggleRoomFloor,
ScriptKernelTask::SetDialogLineReturn,
ScriptKernelTask::DialogMenu,
ScriptKernelTask::ClearInventory,
ScriptKernelTask::Nop,
ScriptKernelTask::FadeType0,
ScriptKernelTask::FadeType1,
ScriptKernelTask::LerpWorldLodBias,
ScriptKernelTask::FadeType2,
ScriptKernelTask::SetActiveTextureSet,
ScriptKernelTask::SetMaxCamSpeedFactor,
ScriptKernelTask::WaitCamStopping,
ScriptKernelTask::CamFollow,
ScriptKernelTask::CamShake,
ScriptKernelTask::LerpCamXY,
ScriptKernelTask::LerpCamZ,
ScriptKernelTask::LerpCamScale,
ScriptKernelTask::LerpCamToObjectWithScale,
ScriptKernelTask::LerpCamToObjectResettingZ,
ScriptKernelTask::LerpCamRotation,
ScriptKernelTask::FadeIn,
ScriptKernelTask::FadeOut,
ScriptKernelTask::FadeIn2,
ScriptKernelTask::FadeOut2,
ScriptKernelTask::LerpCamXYZ,
ScriptKernelTask::LerpCamToObjectKeepingZ
};
static constexpr const char *kMapFiles[] = { // not really inherent to V3 but holds true for all V3 games
"MAPAS/MAPA5.EMC",
"MAPAS/MAPA4.EMC",
"MAPAS/MAPA3.EMC",
"MAPAS/MAPA2.EMC",
"MAPAS/MAPA1.EMC",
"MAPAS/GLOBAL.EMC",
nullptr
};
class GameWithVersion3 : public Game {
public:
Point getResolution() override {
return Point(1024, 768);
}
const char *const *getMapFiles() override {
return kMapFiles;
}
Span<const ScriptOp> getScriptOpMap() override {
return { kScriptOpMap, ARRAYSIZE(kScriptOpMap) };
}
void updateScriptVariables() override {
Script &script = g_engine->script();
if (g_engine->input().wasAnyMousePressed()) // yes, this variable is never reset by the engine (only by script)
script.variable("SeHaPulsadoRaton") = 1;
script.variable("EstanAmbos") = g_engine->world().mortadelo().room() == g_engine->world().filemon().room();
script.variable("textoson") = g_engine->config().subtitles() ? 1 : 0;
}
void onLoadedGameFiles() override {
// this notifies the script whether we are a demo
if (g_engine->world().loadedMapCount() == 2)
g_engine->script().variable("EsJuegoCompleto") = 2;
else if (g_engine->world().loadedMapCount() == 3) // I don't know this demo
g_engine->script().variable("EsJuegoCompleto") = 1;
}
bool doesRoomHaveBackground(const Room *room) override {
return !room->name().equalsIgnoreCase("Global") &&
!room->name().equalsIgnoreCase("HABITACION_NEGRA");
}
bool shouldCharacterTrigger(const Character *character, const char *action) override {
// An original hack to check that bed sheet is used on the other main character only in the correct room
// There *is* another script variable (es_casa_freddy) that should check this
// but, I guess, Alcachofa Soft found a corner case where this does not work?
if (scumm_stricmp(action, "iSABANA") == 0 &&
dynamic_cast<const MainCharacter *>(character) != nullptr &&
!character->room()->name().equalsIgnoreCase("CASA_FREDDY_ARRIBA")) {
return false;
}
return Game::shouldCharacterTrigger(character, action);
}
void onUserChangedCharacter() override {
// An original bug in room POBLADO_INDIO if filemon is bound and mortadelo enters the room
// the door A_PUENTE which was disabled is reenabled to allow mortadelo leaving
// However if the user now changes character, the door is still enabled and filemon can
// enter a ghost state walking through a couple rooms and softlocking.
if (g_engine->player().currentRoom()->name().equalsIgnoreCase("POBLADO_INDIO"))
g_engine->script().createProcess(g_engine->player().activeCharacterKind(), "ENTRAR_POBLADO_INDIO");
}
bool hasMortadeloVoice(const Character *character) override {
return Game::hasMortadeloVoice(character) ||
character->name().equalsIgnoreCase("MORTADELO_TREN"); // an original hard-coded special case
}
PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name) override {
if (scumm_stricmp(action, "put"))
return Game::unknownGoPutTarget(process, action, name);
if (!scumm_stricmp("A_Poblado_Indio", name)) {
// A_Poblado_Indio is a Door but is originally cast into a PointObject
// a pointer and the draw order is then interpreted as position and the character snapped onto the floor shape.
// Instead I just use the A_Poblado_Indio1 object which exists as counter-part for A_Poblado_Indio2 which should have been used
auto target = dynamic_cast<PointObject *>(
g_engine->world().getObjectByName(process.character(), "A_Poblado_Indio1"));
if (target == nullptr)
_message("Unknown put target A_Poblado_Indio1 during exemption for A_Poblado_Indio");
return target;
}
if (!scumm_stricmp("PUNTO_VENTANA", name)) {
// The object is in the previous, now inactive room.
// Luckily Mortadelo already is at that point so not further action required
return nullptr;
}
if (!scumm_stricmp("Puerta_Casa_Freddy_Intermedia", name)) {
// Another case of a door being cast into a PointObject
return nullptr;
}
return Game::unknownGoPutTarget(process, action, name);
}
void unknownAnimateCharacterObject(const char *name) override {
if (!scumm_stricmp(name, "COGE F DCH") || // original bug in MOTEL_ENTRADA
!scumm_stricmp(name, "CHIQUITO_IZQ"))
return;
Game::unknownAnimateCharacterObject(name);
}
void missingSound(const String &fileName) override {
if (fileName == "CHAS" || fileName == "517")
return;
Game::missingSound(fileName);
}
};
class GameMovieAdventureSpecialV30 : public GameWithVersion3 {
public:
Span<const ScriptKernelTask> getScriptKernelTaskMap() override {
return { kScriptKernelTaskMapV30, ARRAYSIZE(kScriptKernelTaskMapV30) };
}
void updateScriptVariables() override {
GameWithVersion3::updateScriptVariables();
// in V3.0 there is no CalcularTiempoSinPulsarRaton variable to reset the timer
g_engine->script().setScriptTimer(g_engine->input().wasAnyMousePressed());
}
bool shouldClipCamera() override {
return true;
}
void missingAnimation(const String &fileName) override {
static const char *exemptions[] = {
"ANIMACION.AN0",
"PP_MORTA.AN0",
"ESTOMAGO.AN0",
"CREDITOS.AN0",
"HABITACION NEGRA.AN0",
nullptr
};
const auto isInExemptions = [&] (const char *const *const list) {
for (const char *const *exemption = list; *exemption != nullptr; exemption++) {
if (fileName.equalsIgnoreCase(*exemption))
return true;
}
return false;
};
if (isInExemptions(exemptions))
debugC(1, kDebugGraphics, "Animation exemption triggered: %s", fileName.c_str());
else
Game::missingAnimation(fileName);
}
};
class GameMovieAdventureSpecialV31 : public GameWithVersion3 {
public:
Span<const ScriptKernelTask> getScriptKernelTaskMap() override {
return { kScriptKernelTaskMapV31, ARRAYSIZE(kScriptKernelTaskMapV31) };
}
void updateScriptVariables() override {
GameWithVersion3::updateScriptVariables();
Script &script = g_engine->script();
script.setScriptTimer(!script.variable("CalcularTiempoSinPulsarRaton"));
script.variable("modored") = 0; // this is signalling whether a network connection is established
}
bool shouldClipCamera() override {
return g_engine->script().variable("EncuadrarCamara") != 0;
}
void drawScreenStates() override {
if (int32 borderWidth = g_engine->script().variable("BordesNegros")) {
int16 width = g_system->getWidth();
int16 height = g_system->getHeight();
g_engine->drawQueue().add<BorderDrawRequest>(Rect(0, 0, width, borderWidth), kBlack);
g_engine->drawQueue().add<BorderDrawRequest>(Rect(0, height - borderWidth, width, height), kBlack);
}
}
bool shouldTriggerDoor(const Door *door) override {
// An invalid door target, the character will go to the door and then ignore it (also in original engine)
// this is a bug introduced in V3.1
if (door->targetRoom() == "LABERINTO" && door->targetObject() == "a_LABERINTO_desde_LABERINTO_2")
return false;
return Game::shouldTriggerDoor(door);
}
void missingAnimation(const String &fileName) override {
static const char *exemptions[] = {
"ANIMACION.AN0",
"DESPACHO_SUPER2_OL_SOMBRAS2.AN0",
"PP_MORTA.AN0",
"DESPACHO_SUPER2___FONDO_PP_SUPER.AN0",
"ESTOMAGO.AN0",
"CREDITOS.AN0",
"MONITOR___OL_EFECTO_FONDO.AN0",
nullptr
};
// these only happen in the german demo
static const char *demoExemptions[] = {
"TROZO_1.AN0",
"TROZO_2.AN0",
"TROZO_3.AN0",
"TROZO_4.AN0",
"TROZO_5.AN0",
"TROZO_6.AN0",
"NOTA_CINE_NEGRO.AN0",
"PP_JOHN_WAYNE_2.AN0",
"ARQUEOLOGO_ESTATICO_TIA.AN0",
"ARQUEOLOGO_HABLANDO_TIA.AN0",
nullptr
};
const auto isInExemptions = [&] (const char *const *const list) {
for (const char *const *exemption = list; *exemption != nullptr; exemption++) {
if (fileName.equalsIgnoreCase(*exemption))
return true;
}
return false;
};
if (isInExemptions(exemptions) ||
((g_engine->gameDescription().desc.flags & ADGF_DEMO) && isInExemptions(demoExemptions)))
debugC(1, kDebugGraphics, "Animation exemption triggered: %s", fileName.c_str());
else
Game::missingAnimation(fileName);
}
void unknownAnimateObject(const char *name) override {
if (!scumm_stricmp("EXPLOSION DISFRAZ", name))
return;
Game::unknownAnimateObject(name);
}
void unknownSayTextCharacter(const char *name, int32 dialogId) override {
if (!scumm_stricmp(name, "OFELIA") && dialogId == 3737)
return;
Game::unknownSayTextCharacter(name, dialogId);
}
void missingSound(const String &fileName) override {
if ((g_engine->gameDescription().desc.flags & ADGF_DEMO) && (
fileName == "M4996" ||
fileName == "T40"))
return;
GameWithVersion3::missingSound(fileName);
}
bool isKnownBadVideo(int32 videoId) override {
return
(videoId == 3 && (g_engine->gameDescription().desc.flags & ADGF_DEMO)) || // The german trailer is WMV-encoded
Game::isKnownBadVideo(videoId);
}
void invalidVideo(int32 videoId, const char *context) override {
// the second intro-video is DV-encoded in the spanish steam version
if (videoId == 1 && g_engine->gameDescription().desc.language != DE_DEU)
warning("Could not play video %d (%s) (WMV not supported)", videoId, context);
else
Game::invalidVideo(videoId, context);
}
};
Game *Game::createForMovieAdventure() {
if (g_engine->version() == EngineVersion::V3_0)
return new GameMovieAdventureSpecialV30();
else
return new GameMovieAdventureSpecialV31();
}
}

File diff suppressed because it is too large Load Diff

202
engines/alcachofa/game.cpp 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/>.
*
*/
#include "alcachofa/alcachofa.h"
#include "alcachofa/game.h"
#include "alcachofa/script.h"
using namespace Common;
namespace Alcachofa {
Game::Game()
#ifdef ALCACHOFA_DEBUG // During development let's check out these errors more carefully
: _message(error)
#else // For release builds the game might still work or the user might still be able to save and restart
: _message(warning)
#endif
{}
void Game::onLoadedGameFiles() {}
void Game::drawScreenStates() {}
bool Game::doesRoomHaveBackground(const Room *room) {
return true;
}
void Game::unknownRoomObject(const String &type) {
_message("Unknown type for room object: %s", type.c_str());
}
void Game::unknownRoomType(const String &type) {
_message("Unknown type for room: %s", type.c_str());
}
void Game::unknownDoorTargetRoom(const String &name) {
_message("Unknown door target room: %s", name.c_str());
}
void Game::unknownDoorTargetDoor(const String &room, const String &door) {
_message("Unknown door target door: %s in %s", door.c_str(), room.c_str());
}
void Game::invalidDialogLine(uint index) {
_message("Invalid dialog line %u");
}
void Game::tooManyDialogLines(uint lineCount, uint maxLineCount) {
// we set max line count as constant, if some game uses more we just have to adapt the constant
// the bug will be not all dialog lines being rendered
_message("Text to be rendered has too many lines (%u), check text validity and max line count (%u)", lineCount, maxLineCount);
}
void Game::tooManyDrawRequests(int order) {
// similar, the bug will be some objects not being rendered
_message("Too many draw requests in order %d", order);
}
bool Game::shouldCharacterTrigger(const Character *character, const char *action) {
return true;
}
bool Game::shouldTriggerDoor(const Door *door) {
return true;
}
void Game::onUserChangedCharacter() {}
bool Game::hasMortadeloVoice(const Character *character) {
return character == &g_engine->world().mortadelo();
}
void Game::unknownCamSetInactiveAttribute(int attribute) {
// this will be a bug by us, but gameplay should not be affected, so don't error in release builds
// it could still happen if an attribute was added/removed in updates so we still want users to report this
_message("Unknown CamSetInactiveAttribute attribute: %d", attribute);
}
void Game::unknownFadeType(int fadeType) {
_message("Unknown fade type %d", fadeType);
}
void Game::unknownSerializedObject(const char *object, const char *owner, const char *room) {
// potentially game-breaking for _currentlyUsingObject but might otherwise be just a graphical bug
_message("Invalid object name \"%s\" saved for \"%s\" in \"%s\"", object, owner, room);
}
void Game::unknownPickupItem(const char *name) {
_message("Tried to pickup unknown item: %s", name);
}
void Game::unknownDropItem(const char *name) {
_message("Tried to drop unknown item: %s", name);
}
void Game::unknownVariable(const char *name) {
_message("Unknown script variable: %s", name);
}
void Game::unknownInstruction(const ScriptInstruction &instruction) {
const char *type;
if (instruction._op < 0 || (uint32)instruction._op >= getScriptOpMap().size())
type = "out-of-bounds";
else if (getScriptOpMap()[instruction._op] == ScriptOp::Crash)
type = "crash"; // these are defined in the game, but implemented as write to null-pointer
else
type = "unimplemented"; // we forgot to implement them
_message("Script reached %s instruction: %d %d", type, (int)instruction._op, instruction._arg);
}
void Game::unknownAnimateObject(const char *name) {
_message("Script tried to animated invalid graphic object: %s", name);
}
void Game::unknownScriptCharacter(const char *action, const char *name) {
_message("Script tried to %s using invalid character: %s", action, name);
}
PointObject *Game::unknownGoPutTarget(const Process &process, const char *action, const char *name) {
_message("Script tried to make character %s to invalid object %s", action, name);
return nullptr;
}
void Game::missingAnimation(const String &fileName) {
_message("Could not open animation %s", fileName.c_str());
}
void Game::unknownSayTextCharacter(const char *name, int32) {
unknownScriptCharacter("say text", name);
}
void Game::unknownChangeCharacterRoom(const char *name) {
_message("Invalid change character room name: %s", name);
}
void Game::unknownAnimateCharacterObject(const char *name) {
_message("Invalid animate character object: %s", name);
}
void Game::unknownAnimateTalkingObject(const char *name) {
_message("Invalid talk object name: %s", name);
}
void Game::unknownClearInventoryTarget(int characterKind) {
_message("Invalid clear inventory character kind: %d", characterKind);
}
void Game::unknownCamLerpTarget(const char *action, const char *name) {
_message("Invalid target object for %s: %s", action, name);
}
void Game::unknownKernelTask(int task) {
_message("Invalid kernel task: %d", task);
}
void Game::unknownScriptProcedure(const String &procedure) {
_message("Unknown required procedure: %s", procedure.c_str());
}
void Game::missingSound(const String &fileName) {
_message("Missing sound file: %s", fileName.c_str());
}
void Game::invalidSNDFormat(uint format, uint channels, uint freq, uint bps) {
_message("Invalid SND file, format: %u, channels: %u, freq: %u, bps: %u", format, channels, freq, bps);
}
void Game::notEnoughRoomDataRead(const char *path, int64 filePos, int64 roomEnd) {
_message("Did not read enough data (%dll < %dll) for a room in %s", filePos, roomEnd, path);
}
void Game::notEnoughObjectDataRead(const char *room, int64 filePos, int64 objectEnd) {
_message("Did not read enough data (%dll < %dll) for an object in room %s", filePos, objectEnd, room);
}
bool Game::isKnownBadVideo(int32 videoId) {
return false;
}
void Game::invalidVideo(int32 videoId, const char *context) {
_message("Could not play video %d (%s)", videoId, context);
}
}

110
engines/alcachofa/game.h Normal file
View File

@@ -0,0 +1,110 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALCACHOFA_GAME_H
#define ALCACHOFA_GAME_H
#include "alcachofa/script.h"
#include "common/textconsole.h"
#include "common/file.h"
namespace Alcachofa {
class ObjectBase;
class PointObject;
class Character;
class Door;
class Room;
class Process;
struct ScriptInstruction;
/**
* @brief Provides functionality specific to a game title.
* Also includes all exemptions to inconsistencies in the original games.
*
* If an error is truly unrecoverable or a warning never an engine bug, no method is necessary here
*/
class Game {
typedef void (*Message)(const char *s, ...);
public:
Game();
virtual ~Game() {}
virtual void onLoadedGameFiles();
virtual Common::Point getResolution() = 0;
virtual const char *const *getMapFiles() = 0; ///< Returns a nullptr-terminated list
virtual Common::Span<const ScriptOp> getScriptOpMap() = 0;
virtual Common::Span<const ScriptKernelTask> getScriptKernelTaskMap() = 0;
virtual void updateScriptVariables() = 0;
virtual bool shouldClipCamera() = 0;
virtual void drawScreenStates();
virtual bool doesRoomHaveBackground(const Room *room);
virtual void unknownRoomObject(const Common::String &type);
virtual void unknownRoomType(const Common::String &type);
virtual void unknownDoorTargetRoom(const Common::String &name);
virtual void unknownDoorTargetDoor(const Common::String &room, const Common::String &door);
virtual void invalidDialogLine(uint index);
virtual void tooManyDialogLines(uint lineCount, uint maxLineCount);
virtual void tooManyDrawRequests(int order);
virtual bool shouldCharacterTrigger(const Character *character, const char *action);
virtual bool shouldTriggerDoor(const Door *door);
virtual bool hasMortadeloVoice(const Character *character);
virtual void onUserChangedCharacter();
virtual void unknownCamSetInactiveAttribute(int attribute);
virtual void unknownFadeType(int fadeType);
virtual void unknownSerializedObject(const char *object, const char *owner, const char *room);
virtual void unknownPickupItem(const char *name);
virtual void unknownDropItem(const char *name);
virtual void unknownVariable(const char *name);
virtual void unknownInstruction(const ScriptInstruction &instruction);
virtual void unknownAnimateObject(const char *name);
virtual void unknownScriptCharacter(const char *action, const char *name);
virtual PointObject *unknownGoPutTarget(const Process &process, const char *action, const char *name); ///< May return an alternative target to use
virtual void unknownChangeCharacterRoom(const char *name);
virtual void unknownAnimateCharacterObject(const char *name);
virtual void unknownSayTextCharacter(const char *name, int32 dialogId);
virtual void unknownAnimateTalkingObject(const char *name);
virtual void unknownClearInventoryTarget(int characterKind);
virtual void unknownCamLerpTarget(const char *action, const char *name);
virtual void unknownKernelTask(int task);
virtual void unknownScriptProcedure(const Common::String &procedure);
virtual void missingAnimation(const Common::String &fileName);
virtual void missingSound(const Common::String &fileName);
virtual void invalidSNDFormat(uint format, uint channels, uint freq, uint bps);
virtual void notEnoughRoomDataRead(const char *path, int64 filePos, int64 objectEnd);
virtual void notEnoughObjectDataRead(const char *room, int64 filePos, int64 objectEnd);
virtual bool isKnownBadVideo(int32 videoId);
virtual void invalidVideo(int32 videoId, const char *context);
static Game *createForMovieAdventure();
const Message _message;
};
}
#endif // ALCACHOFA_GAME_H

View File

@@ -0,0 +1,310 @@
/* 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 "alcachofa/objects.h"
#include "alcachofa/rooms.h"
#include "alcachofa/scheduler.h"
#include "alcachofa/global-ui.h"
#include "alcachofa/alcachofa.h"
#include "common/system.h"
using namespace Common;
using namespace Math;
namespace Alcachofa {
const char *ObjectBase::typeName() const { return "ObjectBase"; }
ObjectBase::ObjectBase(Room *room, const char *name)
: _room(room)
, _name(name)
, _isEnabled(false) {
assert(room != nullptr);
}
ObjectBase::ObjectBase(Room *room, ReadStream &stream)
: _room(room) {
assert(room != nullptr);
_name = readVarString(stream);
_isEnabled = readBool(stream);
}
void ObjectBase::toggle(bool isEnabled) {
_isEnabled = isEnabled;
}
void ObjectBase::draw() {}
void ObjectBase::drawDebug() {}
void ObjectBase::update() {}
void ObjectBase::loadResources() {}
void ObjectBase::freeResources() {}
void ObjectBase::syncGame(Serializer &serializer) {
serializer.syncAsByte(_isEnabled);
}
Graphic *ObjectBase::graphic() {
return nullptr;
}
Shape *ObjectBase::shape() {
return nullptr;
}
const char *PointObject::typeName() const { return "PointObject"; }
PointObject::PointObject(Room *room, ReadStream &stream)
: ObjectBase(room, stream) {
_pos = Shape(stream).firstPoint();
}
const char *GraphicObject::typeName() const { return "GraphicObject"; }
GraphicObject::GraphicObject(Room *room, ReadStream &stream)
: ObjectBase(room, stream)
, _graphic(stream)
, _type((GraphicObjectType)stream.readSint32LE())
, _posterizeAlpha(100 - stream.readSint32LE()) {
_graphic.start(true);
}
GraphicObject::GraphicObject(Room *room, const char *name)
: ObjectBase(room, name)
, _type(GraphicObjectType::Normal)
, _posterizeAlpha(0) {}
void GraphicObject::draw() {
if (!isEnabled() || !_graphic.hasAnimation())
return;
const BlendMode blendMode = _type == GraphicObjectType::Effect
? BlendMode::Additive
: BlendMode::AdditiveAlpha;
const bool is3D = room() != &g_engine->world().inventory();
_graphic.update();
g_engine->drawQueue().add<AnimationDrawRequest>(_graphic, is3D, blendMode);
}
void GraphicObject::drawDebug() {
auto *renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
if (!isEnabled() || !_graphic.hasAnimation() || !g_engine->console().showGraphics() || renderer == nullptr)
return;
const bool is3D = room() != &g_engine->world().inventory();
Vector2d topLeft(as2D(_graphic.topLeft()));
float scale = _graphic.scale() * _graphic.depthScale() * kInvBaseScale;
Vector2d size;
if (is3D) {
Vector3d topLeftTmp = as3D(topLeft);
topLeftTmp.z() = _graphic.scale();
_graphic.animation().outputRect3D(_graphic.frameI(), scale, topLeftTmp, size);
topLeft = as2D(topLeftTmp);
} else
_graphic.animation().outputRect2D(_graphic.frameI(), scale, topLeft, size);
Vector2d points[] = {
topLeft,
topLeft + Vector2d(size.getX(), 0.0f),
topLeft + Vector2d(size.getX(), size.getY()),
topLeft + Vector2d(0.0f, size.getY()),
topLeft
};
renderer->debugPolyline({ points, 5 }, kDebugGreen);
}
void GraphicObject::loadResources() {
_graphic.loadResources();
}
void GraphicObject::freeResources() {
_graphic.freeResources();
}
void GraphicObject::syncGame(Serializer &serializer) {
ObjectBase::syncGame(serializer);
_graphic.syncGame(serializer);
}
Graphic *GraphicObject::graphic() {
return &_graphic;
}
struct AnimateTask final : public Task {
AnimateTask(Process &process, GraphicObject *object)
: Task(process)
, _object(object) {
assert(_object != nullptr);
_graphic = object->graphic();
scumm_assert(_graphic != nullptr);
_duration = _graphic->animation().totalDuration();
}
AnimateTask(Process &process, Serializer &s)
: Task(process) {
AnimateTask::syncGame(s);
}
TaskReturn run() override {
TASK_BEGIN;
_object->toggle(true);
_graphic->start(false);
TASK_WAIT(1, delay(_duration));
_object->toggle(false); //-V779
TASK_END;
}
void debugPrint() override {
g_engine->getDebugger()->debugPrintf("Animate \"%s\" for %ums", _object->name().c_str(), _duration);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
s.syncAsUint32LE(_duration);
syncObjectAsString(s, _object);
_graphic = _object->graphic();
scumm_assert(_graphic != nullptr);
}
const char *taskName() const override;
private:
GraphicObject *_object = nullptr;
Graphic *_graphic = nullptr;
uint32 _duration = 0;
};
DECLARE_TASK(AnimateTask)
Task *GraphicObject::animate(Process &process) {
return new AnimateTask(process, this);
}
const char *SpecialEffectObject::typeName() const { return "SpecialEffectObject"; }
SpecialEffectObject::SpecialEffectObject(Room *room, ReadStream &stream)
: GraphicObject(room, stream) {
_topLeft = Shape(stream).firstPoint();
_bottomRight = Shape(stream).firstPoint();
_texShift.setX(stream.readSint32LE());
_texShift.setY(stream.readSint32LE());
_texShift *= kShiftSpeed;
}
void SpecialEffectObject::draw() {
if (!isEnabled() || !g_engine->config().highQuality())
return;
const auto texOffset = g_engine->getMillis() * 0.001f * _texShift;
const BlendMode blendMode = _type == GraphicObjectType::Effect
? BlendMode::Additive
: BlendMode::AdditiveAlpha;
Point topLeft = _topLeft, bottomRight = _bottomRight;
if (topLeft.x == bottomRight.x || topLeft.y == bottomRight.y) {
topLeft = _graphic.topLeft();
bottomRight = topLeft + _graphic.animation().imageSize(0);
}
_graphic.update();
g_engine->drawQueue().add<SpecialEffectDrawRequest>(_graphic, topLeft, bottomRight, texOffset, blendMode);
}
const char *ShapeObject::typeName() const { return "ShapeObject"; }
ShapeObject::ShapeObject(Room *room, ReadStream &stream)
: ObjectBase(room, stream)
, _shape(stream)
, _cursorType((CursorType)stream.readSint32LE()) {}
void ShapeObject::update() {
if (isEnabled())
updateSelection();
else {
_isNewlySelected = false;
_wasSelected = false;
}
}
void ShapeObject::syncGame(Serializer &serializer) {
ObjectBase::syncGame(serializer);
serializer.syncAsSByte(_order);
_isNewlySelected = false;
_wasSelected = false;
}
Shape *ShapeObject::shape() {
return &_shape;
}
CursorType ShapeObject::cursorType() const {
return _cursorType;
}
void ShapeObject::onHoverStart() {
onHoverUpdate();
}
void ShapeObject::onHoverEnd() {}
void ShapeObject::onHoverUpdate() {
g_engine->drawQueue().add<TextDrawRequest>(
g_engine->globalUI().generalFont(),
g_engine->world().getLocalizedName(name()),
g_engine->input().mousePos2D() - Point(0, 35),
-1, true, kWhite, -kForegroundOrderCount);
}
void ShapeObject::onClick() {
onHoverUpdate();
}
void ShapeObject::markSelected() {
_isNewlySelected = true;
}
void ShapeObject::updateSelection() {
if (_isNewlySelected) {
_isNewlySelected = false;
if (_wasSelected) {
if (g_engine->input().wasAnyMouseReleased() && g_engine->player().selectedObject() == this)
onClick();
else
onHoverUpdate();
} else {
_wasSelected = true;
onHoverStart();
}
} else if (_wasSelected) {
_wasSelected = false;
onHoverEnd();
}
}
const char *PhysicalObject::typeName() const { return "PhysicalObject"; }
PhysicalObject::PhysicalObject(Room *room, ReadStream &stream)
: ShapeObject(room, stream) {
_order = stream.readSByte();
}
}

View File

@@ -0,0 +1,265 @@
/* 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 "alcachofa/global-ui.h"
#include "alcachofa/menu.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/script.h"
using namespace Common;
namespace Alcachofa {
// originally the inventory only reacts to exactly top-left/bottom-right which is fine in
// fullscreen when you just slam the mouse cursor into the corner.
// In any other scenario this is cumbersome so I expand this area.
// And it is still pretty bad, especially in windowed mode so there is a key to open/close as well
static constexpr int16 kInventoryTriggerSize = 10;
Rect openInventoryTriggerBounds() {
int16 size = kInventoryTriggerSize * 1024 / g_system->getWidth();
return Rect(0, 0, size, size);
}
Rect closeInventoryTriggerBounds() {
int16 size = kInventoryTriggerSize * 1024 / g_system->getWidth();
return Rect(g_system->getWidth() - size, g_system->getHeight() - size, g_system->getWidth(), g_system->getHeight());
}
GlobalUI::GlobalUI() {
auto &world = g_engine->world();
_generalFont.reset(new Font(world.getGlobalAnimationName(GlobalAnimationKind::GeneralFont)));
_dialogFont.reset(new Font(world.getGlobalAnimationName(GlobalAnimationKind::DialogFont)));
_iconMortadelo.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::MortadeloIcon)));
_iconFilemon.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::FilemonIcon)));
_iconInventory.reset(new Animation(world.getGlobalAnimationName(GlobalAnimationKind::InventoryIcon)));
_generalFont->load();
_dialogFont->load();
_iconMortadelo->load();
_iconFilemon->load();
_iconInventory->load();
}
void GlobalUI::startClosingInventory() {
_isOpeningInventory = false;
_isClosingInventory = true;
_timeForInventory = g_engine->getMillis();
updateClosingInventory(); // prevents the first frame of closing to not render the inventory overlay
}
void GlobalUI::updateClosingInventory() {
static constexpr uint32 kDuration = 300;
static constexpr float kSpeed = -10 / 3.0f / 1000.0f;
uint32 deltaTime = g_engine->getMillis() - _timeForInventory;
if (!_isClosingInventory || deltaTime >= kDuration)
_isClosingInventory = false;
else
g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed)));
}
bool GlobalUI::updateOpeningInventory() {
static constexpr float kSpeed = 10 / 3.0f / 1000.0f;
if (g_engine->menu().isOpen() || !g_engine->player().isGameLoaded())
return false;
const bool userWantsToOpenInventory =
openInventoryTriggerBounds().contains(g_engine->input().mousePos2D()) ||
g_engine->input().wasInventoryKeyPressed();
if (_isOpeningInventory) {
uint32 deltaTime = g_engine->getMillis() - _timeForInventory;
if (deltaTime >= 1000) {
_isOpeningInventory = false;
g_engine->world().inventory().open();
} else {
deltaTime = MIN<uint32>(300, deltaTime);
g_engine->world().inventory().drawAsOverlay((int32)(g_system->getHeight() * (deltaTime * kSpeed - 1)));
}
return true;
} else if (userWantsToOpenInventory) {
_isClosingInventory = false;
_isOpeningInventory = true;
_timeForInventory = g_engine->getMillis();
g_engine->player().activeCharacter()->stopWalking();
g_engine->world().inventory().updateItemsByActiveCharacter();
return true;
}
return false;
}
Animation *GlobalUI::activeAnimation() const {
return g_engine->player().activeCharacterKind() == MainCharacterKind::Mortadelo
? _iconFilemon.get()
: _iconMortadelo.get();
}
bool GlobalUI::isHoveringChangeButton() const {
auto mousePos = g_engine->input().mousePos2D();
auto anim = activeAnimation();
auto offset = anim->totalFrameOffset(0);
auto bounds = anim->frameBounds(0);
const int minX = g_system->getWidth() + offset.x;
const int maxY = bounds.height() + offset.y;
return mousePos.x >= minX && mousePos.y <= maxY;
}
bool GlobalUI::updateChangingCharacter() {
auto &player = g_engine->player();
if (g_engine->menu().isOpen() ||
!player.isGameLoaded() ||
_isOpeningInventory)
return false;
_changeButton.frameI() = 0;
if (!isHoveringChangeButton())
return false;
if (g_engine->input().wasMouseLeftPressed()) {
player.pressedObject() = &_changeButton;
return true;
}
if (player.pressedObject() != &_changeButton)
return true;
player.setActiveCharacter(player.inactiveCharacter()->kind());
player.heldItem() = nullptr;
g_engine->camera().setFollow(player.activeCharacter());
g_engine->camera().restore(0);
player.changeRoom(player.activeCharacter()->room()->name(), false);
g_engine->game().onUserChangedCharacter();
int32 characterJingle = g_engine->script().variable(
player.activeCharacterKind() == MainCharacterKind::Mortadelo
? "PistaMorta"
: "PistaFile"
);
g_engine->sounds().startMusic(characterJingle);
g_engine->sounds().queueMusic(player.currentRoom()->musicID());
_changeButton.setAnimation(activeAnimation());
_changeButton.start(false);
return true;
}
void GlobalUI::drawChangingButton() {
auto &player = g_engine->player();
if (g_engine->menu().isOpen() ||
!player.isGameLoaded() ||
!player.semaphore().isReleased() ||
_isOpeningInventory ||
_isClosingInventory)
return;
auto anim = activeAnimation();
if (!_changeButton.hasAnimation() || &_changeButton.animation() != anim) {
_changeButton.setAnimation(anim);
_changeButton.pause();
_changeButton.lastTime() = 42 * (anim->frameCount() - 1) + 1;
}
_changeButton.topLeft() = { (int16)(g_system->getWidth() + 2), -2 };
if (isHoveringChangeButton() &&
g_engine->input().isMouseLeftDown() &&
player.pressedObject() == &_changeButton) {
_changeButton.topLeft().x -= 2;
_changeButton.topLeft().y += 2;
}
_changeButton.order() = -9;
_changeButton.update();
g_engine->drawQueue().add<AnimationDrawRequest>(_changeButton, false, BlendMode::AdditiveAlpha);
}
struct CenterBottomTextTask final : public Task {
CenterBottomTextTask(Process &process, int32 dialogId, uint32 durationMs)
: Task(process)
, _dialogId(dialogId)
, _durationMs(durationMs) {}
CenterBottomTextTask(Process &process, Serializer &s)
: Task(process) {
CenterBottomTextTask::syncGame(s);
}
TaskReturn run() override {
Font &font = g_engine->globalUI().dialogFont();
const char *text = g_engine->world().getDialogLine(_dialogId);
const Point pos(
g_system->getWidth() / 2,
g_system->getHeight() - 200
);
TASK_BEGIN;
_startTime = g_engine->getMillis();
while (g_engine->getMillis() - _startTime < _durationMs) {
if (process().isActiveForPlayer()) {
g_engine->drawQueue().add<TextDrawRequest>(
font, text, pos, -1, true, kWhite, -kForegroundOrderCount + 1);
}
TASK_YIELD(1);
}
TASK_END;
}
void debugPrint() override {
uint32 remaining = g_engine->getMillis() - _startTime <= _durationMs
? _durationMs - (g_engine->getMillis() - _startTime)
: 0;
g_engine->console().debugPrintf("CenterBottomText (%d) with %ums remaining\n", _dialogId, remaining);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
s.syncAsSint32LE(_dialogId);
s.syncAsUint32LE(_startTime);
s.syncAsUint32LE(_durationMs);
}
const char *taskName() const override;
private:
int32 _dialogId = 0;
uint32 _startTime = 0, _durationMs = 0;
};
DECLARE_TASK(CenterBottomTextTask)
Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs) {
return new CenterBottomTextTask(process, dialogId, durationMs);
}
void GlobalUI::drawScreenStates() {
if (g_engine->menu().isOpen())
return;
auto &drawQueue = g_engine->drawQueue();
if (_isPermanentFaded)
drawQueue.add<FadeDrawRequest>(FadeType::ToBlack, 1.0f, -9);
else
g_engine->game().drawScreenStates();
}
void GlobalUI::syncGame(Serializer &s) {
s.syncAsByte(_isPermanentFaded);
}
}

View File

@@ -0,0 +1,73 @@
/* 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 ALCACHOFA_GLOBAL_UI_H
#define ALCACHOFA_GLOBAL_UI_H
#include "alcachofa/objects.h"
namespace Alcachofa {
Common::Rect openInventoryTriggerBounds();
Common::Rect closeInventoryTriggerBounds();
class GlobalUI {
public:
GlobalUI();
inline Font &generalFont() const { assert(_generalFont != nullptr); return *_generalFont; }
inline Font &dialogFont() const { assert(_dialogFont != nullptr); return *_dialogFont; }
inline bool &isPermanentFaded() { return _isPermanentFaded; }
bool updateChangingCharacter();
void drawChangingButton();
bool updateOpeningInventory();
void updateClosingInventory();
void startClosingInventory();
void drawScreenStates(); // black borders and/or permanent fade
void syncGame(Common::Serializer &s);
private:
Animation *activeAnimation() const;
bool isHoveringChangeButton() const;
Graphic _changeButton;
Common::ScopedPtr<Font>
_generalFont,
_dialogFont;
Common::ScopedPtr<Animation>
_iconMortadelo,
_iconFilemon,
_iconInventory;
bool
_isOpeningInventory = false,
_isClosingInventory = false,
_isPermanentFaded = false;
uint32 _timeForInventory = 0;
};
Task *showCenterBottomText(Process &process, int32 dialogId, uint32 durationMs);
}
#endif // ALCACHOFA_GLOBAL_UI_H

View File

@@ -0,0 +1,141 @@
/* 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 "alcachofa/graphics-opengl-base.h"
#include "common/config-manager.h"
#include "common/system.h"
#include "graphics/renderer.h"
using namespace Common;
using namespace Graphics;
using namespace Math;
namespace Alcachofa {
OpenGLRendererBase::OpenGLRendererBase(Point resolution) : _resolution(resolution) {}
bool OpenGLRendererBase::hasOutput() const {
return _currentOutput != nullptr;
}
void OpenGLRendererBase::resetState() {
setViewportToScreen();
_currentOutput = nullptr;
_currentLodBias = -1000.0f;
_currentBlendMode = (BlendMode)-1;
_isFirstDrawCommand = true;
}
void OpenGLRendererBase::setViewportToScreen() {
int32 screenWidth = g_system->getWidth();
int32 screenHeight = g_system->getHeight();
Rect viewport(
MIN<int32>(screenWidth, screenHeight * (float)_resolution.x / _resolution.y),
MIN<int32>(screenHeight, screenWidth * (float)_resolution.y / _resolution.x));
viewport.translate(
(screenWidth - viewport.width()) / 2,
(screenHeight - viewport.height()) / 2);
setViewportInner(viewport.left, viewport.top, viewport.width(), viewport.height());
setMatrices(true);
}
void OpenGLRendererBase::setViewportToRect(int16 outputWidth, int16 outputHeight) {
_outputSize.x = MIN(outputWidth, g_system->getWidth());
_outputSize.y = MIN(outputHeight, g_system->getHeight());
setViewportInner(0, 0, _outputSize.x, _outputSize.y);
setMatrices(false);
}
void OpenGLRendererBase::getQuadPositions(Vector2d topLeft, Vector2d size, Angle rotation, Vector2d positions[]) const {
positions[0] = topLeft + Vector2d(0, 0);
positions[1] = topLeft + Vector2d(0, +size.getY());
positions[2] = topLeft + Vector2d(+size.getX(), +size.getY());
positions[3] = topLeft + Vector2d(+size.getX(), 0);
if (abs(rotation.getDegrees()) > epsilon) {
const Vector2d zero(0, 0);
for (int i = 0; i < 4; i++)
positions[i].rotateAround(zero, rotation);
}
}
void OpenGLRendererBase::getQuadTexCoords(Vector2d texMin, Vector2d texMax, Vector2d texCoords[]) const {
texCoords[0] = { texMin.getX(), texMin.getY() };
texCoords[1] = { texMin.getX(), texMax.getY() };
texCoords[2] = { texMax.getX(), texMax.getY() };
texCoords[3] = { texMax.getX(), texMin.getY() };
}
IRenderer *IRenderer::createOpenGLRenderer(Point resolution) {
const auto available = Renderer::getAvailableTypes();
const auto &rendererCode = ConfMan.get("renderer");
RendererType rendererType = Renderer::parseTypeCode(rendererCode);
rendererType = (RendererType)(rendererType & available);
IRenderer *renderer = nullptr;
switch (rendererType) {
case kRendererTypeOpenGLShaders:
renderer = createOpenGLRendererShaders(resolution);
break;
case kRendererTypeOpenGL:
renderer = createOpenGLRendererClassic(resolution);
break;
case kRendererTypeTinyGL:
renderer = createTinyGLRenderer(resolution);
break;
default:
if (available & kRendererTypeOpenGLShaders)
renderer = createOpenGLRendererShaders(resolution);
else if (available & kRendererTypeOpenGL)
renderer = createOpenGLRendererClassic(resolution);
else if (available & kRendererTypeTinyGL)
renderer = createTinyGLRenderer(resolution);
break;
}
if (renderer == nullptr)
error("Could not create a renderer, available: %d", (int)available);
return renderer;
}
#ifndef USE_OPENGL_SHADERS
IRenderer *IRenderer::createOpenGLRendererShaders(Point _) {
(void)_;
return nullptr;
}
#endif
#ifndef USE_OPENGL_GAME
IRenderer *IRenderer::createOpenGLRendererClassic(Point _) {
(void)_;
return nullptr;
}
#endif
#ifndef USE_TINYGL
IRenderer *IRenderer::createTinyGLRenderer(Point _) {
(void)_;
return nullptr;
}
#endif
}

View File

@@ -0,0 +1,56 @@
/* 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 ALCACHOFA_GRAPHICS_OPENGL_BASE_H
#define ALCACHOFA_GRAPHICS_OPENGL_BASE_H
// This file shall not use any OpenGL API surface or include headers related to it
#include "alcachofa/graphics.h"
namespace Alcachofa {
class OpenGLRendererBase : public virtual IRenderer {
public:
OpenGLRendererBase(Common::Point resolution);
bool hasOutput() const override;
protected:
virtual void setViewportInner(int x, int y, int width, int height) = 0;
virtual void setMatrices(bool flipped) = 0;
void resetState();
void setViewportToScreen();
void setViewportToRect(int16 outputWidth, int16 outputHeight);
void getQuadPositions(Math::Vector2d topLeft, Math::Vector2d size, Math::Angle rotation, Math::Vector2d positions[]) const;
void getQuadTexCoords(Math::Vector2d texMin, Math::Vector2d texMax, Math::Vector2d texCoords[]) const;
Common::Point _resolution, _outputSize;
Graphics::Surface *_currentOutput = nullptr;
BlendMode _currentBlendMode = (BlendMode)-1;
float _currentLodBias = 0.0f;
bool _isFirstDrawCommand = false;
};
}
#endif // ALCACHOFA_GRAPHICS_OPENGL_BASE_H

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/>.
*
*/
#include "alcachofa/graphics-opengl.h"
#include "alcachofa/detection.h"
#include "common/system.h"
#include "engines/util.h"
using namespace Common;
using namespace Math;
using namespace Graphics;
namespace Alcachofa {
class OpenGLRendererClassic : public OpenGLRenderer, public virtual IDebugRenderer {
public:
using OpenGLRenderer::OpenGLRenderer;
void begin() override {
GL_CALL(glEnableClientState(GL_VERTEX_ARRAY));
GL_CALL(glDisableClientState(GL_INDEX_ARRAY));
GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
resetState();
_currentTexture = nullptr;
}
void setTexture(ITexture *texture) override {
if (texture == _currentTexture)
return;
else if (texture == nullptr) {
GL_CALL(glDisable(GL_TEXTURE_2D));
GL_CALL(glDisableClientState(GL_TEXTURE_COORD_ARRAY));
_currentTexture = nullptr;
} else {
if (_currentTexture == nullptr) {
GL_CALL(glEnable(GL_TEXTURE_2D));
GL_CALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY));
}
auto glTexture = dynamic_cast<OpenGLTexture *>(texture);
assert(glTexture != nullptr);
GL_CALL(glBindTexture(GL_TEXTURE_2D, glTexture->handle())); //-V522
_currentTexture = glTexture;
}
}
void setBlendMode(BlendMode blendMode) override {
if (blendMode == _currentBlendMode)
return;
setBlendFunc(blendMode);
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE));
switch (blendMode) {
case BlendMode::AdditiveAlpha:
case BlendMode::Additive:
case BlendMode::Multiply:
// TintAlpha * TexColor, TexAlpha
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_ALPHA)); // alpha replaces color
break;
case BlendMode::Alpha:
// TexColor, TintAlpha
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PRIMARY_COLOR));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
break;
case BlendMode::Tinted:
// (TintColor * TintAlpha) * TexColor, TexAlpha
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR));
GL_CALL(glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR)); // we have to pre-multiply
break;
default:
assert(false && "Invalid blend mode");
break;
}
_currentBlendMode = blendMode;
}
void setLodBias(float lodBias) override {
if (abs(_currentLodBias - lodBias) < epsilon)
return;
GL_CALL(glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, lodBias));
_currentLodBias = lodBias;
}
void quad(
Vector2d topLeft,
Vector2d size,
Color color,
Angle rotation,
Vector2d texMin,
Vector2d texMax) override {
Vector2d positions[4], texCoords[4];
getQuadPositions(topLeft, size, rotation, positions);
getQuadTexCoords(texMin, texMax, texCoords);
if (_currentTexture != nullptr) {
// float equality is fine here, if it was calculated it was not a normal graphic
_currentTexture->setMirrorWrap(texMin != Vector2d() || texMax != Vector2d(1, 1));
}
float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
if (_currentBlendMode == BlendMode::Tinted) {
colors[0] *= colors[3];
colors[1] *= colors[3];
colors[2] *= colors[3];
}
checkFirstDrawCommand();
GL_CALL(glColor4fv(colors));
GL_CALL(glVertexPointer(2, GL_FLOAT, 0, positions));
if (_currentTexture != nullptr)
GL_CALL(glTexCoordPointer(2, GL_FLOAT, 0, texCoords));
GL_CALL(glDrawArrays(GL_QUADS, 0, 4));
#ifdef ALCACHOFA_DEBUG
// make sure we crash instead of someone using our stack arrays
GL_CALL(glVertexPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
GL_CALL(glTexCoordPointer(2, GL_FLOAT, sizeof(Vector2d), nullptr));
#endif
}
void debugPolygon(
Span<Vector2d> points,
Color color
) override {
checkFirstDrawCommand();
setTexture(nullptr);
setBlendMode(BlendMode::Alpha);
GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
GL_CALL(glLineWidth(4.0f));
GL_CALL(glPointSize(8.0f));
GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
if (points.size() > 2)
GL_CALL(glDrawArrays(GL_POLYGON, 0, points.size()));
color.a = (byte)(MIN(255.0f, color.a * 1.3f));
GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
if (points.size() > 1)
GL_CALL(glDrawArrays(GL_LINE_LOOP, 0, points.size()));
color.a = (byte)(MIN(255.0f, color.a * 1.3f));
GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
if (points.size() > 0)
GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
}
void debugPolyline(
Span<Vector2d> points,
Color color
) override {
checkFirstDrawCommand();
setTexture(nullptr);
setBlendMode(BlendMode::Alpha);
GL_CALL(glVertexPointer(2, GL_FLOAT, 0, points.data()));
GL_CALL(glLineWidth(4.0f));
GL_CALL(glPointSize(8.0f));
GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
if (points.size() > 1)
GL_CALL(glDrawArrays(GL_LINE_STRIP, 0, points.size()));
color.a = (byte)(MIN(255.0f, color.a * 1.3f));
GL_CALL(glColor4ub(color.r, color.g, color.b, color.a));
if (points.size() > 0)
GL_CALL(glDrawArrays(GL_POINTS, 0, points.size()));
}
void setMatrices(bool flipped) override {
float bottom = flipped ? _resolution.y : 0.0f;
float top = flipped ? 0.0f : _resolution.y;
GL_CALL(glMatrixMode(GL_PROJECTION));
GL_CALL(glLoadIdentity());
GL_CALL(glOrtho(0.0f, _resolution.x, bottom, top, -1.0f, 1.0f));
GL_CALL(glMatrixMode(GL_MODELVIEW));
GL_CALL(glLoadIdentity());
}
};
IRenderer *IRenderer::createOpenGLRendererClassic(Point resolution) {
debug("Use OpenGL classic renderer");
return new OpenGLRendererClassic(resolution);
}
}

View File

@@ -0,0 +1,263 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "alcachofa/alcachofa.h"
#include "alcachofa/graphics-opengl.h"
#include "alcachofa/detection.h"
#include "common/system.h"
#include "engines/util.h"
#include "graphics/opengl/shader.h"
using namespace Common;
using namespace Math;
using namespace Graphics;
using namespace OpenGL;
namespace Alcachofa {
class OpenGLRendererShaders : public OpenGLRenderer {
struct Vertex {
Vector2d pos;
Vector2d uv;
Color color;
};
struct VBO {
VBO(GLuint bufferId) : _bufferId(bufferId) {}
~VBO() {
Shader::freeBuffer(_bufferId);
}
GLuint _bufferId;
uint _capacity = 0;
};
public:
OpenGLRendererShaders(Point resolution)
: OpenGLRenderer(resolution) {
static constexpr const char *const kAttributes[] = {
"in_pos",
"in_uv",
"in_color",
nullptr
};
if (!_shader.loadFromStrings("alcachofa", kVertexShader, kFragmentShader, kAttributes))
error("Could not load shader");
// we use more than one VBO to reduce implicit synchronization
for (int i = 0; i < 4; i++)
_vbos.emplace_back(Shader::createBuffer(GL_ARRAY_BUFFER, 0, nullptr, GL_STREAM_DRAW));
_vertices.resize(8 * 6); // heuristic, we should be lucky if we can batch 8 quads together
_whiteTexture.reset(new OpenGLTexture(1, 1, false));
const byte whiteData[] = { 0xff, 0xff, 0xff, 0xff };
GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whiteData));
}
void begin() override {
resetState();
_currentTexture = nullptr;
_needsNewBatch = true;
}
void end() override {
if (!_vertices.empty()) // submit last batch
checkFirstDrawCommand();
OpenGLRenderer::end();
}
void setTexture(ITexture *texture) override {
if (texture == _currentTexture)
return;
_needsNewBatch = true;
if (texture == nullptr)
_currentTexture = nullptr;
else {
_currentTexture = dynamic_cast<OpenGLTexture *>(texture);
assert(_currentTexture != nullptr);
}
}
void setBlendMode(BlendMode blendMode) override {
if (blendMode == _currentBlendMode)
return;
_needsNewBatch = true;
_currentBlendMode = blendMode;
}
void setLodBias(float lodBias) override {
if (abs(_currentLodBias - lodBias) < epsilon)
return;
_needsNewBatch = true;
_currentLodBias = lodBias;
}
void quad(
Vector2d topLeft,
Vector2d size,
Color color,
Angle rotation,
Vector2d texMin,
Vector2d texMax) override {
if (_needsNewBatch) {
_needsNewBatch = false;
checkFirstDrawCommand();
}
if (_currentTexture != nullptr) {
// float equality is fine here, if it was calculated it was not a normal graphic
_currentTexture->setMirrorWrap(texMin != Vector2d() || texMax != Vector2d(1, 1));
}
Vector2d positions[4], texCoords[4];
getQuadPositions(topLeft, size, rotation, positions);
getQuadTexCoords(texMin, texMax, texCoords);
_vertices.push_back({ positions[0], texCoords[0], color });
_vertices.push_back({ positions[1], texCoords[1], color });
_vertices.push_back({ positions[2], texCoords[2], color });
_vertices.push_back({ positions[0], texCoords[0], color });
_vertices.push_back({ positions[2], texCoords[2], color });
_vertices.push_back({ positions[3], texCoords[3], color });
}
void setMatrices(bool flipped) override {
// adapted from https://en.wikipedia.org/wiki/Orthographic_projection
const float left = 0.0f;
const float right = _resolution.x;
const float bottom = flipped ? _resolution.y : 0.0f;
const float top = flipped ? 0.0f : _resolution.y;
const float near = -1.0f;
const float far = 1.0f;
_projection.setToIdentity();
_projection(0, 0) = 2.0f / (right - left);
_projection(1, 1) = 2.0f / (top - bottom);
_projection(2, 2) = -2.0f / (far - near);
_projection(3, 0) = -(right + left) / (right - left);
_projection(3, 1) = -(top + bottom) / (top - bottom);
_projection(3, 2) = -(far + near) / (far - near);
}
private:
void checkFirstDrawCommand() {
OpenGLRenderer::checkFirstDrawCommand();
// submit batch
if (!_vertices.empty()) {
auto &vbo = _vbos[_curVBO];
_curVBO = (_curVBO + 1) % _vbos.size();
_shader.enableVertexAttribute("in_pos", vbo._bufferId, 2, GL_FLOAT, false, sizeof(Vertex), offsetof(Vertex, pos));
_shader.enableVertexAttribute("in_uv", vbo._bufferId, 2, GL_FLOAT, false, sizeof(Vertex), offsetof(Vertex, uv));
_shader.enableVertexAttribute("in_color", vbo._bufferId, 4, GL_UNSIGNED_BYTE, true, sizeof(Vertex), offsetof(Vertex, color));
_shader.use(true);
GL_CALL(glBindTexture(GL_TEXTURE_2D, _batchTexture == nullptr
? _whiteTexture->handle()
: _batchTexture->handle()));
GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, vbo._bufferId));
if (vbo._capacity < _vertices.size()) {
vbo._capacity = _vertices.size();
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * _vertices.size(), _vertices.data(), GL_STREAM_DRAW);
} else
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(Vertex) * _vertices.size(), _vertices.data());
glDrawArrays(GL_TRIANGLES, 0, _vertices.size());
_vertices.clear();
}
// setup next batch
setBlendFunc(_currentBlendMode);
_shader.setUniform("projection", _projection);
_shader.setUniform("blendMode", _currentTexture == nullptr ? 5 : (int)_currentBlendMode);
_shader.setUniform("posterize", g_engine->config().highQuality() ? 1 : 0);
_shader.setUniform1f("lodBias", _currentLodBias);
_shader.setUniform("texture", 0);
_batchTexture = _currentTexture;
}
Matrix4 _projection;
Shader _shader;
Array<VBO> _vbos;
Array<Vertex> _vertices;
uint _curVBO = 0;
bool _needsNewBatch = false;
OpenGLTexture *_batchTexture = nullptr;
ScopedPtr<OpenGLTexture> _whiteTexture;
static constexpr const char *const kVertexShader = R"(
uniform mat4 projection;
attribute vec2 in_pos;
attribute vec2 in_uv;
attribute vec4 in_color;
varying vec2 var_uv;
varying vec4 var_color;
void main() {
gl_Position = projection * vec4(in_pos, 0.0, 1.0);
var_uv = in_uv;
var_color = in_color;
})";
static constexpr const char *const kFragmentShader = R"(
#ifdef GL_ES
precision mediump float;
#endif
uniform sampler2D texture;
uniform int blendMode;
uniform int posterize;
uniform float lodBias;
varying vec2 var_uv;
varying vec4 var_color;
void main() {
vec4 tex_color = texture2D(texture, var_uv, lodBias);
if (blendMode <= 2) { // AdditiveAlpha, Additive and Multiply
gl_FragColor.rgb = tex_color.rgb * var_color.a;
gl_FragColor.a = tex_color.a;
} else if (blendMode == 3) { // Alpha
gl_FragColor.rgb = tex_color.rgb;
gl_FragColor.a = var_color.a;
} else if (blendMode == 4) { // Tinted
gl_FragColor.rgb = var_color.rgb * var_color.a * tex_color.rgb;
gl_FragColor.a = tex_color.a;
} else { // Disabled texture
gl_FragColor = var_color;
}
if (posterize == 0) {
// shave off 3 bits for that 16-bit look
gl_FragColor = floor(gl_FragColor * (256.0 / 8.0)) / (256.0 / 8.0);
}
})";
};
IRenderer *IRenderer::createOpenGLRendererShaders(Point resolution) {
debug("Use OpenGL shaders renderer");
return new OpenGLRendererShaders(resolution);
}
}

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/>.
*
*/
#include "alcachofa/alcachofa.h"
#include "alcachofa/graphics.h"
#include "alcachofa/detection.h"
#include "alcachofa/graphics-opengl.h"
#include "common/system.h"
#include "common/translation.h"
#include "common/config-manager.h"
#include "engines/util.h"
#include "graphics/renderer.h"
#include "gui/error.h"
using namespace Common;
using namespace Math;
using namespace Graphics;
namespace Alcachofa {
//
// OpenGL classes, calls to gl* are allowed here
//
OpenGLTexture::OpenGLTexture(int32 w, int32 h, bool withMipmaps)
: ITexture({ (int16)w, (int16)h })
, _withMipmaps(withMipmaps) {
glEnable(GL_TEXTURE_2D); // will error on GLES2, but that is okay
OpenGL::clearGLError(); // we will just ignore it
GL_CALL(glGenTextures(1, &_handle));
GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR));
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
setMirrorWrap(false);
}
OpenGLTexture::~OpenGLTexture() {
if (_handle != 0)
GL_CALL(glDeleteTextures(1, &_handle));
}
void OpenGLTexture::setMirrorWrap(bool wrap) {
if (_mirrorWrap == wrap)
return;
_mirrorWrap = wrap;
GLint wrapMode;
if (wrap)
wrapMode = OpenGLContext.textureMirrorRepeatSupported ? GL_MIRRORED_REPEAT : GL_REPEAT;
else
#if USE_FORCED_GLES2 // GLES2 does not define GL_CLAMP
wrapMode = GL_CLAMP_TO_EDGE;
#else
wrapMode = OpenGLContext.textureEdgeClampSupported ? GL_CLAMP_TO_EDGE : GL_CLAMP;
#endif
GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode));
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode));
}
void OpenGLTexture::update(const Surface &surface) {
assert(surface.format == g_engine->renderer().getPixelFormat());
assert(surface.w == size().x && surface.h == size().y);
const void *pixels = surface.getPixels();
glEnable(GL_TEXTURE_2D);
OpenGL::clearGLError();
GL_CALL(glBindTexture(GL_TEXTURE_2D, _handle));
GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size().x, size().y, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels));
if (_withMipmaps)
GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));
else
GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0));
}
OpenGLRenderer::OpenGLRenderer(Point resolution) : OpenGLRendererBase(resolution) {
initGraphics3d(resolution.x, resolution.y);
GL_CALL(glDisable(GL_DEPTH_TEST));
GL_CALL(glDisable(GL_SCISSOR_TEST));
GL_CALL(glDisable(GL_STENCIL_TEST));
GL_CALL(glDisable(GL_CULL_FACE));
GL_CALL(glEnable(GL_BLEND));
GL_CALL(glDepthMask(GL_FALSE));
if (!OpenGLContext.textureMirrorRepeatSupported) {
GUI::displayErrorDialog(_("Old OpenGL detected, some graphical errors will occur."));
}
}
ScopedPtr<ITexture> OpenGLRenderer::createTexture(int32 w, int32 h, bool withMipmaps) {
assert(w >= 0 && h >= 0);
return ScopedPtr<ITexture>(new OpenGLTexture(w, h, withMipmaps));
}
PixelFormat OpenGLRenderer::getPixelFormat() const {
return PixelFormat::createFormatRGBA32();
}
bool OpenGLRenderer::requiresPoTTextures() const {
return !OpenGLContext.NPOTSupported;
}
void OpenGLRenderer::end() {
GL_CALL(glFlush());
if (_currentOutput != nullptr) {
g_system->presentBuffer();
GL_CALL(glReadPixels(
0,
0,
_outputSize.x,
_outputSize.y,
GL_RGBA,
GL_UNSIGNED_BYTE,
_currentOutput->getPixels()
));
}
}
void OpenGLRenderer::setBlendFunc(BlendMode blendMode) {
switch (blendMode) {
case BlendMode::AdditiveAlpha:
GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
break;
case BlendMode::Additive:
GL_CALL(glBlendFunc(GL_ONE, GL_ONE));
break;
case BlendMode::Multiply:
GL_CALL(glBlendFunc(GL_DST_COLOR, GL_ONE));
break;
case BlendMode::Alpha:
GL_CALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
break;
case BlendMode::Tinted:
GL_CALL(glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
break;
default: assert(false && "Invalid blend mode"); break;
}
}
void OpenGLRenderer::setOutput(Surface &output) {
assert(_isFirstDrawCommand);
setViewportToRect(output.w, output.h);
_currentOutput = &output;
// just debug warnings as it will only produce a graphical glitch while
// there is some chance the resolution could change from here to ::end
// and this is per-frame so maybe don't spam the console with the same message
if (output.w > g_system->getWidth() || output.h > g_system->getHeight())
debugC(0, kDebugGraphics, "Output is larger than screen, output will be cropped (%d, %d) > (%d, %d)",
output.w, output.h, g_system->getWidth(), g_system->getHeight());
if (output.format != getPixelFormat()) {
auto formatString = output.format.toString();
debugC(0, kDebugGraphics, "Cannot use pixelformat of given output surface: %s", formatString.c_str());
_currentOutput = nullptr;
}
if (output.pitch != output.format.bytesPerPixel * output.w) {
// Maybe there would be a way with glPixelStore
debugC(0, kDebugGraphics, "Incompatible output surface pitch");
_currentOutput = nullptr;
}
}
void OpenGLRenderer::setViewportInner(int x, int y, int width, int height) {
GL_CALL(glViewport(x, y, width, height));
}
void OpenGLRenderer::checkFirstDrawCommand() {
// We delay clearing the screen. It is much easier for the game
// to switch to a framebuffer before
if (!_isFirstDrawCommand)
return;
_isFirstDrawCommand = false;
GL_CALL(glClearColor(0.0f, 0.0f, 0.0f, 1.0f));
GL_CALL(glClear(GL_COLOR_BUFFER_BIT));
}
}

View File

@@ -0,0 +1,68 @@
/* 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 ALCACHOFA_GRAPHICS_OPENGL_H
#define ALCACHOFA_GRAPHICS_OPENGL_H
#include "alcachofa/graphics-opengl-base.h"
#include "graphics/managed_surface.h"
#include "graphics/opengl/system_headers.h"
#include "graphics/opengl/debug.h"
namespace Alcachofa {
class OpenGLTexture : public ITexture {
public:
OpenGLTexture(int32 w, int32 h, bool withMipmaps);
~OpenGLTexture() override;
inline GLuint handle() const { return _handle; }
void setMirrorWrap(bool wrap);
void update(const Graphics::Surface &surface) override;
protected:
bool _withMipmaps;
bool _mirrorWrap = true;
GLuint _handle = 0;
};
class OpenGLRenderer : public OpenGLRendererBase {
public:
OpenGLRenderer(Common::Point resolution);
Common::ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override;
Graphics::PixelFormat getPixelFormat() const override;
bool requiresPoTTextures() const override;
void end() override;
void setOutput(Graphics::Surface &output) override;
protected:
void setViewportInner(int x, int y, int width, int height) override;
void setBlendFunc(BlendMode blendMode); ///< just the blend-func, not texenv/shader uniform
void checkFirstDrawCommand();
OpenGLTexture *_currentTexture = nullptr;
};
}
#endif // ALCACHOFA_GRAPHICS_OPENGL_H

View File

@@ -0,0 +1,324 @@
/* 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 "alcachofa/alcachofa.h"
#include "alcachofa/graphics.h"
#include "alcachofa/detection.h"
#include "alcachofa/graphics-opengl-base.h"
#include "common/system.h"
#include "common/config-manager.h"
#include "engines/util.h"
#include "graphics/tinygl/tinygl.h"
using namespace Common;
using namespace Math;
using namespace Graphics;
namespace Alcachofa {
class TinyGLTexture : public ITexture {
public:
TinyGLTexture(int32 w, int32 h, bool withMipmaps)
: ITexture({ (int16)w, (int16)h })
, _withMipmaps(withMipmaps) {
tglEnable(TGL_TEXTURE_2D);
tglGenTextures(1, &_handle);
tglBindTexture(TGL_TEXTURE_2D, _handle);
tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_MIN_FILTER, TGL_LINEAR_MIPMAP_LINEAR);
tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_MAG_FILTER, TGL_LINEAR);
tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_WRAP_S, TGL_MIRRORED_REPEAT);
tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_WRAP_T, TGL_MIRRORED_REPEAT);
}
~TinyGLTexture() override {
if (_handle != 0)
tglDeleteTextures(1, &_handle);
}
inline TGLuint handle() const { return _handle; }
void update(const Surface &surface) override {
assert(surface.format == g_engine->renderer().getPixelFormat());
assert(surface.w == size().x && surface.h == size().y);
const void *pixels = surface.getPixels();
tglEnable(TGL_TEXTURE_2D);
tglBindTexture(TGL_TEXTURE_2D, _handle);
tglTexImage2D(TGL_TEXTURE_2D, 0, TGL_RGBA, size().x, size().y, 0, TGL_RGBA, TGL_UNSIGNED_BYTE, pixels);
if (_withMipmaps && false)
warning("NO TINYGL MIPMAPS IMPLEMENTED YET");
else
tglTexParameteri(TGL_TEXTURE_2D, TGL_TEXTURE_MAX_LEVEL, 0);
}
protected:
bool _withMipmaps;
TGLuint _handle;
};
class TinyGLRenderer : public OpenGLRendererBase {
public:
TinyGLRenderer(Point resolution) : OpenGLRendererBase(resolution) {
if (g_engine->config().bits32())
initGraphics(resolution.x, resolution.y, nullptr);
else {
// we try to take some 16-bit format and fallback
auto formats = g_system->getSupportedFormats();
for (auto it = formats.begin(); it != formats.end();) {
if (it->bytesPerPixel == 2)
++it;
else
it = formats.erase(it);
}
if (formats.empty())
initGraphics(resolution.x, resolution.y, nullptr);
else
initGraphics(resolution.x, resolution.y, formats);
}
debug("Using framebuffer format: %s", g_system->getScreenFormat().toString().c_str());
if (g_system->getScreenFormat().bytesPerPixel < 2)
error("Alcachofa needs at least 16bit colors");
_context = TinyGL::createContext(
resolution.x, resolution.y,
g_system->getScreenFormat(),
1024, // some background images are even larger than this
false, false);
TinyGL::setContext(_context);
tglDisable(TGL_DEPTH_TEST);
tglDisable(TGL_SCISSOR_TEST);
tglDisable(TGL_STENCIL_TEST);
tglDisable(TGL_CULL_FACE);
tglEnable(TGL_BLEND);
tglDepthMask(TGL_FALSE);
}
~TinyGLRenderer() override {
if (_context != nullptr)
TinyGL::destroyContext(_context);
}
ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps) override {
assert(w >= 0 && h >= 0);
return ScopedPtr<ITexture>(new TinyGLTexture(w, h, withMipmaps));
}
PixelFormat getPixelFormat() const override {
return PixelFormat::createFormatRGBA32();
}
bool requiresPoTTextures() const override {
return false;
}
void begin() override {
resetState();
_currentTexture = nullptr;
}
void setTexture(ITexture *texture) override {
if (texture == _currentTexture)
return;
else if (texture == nullptr) {
tglDisable(TGL_TEXTURE_2D);
_currentTexture = nullptr;
} else {
if (_currentTexture == nullptr) {
tglEnable(TGL_TEXTURE_2D);
}
auto glTexture = dynamic_cast<TinyGLTexture *>(texture);
assert(glTexture != nullptr);
tglBindTexture(TGL_TEXTURE_2D, glTexture->handle()); //-V522
_currentTexture = glTexture;
}
}
void setBlendMode(BlendMode blendMode) override {
if (blendMode == _currentBlendMode)
return;
switch (blendMode) {
case BlendMode::AdditiveAlpha:
case BlendMode::Tinted:
tglBlendFunc(TGL_ONE, TGL_ONE_MINUS_SRC_ALPHA);
break;
case BlendMode::Additive:
tglBlendFunc(TGL_ONE, TGL_ONE);
break;
case BlendMode::Multiply:
tglBlendFunc(TGL_DST_COLOR, TGL_ONE);
break;
case BlendMode::Alpha:
tglBlendFunc(TGL_SRC_ALPHA, TGL_ONE_MINUS_SRC_ALPHA);
break;
default: assert(false && "Invalid blend mode"); break;
}
tglTexEnvi(TGL_TEXTURE_ENV, TGL_TEXTURE_ENV_MODE, TGL_COMBINE);
switch (blendMode) {
case BlendMode::AdditiveAlpha:
case BlendMode::Additive:
case BlendMode::Multiply:
// TintAlpha * TexColor, TexAlpha
tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_RGB, TGL_MODULATE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_ALPHA, TGL_REPLACE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE0_RGB, TGL_TEXTURE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND0_RGB, TGL_SRC_COLOR);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE0_ALPHA, TGL_TEXTURE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND0_ALPHA, TGL_SRC_ALPHA);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE1_RGB, TGL_PRIMARY_COLOR);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND1_RGB, TGL_SRC_ALPHA); // alpha replaces color
break;
case BlendMode::Alpha:
// TexColor, TintAlpha
tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_RGB, TGL_REPLACE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_ALPHA, TGL_REPLACE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE0_RGB, TGL_TEXTURE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND0_RGB, TGL_SRC_COLOR);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE0_ALPHA, TGL_PRIMARY_COLOR);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND0_ALPHA, TGL_SRC_ALPHA);
break;
case BlendMode::Tinted:
// (TintColor * TintAlpha) * TexColor, TexAlpha
tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_RGB, TGL_MODULATE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_COMBINE_ALPHA, TGL_REPLACE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE0_RGB, TGL_TEXTURE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND0_RGB, TGL_SRC_COLOR);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE0_ALPHA, TGL_TEXTURE);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND0_ALPHA, TGL_SRC_ALPHA);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_SOURCE1_RGB, TGL_PRIMARY_COLOR);
tglTexEnvi(TGL_TEXTURE_ENV, TGL_OPERAND1_RGB, TGL_SRC_COLOR); // we have to pre-multiply
break;
default:
assert(false && "Invalid blend mode");
break;
}
_currentBlendMode = blendMode;
}
void setLodBias(float lodBias) override {
_currentLodBias = lodBias;
// TinyGL does not support lod bias
}
void setOutput(Surface &output) override {
assert(_isFirstDrawCommand);
setViewportToRect(output.w, output.h);
_currentOutput = &output;
if (output.w > _resolution.x || output.h > _resolution.y)
debugC(0, kDebugGraphics, "Output is larger than screen, output will be cropped (%d, %d) > (%d, %d)",
output.w, output.h, _resolution.x, _resolution.y);
// no need to check format for TinyGL, we need to be prepared for conversion anyways
}
void end() override {
tglFlush();
TinyGL::presentBuffer();
Surface framebuffer;
TinyGL::getSurfaceRef(framebuffer);
if (_currentOutput == nullptr) {
g_system->copyRectToScreen(framebuffer.getPixels(), framebuffer.pitch, 0, 0, framebuffer.w, framebuffer.h);
g_system->updateScreen();
} else {
framebuffer = framebuffer.getSubArea(Rect(0, 0, _currentOutput->w, _currentOutput->h));
crossBlit(
(byte *)_currentOutput->getPixels(),
(const byte *)framebuffer.getPixels(),
_currentOutput->pitch,
framebuffer.pitch,
framebuffer.w,
framebuffer.h,
_currentOutput->format,
framebuffer.format);
}
}
void quad(
Vector2d topLeft,
Vector2d size,
Color color,
Angle rotation,
Vector2d texMin,
Vector2d texMax) override {
Vector2d positions[4], texCoords[4];
getQuadPositions(topLeft, size, rotation, positions);
getQuadTexCoords(texMin, texMax, texCoords);
float colors[] = { color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, color.a / 255.0f };
if (_currentBlendMode == BlendMode::Tinted) {
colors[0] *= colors[3];
colors[1] *= colors[3];
colors[2] *= colors[3];
}
if (_isFirstDrawCommand) {
_isFirstDrawCommand = false;
tglClearColor(0, 0, 0, 0);
tglClear(TGL_COLOR_BUFFER_BIT);
}
tglColor4fv(colors);
tglBegin(TGL_QUADS);
for (int i = 0; i < 4; i++) {
tglTexCoord2f(texCoords[i].getX(), texCoords[i].getY());
tglVertex2f(positions[i].getX(), positions[i].getY());
}
tglEnd();
TinyGL::presentBuffer();
}
protected:
void setViewportInner(int x, int y, int width, int height) override {
tglViewport(x, y, width, height);
}
void setMatrices(bool flipped) override {
float bottom = flipped ? _resolution.y : 0.0f;
float top = flipped ? 0.0f : _resolution.y;
tglMatrixMode(TGL_PROJECTION);
tglLoadIdentity();
tglOrtho(0.0f, _resolution.x, bottom, top, -1.0f, 1.0f);
tglMatrixMode(TGL_MODELVIEW);
tglLoadIdentity();
}
private:
TinyGL::ContextHandle *_context = nullptr;
TinyGLTexture *_currentTexture = nullptr;
};
IRenderer *IRenderer::createTinyGLRenderer(Point resolution) {
debug("Use TinyGL renderer");
return new TinyGLRenderer(resolution);
}
}

View File

@@ -0,0 +1,999 @@
/* 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 "alcachofa/graphics.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/shape.h"
#include "alcachofa/global-ui.h"
#include "common/system.h"
#include "common/file.h"
#include "common/substream.h"
#include "common/bufferedstream.h"
#include "image/tga.h"
using namespace Common;
using namespace Math;
using namespace Image;
using namespace Graphics;
namespace Alcachofa {
ITexture::ITexture(Point size) : _size(size) {
if ((!isPowerOfTwo(size.x) || !isPowerOfTwo(size.y)) &&
g_engine->renderer().requiresPoTTextures())
warning("Created unsupported NPOT texture (%dx%d)", size.x, size.y);
}
void IDebugRenderer::debugShape(const Shape &shape, Color color) {
constexpr uint kMaxPoints = 16;
Vector2d points2d[kMaxPoints];
for (auto polygon : shape) {
// I don't think this will happen but let's be sure
assert(polygon._points.size() <= kMaxPoints);
for (uint i = 0; i < polygon._points.size(); i++) {
const auto p3d = polygon._points[i];
const auto p2d = g_engine->camera().transform3Dto2D(Vector3d(p3d.x, p3d.y, kBaseScale));
points2d[i] = Vector2d(p2d.x(), p2d.y());
}
debugPolygon({ points2d, polygon._points.size() }, color);
}
}
AnimationBase::AnimationBase(String fileName, AnimationFolder folder)
: _fileName(reencode(fileName))
, _folder(folder) {}
AnimationBase::~AnimationBase() {
freeImages();
}
void AnimationBase::load() {
if (_isLoaded)
return;
String fullPath;
switch (_folder) {
case AnimationFolder::Animations:
fullPath = "Animaciones/";
break;
case AnimationFolder::Masks:
fullPath = "Mascaras/";
break;
case AnimationFolder::Backgrounds:
fullPath = "Fondos/";
break;
default:
assert(false && "Invalid AnimationFolder");
break;
}
if (_fileName.size() < 4 || scumm_strnicmp(_fileName.end() - 4, ".AN0", 4) != 0)
_fileName += ".AN0";
fullPath += _fileName;
File file;
if (!file.open(fullPath.c_str())) {
// original fallback
fullPath = "Mascaras/" + _fileName;
if (!file.open(fullPath.c_str())) {
loadMissingAnimation();
return;
}
}
// Reading the images is a major bottleneck in loading, buffering helps a lot with that
ScopedPtr<SeekableReadStream> stream(wrapBufferedSeekableReadStream(&file, file.size(), DisposeAfterUse::NO));
uint spriteCount = stream->readUint32LE();
assert(spriteCount < kMaxSpriteIDs);
_spriteBases.reserve(spriteCount);
uint imageCount = stream->readUint32LE();
_images.reserve(imageCount);
_imageOffsets.reserve(imageCount);
for (uint i = 0; i < imageCount; i++) {
_images.push_back(readImage(*stream));
}
// an inconsistency, maybe a historical reason:
// the sprite bases are also stored as fixed 256 array, but as sprite *indices*
// have to be contiguous we do not need to do that ourselves.
// but let's check in Debug to be sure
for (uint i = 0; i < spriteCount; i++) {
_spriteBases.push_back(stream->readUint32LE());
assert(_spriteBases.back() < imageCount);
}
#ifdef ALCACHOFA_DEBUG
for (uint i = spriteCount; i < kMaxSpriteIDs; i++)
assert(stream->readSint32LE() == 0);
#else
stream->skip(sizeof(int32) * (kMaxSpriteIDs - spriteCount));
#endif
for (uint i = 0; i < imageCount; i++)
_imageOffsets.push_back(readPoint(*stream));
for (uint i = 0; i < kMaxSpriteIDs; i++)
_spriteIndexMapping[i] = stream->readSint32LE();
uint frameCount = stream->readUint32LE();
_frames.reserve(frameCount);
_spriteOffsets.reserve(frameCount * spriteCount);
_totalDuration = 0;
for (uint i = 0; i < frameCount; i++) {
for (uint j = 0; j < spriteCount; j++)
_spriteOffsets.push_back(stream->readUint32LE());
AnimationFrame frame;
frame._center = readPoint(*stream);
frame._offset = readPoint(*stream);
frame._duration = stream->readUint32LE();
_frames.push_back(frame);
_totalDuration += frame._duration;
}
_isLoaded = true;
}
void AnimationBase::freeImages() {
if (!_isLoaded)
return;
for (auto *image : _images) {
if (image != nullptr)
delete image;
}
_images.clear();
_spriteOffsets.clear();
_spriteBases.clear();
_frames.clear();
_imageOffsets.clear();
_isLoaded = false;
}
ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const {
SeekableSubReadStream subStream(&stream, stream.pos(), stream.size());
TGADecoder decoder;
if (!decoder.loadStream(subStream))
error("Failed to load TGA from animation %s", _fileName.c_str());
// The length of the image is unknown but TGADecoder does not read
// the end marker, so let's search for it.
static const char *kExpectedMarker = "TRUEVISION-XFILE.";
static const uint kMarkerLength = 18;
char buffer[kMarkerLength] = { 0 };
char *potentialStart = buffer + kMarkerLength;
do {
uint nextRead = potentialStart - buffer;
if (potentialStart < buffer + kMarkerLength)
memmove(buffer, potentialStart, kMarkerLength - nextRead);
if (stream.read(buffer + kMarkerLength - nextRead, nextRead) != nextRead)
error("Unexpected end-of-file in animation %s", _fileName.c_str());
potentialStart = find(buffer + 1, buffer + kMarkerLength, kExpectedMarker[0]);
} while (strncmp(buffer, kExpectedMarker, kMarkerLength) != 0);
// instead of not storing unused frame images the animation contains
// transparent 2x1 images. Let's just ignore them.
auto source = decoder.getSurface();
if (source->w == 2 && source->h == 1)
return nullptr;
const auto &palette = decoder.getPalette();
auto target = new ManagedSurface();
target->setPalette(palette.data(), 0, palette.size());
target->convertFrom(*source, g_engine->renderer().getPixelFormat());
return target;
}
void AnimationBase::loadMissingAnimation() {
// only allow missing animations we know are faulty in the original game
g_engine->game().missingAnimation(_fileName);
// otherwise setup a functioning but empty animation
_isLoaded = true;
_totalDuration = 1;
_spriteIndexMapping[0] = 0;
_spriteOffsets.push_back(1);
_spriteBases.push_back(0);
_images.push_back(nullptr);
_imageOffsets.push_back(Point());
_frames.push_back({ Point(), Point(), 1 });
}
// unfortunately ScummVMs BLEND_NORMAL does not blend alpha
// but this also bad, let's find/discuss a better solution later
void AnimationBase::fullBlend(const ManagedSurface &source, ManagedSurface &destination, int offsetX, int offsetY) {
// TODO: Support other pixel formats
assert(source.format == Graphics::PixelFormat::createFormatRGBA32() ||
source.format == Graphics::PixelFormat::createFormatBGRA32());
assert(destination.format == source.format);
assert(offsetX >= 0 && offsetX + source.w <= destination.w);
assert(offsetY >= 0 && offsetY + source.h <= destination.h);
const byte *sourceLine = (const byte *)source.getPixels();
byte *destinationLine = (byte *)destination.getPixels() + offsetY * destination.pitch + offsetX * 4;
for (int y = 0; y < source.h; y++) {
const byte *sourcePixel = sourceLine;
byte *destPixel = destinationLine;
for (int x = 0; x < source.w; x++) {
byte alpha = sourcePixel[3];
for (int i = 0; i < 3; i++)
destPixel[i] = ((byte)(alpha * sourcePixel[i] / 255)) + ((byte)((255 - alpha) * destPixel[i] / 255));
destPixel[3] = alpha + ((byte)((255 - alpha) * destPixel[3] / 255));
sourcePixel += 4;
destPixel += 4;
}
sourceLine += source.pitch;
destinationLine += destination.pitch;
}
}
Point AnimationBase::imageSize(int32 imageI) const {
auto image = _images[imageI];
return image == nullptr ? Point() : Point(image->w, image->h);
}
Animation::Animation(String fileName, AnimationFolder folder)
: AnimationBase(fileName, folder) {}
void Animation::load() {
if (_isLoaded)
return;
AnimationBase::load();
Rect maxBounds = maxFrameBounds();
int16 texWidth = maxBounds.width(), texHeight = maxBounds.height();
if (g_engine->renderer().requiresPoTTextures()) {
texWidth = nextHigher2(maxBounds.width());
texHeight = nextHigher2(maxBounds.height());
}
_renderedSurface.create(texWidth, texHeight, g_engine->renderer().getPixelFormat());
_renderedTexture = g_engine->renderer().createTexture(texWidth, texHeight, true);
// We always create mipmaps, even for the backgrounds that usually do not scale much,
// the exception to this is the thumbnails for the savestates.
// If we need to reduce graphics memory usage in the future, we can change it right here
}
void Animation::freeImages() {
if (!_isLoaded)
return;
AnimationBase::freeImages();
_renderedSurface.free();
_renderedTexture.reset(nullptr);
_renderedFrameI = -1;
_premultiplyAlpha = 100;
}
int32 Animation::imageIndex(int32 frameI, int32 spriteId) const {
assert(frameI >= 0 && (uint)frameI < frameCount());
assert(spriteId >= 0 && (uint)spriteId < spriteCount());
int32 spriteIndex = _spriteIndexMapping[spriteId];
int32 offset = _spriteOffsets[frameI * spriteCount() + spriteIndex];
return offset <= 0 ? -1
: offset + _spriteBases[spriteIndex] - 1;
}
Rect Animation::spriteBounds(int32 frameI, int32 spriteId) const {
int32 imageI = imageIndex(frameI, spriteId);
auto image = imageI < 0 ? nullptr : _images[imageI];
return image == nullptr
? Rect(imageI < 0 ? Point() : _imageOffsets[imageI], 2, 1)
: Rect(_imageOffsets[imageI], image->w, image->h);
}
Rect Animation::frameBounds(int32 frameI) const {
if (spriteCount() == 0)
return Rect();
Rect bounds = spriteBounds(frameI, 0);
for (uint spriteI = 1; spriteI < spriteCount(); spriteI++)
bounds.extend(spriteBounds(frameI, spriteI));
return bounds;
}
Rect Animation::maxFrameBounds() const {
if (frameCount() == 0)
return Rect();
Rect bounds = frameBounds(0);
for (uint frameI = 1; frameI < frameCount(); frameI++)
bounds.extend(frameBounds(frameI));
return bounds;
}
Point Animation::totalFrameOffset(int32 frameI) const {
const auto &frame = _frames[frameI];
const auto bounds = frameBounds(frameI);
return Point(
bounds.left - frame._center.x + frame._offset.x,
bounds.top - frame._center.y + frame._offset.y);
}
int32 Animation::frameAtTime(uint32 time) const {
for (int32 i = 0; (uint)i < _frames.size(); i++) {
if (time <= _frames[i]._duration)
return i;
time -= _frames[i]._duration;
}
return -1;
}
void Animation::overrideTexture(const ManagedSurface &surface) {
int16 texWidth = surface.w, texHeight = surface.h;
if (g_engine->renderer().requiresPoTTextures()) {
texWidth = nextHigher2(texWidth);
texHeight = nextHigher2(texHeight);
}
// In order to really use the overridden surface we have to override all
// values used for calculating the output size
_renderedFrameI = 0;
_renderedPremultiplyAlpha = _premultiplyAlpha;
_renderedSurface.free();
_renderedSurface.w = texWidth;
_renderedSurface.h = texHeight;
_images[0]->free();
_images[0]->w = surface.w;
_images[0]->h = surface.h;
if (_renderedTexture->size() != Point(texWidth, texHeight)) {
_renderedTexture = Common::move(
g_engine->renderer().createTexture(texWidth, texHeight, false));
}
if (surface.w == texWidth && surface.h == texHeight)
_renderedTexture->update(surface);
else {
ManagedSurface tmpSurface(texWidth, texHeight, g_engine->renderer().getPixelFormat());
tmpSurface.blitFrom(surface);
_renderedTexture->update(tmpSurface);
}
}
void Animation::prerenderFrame(int32 frameI) {
assert(frameI >= 0 && (uint)frameI < frameCount());
if (frameI == _renderedFrameI && _renderedPremultiplyAlpha == _premultiplyAlpha)
return;
auto bounds = frameBounds(frameI);
_renderedSurface.clear();
for (uint spriteI = 0; spriteI < spriteCount(); spriteI++) {
int32 imageI = imageIndex(frameI, spriteI);
auto image = imageI < 0 ? nullptr : _images[imageI];
if (image == nullptr)
continue;
int offsetX = _imageOffsets[imageI].x - bounds.left;
int offsetY = _imageOffsets[imageI].y - bounds.top;
fullBlend(*image, _renderedSurface, offsetX, offsetY);
}
// Here was some alpha premultiplication, but it only produces bugs so is ignored
_renderedTexture->update(_renderedSurface);
_renderedFrameI = frameI;
_renderedPremultiplyAlpha = _premultiplyAlpha;
}
void Animation::outputRect2D(int32 frameI, float scale, Vector2d &topLeft, Vector2d &size) const {
auto bounds = frameBounds(frameI);
topLeft += as2D(totalFrameOffset(frameI)) * scale;
size = Vector2d(bounds.width(), bounds.height()) * scale;
}
void Animation::draw2D(int32 frameI, Vector2d topLeft, float scale, BlendMode blendMode, Color color) {
prerenderFrame(frameI);
auto bounds = frameBounds(frameI);
Vector2d texMin(0, 0);
Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
Vector2d size;
outputRect2D(frameI, scale, topLeft, size);
auto &renderer = g_engine->renderer();
renderer.setTexture(_renderedTexture.get());
renderer.setBlendMode(blendMode);
renderer.quad(topLeft, size, color, Angle(), texMin, texMax);
}
void Animation::outputRect3D(int32 frameI, float scale, Vector3d &topLeft, Vector2d &size) const {
auto bounds = frameBounds(frameI);
topLeft += as3D(totalFrameOffset(frameI)) * scale;
topLeft = g_engine->camera().transform3Dto2D(topLeft);
size = Vector2d(bounds.width(), bounds.height()) * scale * topLeft.z();
}
void Animation::draw3D(int32 frameI, Vector3d topLeft, float scale, BlendMode blendMode, Color color) {
prerenderFrame(frameI);
auto bounds = frameBounds(frameI);
Vector2d texMin(0, 0);
Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
Vector2d size;
outputRect3D(frameI, scale, topLeft, size);
const auto rotation = -g_engine->camera().rotation();
auto &renderer = g_engine->renderer();
renderer.setTexture(_renderedTexture.get());
renderer.setBlendMode(blendMode);
renderer.quad(as2D(topLeft), size, color, rotation, texMin, texMax);
}
void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d size, Vector2d texOffset, BlendMode blendMode) {
prerenderFrame(frameI);
auto bounds = frameBounds(frameI);
Vector2d texMin(0, 0);
Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h);
topLeft += as3D(totalFrameOffset(frameI));
topLeft = g_engine->camera().transform3Dto2D(topLeft);
const auto rotation = -g_engine->camera().rotation();
size(0, 0) *= bounds.width() * topLeft.z() / _renderedSurface.w;
size(1, 0) *= bounds.height() * topLeft.z() / _renderedSurface.h;
auto &renderer = g_engine->renderer();
renderer.setTexture(_renderedTexture.get());
renderer.setBlendMode(blendMode);
renderer.quad(as2D(topLeft), size, kWhite, rotation, texMin + texOffset, texMax + texOffset);
}
Font::Font(String fileName) : AnimationBase(fileName) {}
void Font::load() {
if (_isLoaded)
return;
AnimationBase::load();
// We now render all frames into a 16x16 atlas and fill up to power of two size just because it is easy here
// However in two out of three fonts the character 128 is massive, it looks like a bug
// as we want easy regular-sized characters it is ignored
Point cellSize;
for (auto image : _images) {
assert(image != nullptr); // no fake pictures in fonts please
if (image == _images[128])
continue;
cellSize.x = MAX(cellSize.x, image->w);
cellSize.y = MAX(cellSize.y, image->h);
}
_texMins.resize(_images.size());
_texMaxs.resize(_images.size());
ManagedSurface atlasSurface(nextHigher2(cellSize.x * 16), nextHigher2(cellSize.y * 16), g_engine->renderer().getPixelFormat());
cellSize.x = atlasSurface.w / 16;
cellSize.y = atlasSurface.h / 16;
const float invWidth = 1.0f / atlasSurface.w;
const float invHeight = 1.0f / atlasSurface.h;
for (uint i = 0; i < _images.size(); i++) {
if (i == 128) continue;
int offsetX = (i % 16) * cellSize.x + (cellSize.x - _images[i]->w) / 2;
int offsetY = (i / 16) * cellSize.y + (cellSize.y - _images[i]->h) / 2;
fullBlend(*_images[i], atlasSurface, offsetX, offsetY);
_texMins[i].setX(offsetX * invWidth);
_texMins[i].setY(offsetY * invHeight);
_texMaxs[i].setX((offsetX + _images[i]->w) * invWidth);
_texMaxs[i].setY((offsetY + _images[i]->h) * invHeight);
}
_texture = g_engine->renderer().createTexture(atlasSurface.w, atlasSurface.h, false);
_texture->update(atlasSurface);
debugCN(1, kDebugGraphics, "Rendered font atlas %s at %dx%d", _fileName.c_str(), atlasSurface.w, atlasSurface.h);
}
void Font::freeImages() {
if (!_isLoaded)
return;
AnimationBase::freeImages();
_texture.reset();
_texMins.clear();
_texMaxs.clear();
}
void Font::drawCharacter(int32 imageI, Point centerPoint, Color color) {
assert(imageI >= 0 && (uint)imageI < _images.size());
Vector2d center = as2D(centerPoint + _imageOffsets[imageI]);
Vector2d size(_images[imageI]->w, _images[imageI]->h);
auto &renderer = g_engine->renderer();
renderer.setTexture(_texture.get());
renderer.setBlendMode(BlendMode::Tinted);
renderer.quad(center, size, color, Angle(), _texMins[imageI], _texMaxs[imageI]);
}
Graphic::Graphic() {}
Graphic::Graphic(ReadStream &stream) {
_topLeft.x = stream.readSint16LE();
_topLeft.y = stream.readSint16LE();
_scale = stream.readSint16LE();
_order = stream.readSByte();
auto animationName = readVarString(stream);
if (!animationName.empty())
setAnimation(animationName, AnimationFolder::Animations);
}
Graphic::Graphic(const Graphic &other)
: _animation(other._animation)
, _topLeft(other._topLeft)
, _scale(other._scale)
, _order(other._order)
, _color(other._color)
, _isPaused(other._isPaused)
, _isLooping(other._isLooping)
, _lastTime(other._lastTime)
, _frameI(other._frameI)
, _depthScale(other._depthScale) {}
Graphic &Graphic::operator= (const Graphic &other) {
_ownedAnimation.reset();
_animation = other._animation;
_topLeft = other._topLeft;
_scale = other._scale;
_order = other._order;
_color = other._color;
_isPaused = other._isPaused;
_isLooping = other._isLooping;
_lastTime = other._lastTime;
_frameI = other._frameI;
_depthScale = other._depthScale;
return *this;
}
void Graphic::loadResources() {
if (_animation != nullptr)
_animation->load();
}
void Graphic::freeResources() {
if (_ownedAnimation == nullptr)
_animation = nullptr;
else {
_ownedAnimation->freeImages();
_animation = _ownedAnimation.get();
}
}
void Graphic::update() {
if (_animation == nullptr || _animation->frameCount() == 0)
return;
const uint32 totalDuration = _animation->totalDuration();
uint32 curTime = _isPaused
? _lastTime
: g_engine->getMillis() - _lastTime;
if (curTime > totalDuration) {
if (_isLooping && totalDuration > 0)
curTime %= totalDuration;
else {
pause();
curTime = totalDuration ? totalDuration - 1 : 0;
_lastTime = curTime;
}
}
_frameI = totalDuration == 0 ? 0 : _animation->frameAtTime(curTime);
assert(_frameI >= 0);
}
void Graphic::start(bool isLooping) {
_isPaused = false;
_isLooping = isLooping;
_lastTime = g_engine->getMillis();
}
void Graphic::pause() {
_isPaused = true;
_isLooping = false;
_lastTime = g_engine->getMillis() - _lastTime;
}
void Graphic::reset() {
_frameI = 0;
_lastTime = _isPaused ? 0 : g_engine->getMillis();
}
void Graphic::setAnimation(const Common::String &fileName, AnimationFolder folder) {
_ownedAnimation.reset(new Animation(fileName, folder));
_animation = _ownedAnimation.get();
}
void Graphic::setAnimation(Animation *animation) {
_animation = animation;
}
void Graphic::syncGame(Serializer &serializer) {
syncPoint(serializer, _topLeft);
serializer.syncAsSint16LE(_scale);
serializer.syncAsUint32LE(_lastTime);
serializer.syncAsByte(_isPaused);
serializer.syncAsByte(_isLooping);
serializer.syncAsFloatLE(_depthScale);
}
static int8 shiftAndClampOrder(int8 order) {
return MAX<int8>(0, MIN<int8>(kOrderCount - 1, order + kForegroundOrderCount));
}
IDrawRequest::IDrawRequest(int8 order)
: _order(shiftAndClampOrder(order)) {}
AnimationDrawRequest::AnimationDrawRequest(Graphic &graphic, bool is3D, BlendMode blendMode, float lodBias)
: IDrawRequest(graphic._order)
, _is3D(is3D)
, _animation(&graphic.animation())
, _frameI(graphic._frameI)
, _topLeft(graphic._topLeft.x, graphic._topLeft.y, graphic._scale)
, _scale(graphic._scale * graphic._depthScale)
, _color(graphic.color())
, _blendMode(blendMode)
, _lodBias(lodBias) {
assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount());
}
AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, Vector2d center, int8 order)
: IDrawRequest(order)
, _is3D(false)
, _animation(animation)
, _frameI(frameI)
, _topLeft(as3D(center))
, _scale(kBaseScale)
, _color(kWhite)
, _blendMode(BlendMode::AdditiveAlpha)
, _lodBias(0.0f) {
assert(animation != nullptr && animation->isLoaded());
assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount());
}
void AnimationDrawRequest::draw() {
g_engine->renderer().setLodBias(_lodBias);
if (_is3D)
_animation->draw3D(_frameI, _topLeft, _scale * kInvBaseScale, _blendMode, _color);
else
_animation->draw2D(_frameI, as2D(_topLeft), _scale * kInvBaseScale, _blendMode, _color);
}
SpecialEffectDrawRequest::SpecialEffectDrawRequest(Graphic &graphic, Point topLeft, Point bottomRight, Vector2d texOffset, BlendMode blendMode)
: IDrawRequest(graphic._order)
, _animation(&graphic.animation())
, _frameI(graphic._frameI)
, _topLeft(topLeft.x, topLeft.y, graphic._scale)
, _size(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
, _texOffset(texOffset)
, _blendMode(blendMode) {
assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount());
}
void SpecialEffectDrawRequest::draw() {
_animation->drawEffect(_frameI, _topLeft, _size, _texOffset, _blendMode);
}
static const byte *trimLeading(const byte *text, const byte *end) {
while (*text && text < end && *text <= ' ')
text++;
return text;
}
static const byte *trimTrailing(const byte *text, const byte *begin, bool trimSpaces) {
while (text != begin && (*text <= ' ') == trimSpaces)
text--;
return text;
}
static Point characterSize(const Font &font, byte ch) {
if (ch <= ' ' || (uint)(ch - ' ') >= font.imageCount())
ch = 0;
else
ch -= ' ';
return font.imageSize(ch);
}
TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos, int maxWidth, bool centered, Color color, int8 order)
: IDrawRequest(order)
, _font(font)
, _color(color) {
const int screenW = g_system->getWidth();
const int screenH = g_system->getHeight();
if (maxWidth < 0)
maxWidth = screenW;
// allocate on drawQueue to prevent having destruct it
assert(originalText != nullptr);
auto textLen = strlen(originalText);
char *text = (char *)g_engine->drawQueue().allocator().allocateRaw(textLen + 1, 1);
memcpy(text, originalText, textLen + 1);
// split into trimmed lines
uint lineCount = 0;
const byte *itChar = (byte *)text, *itLine = (byte *)text, *textEnd = itChar + textLen + 1;
int lineWidth = 0;
while (true) {
if (lineCount >= kMaxLines) {
g_engine->game().tooManyDialogLines(lineCount, kMaxLines);
break;
}
if (*itChar != '\r' && *itChar)
lineWidth += characterSize(font, *itChar).x;
if (lineWidth <= maxWidth && *itChar != '\r' && *itChar) {
itChar++;
continue;
}
// now we are in new-line territory
if (*itChar > ' ')
itChar = trimTrailing(itChar, itLine, false); // trim last word
if (centered) {
itChar = trimTrailing(itChar, itLine, true) + 1;
itLine = trimLeading(itLine, itChar);
_allLines[lineCount] = TextLine(itLine, itChar - itLine);
} else
_allLines[lineCount] = TextLine(itLine, itChar - itLine);
itChar = trimLeading(itChar, textEnd);
lineCount++;
lineWidth = 0;
itLine = itChar;
if (!*itChar)
break;
}
_lines = Span<TextLine>(_allLines, lineCount);
_posX = Span<int>(_allPosX, lineCount);
// calc line widths and max line width
_width = 0;
for (uint i = 0; i < lineCount; i++) {
lineWidth = 0;
for (auto ch : _lines[i]) {
if (ch != '\r' && ch)
lineWidth += characterSize(font, ch).x;
}
_posX[i] = lineWidth;
_width = MAX(_width, lineWidth);
}
// setup line positions
if (centered) {
if (pos.x - _width / 2 < 0)
pos.x = _width / 2 + 1;
if (pos.x + _width / 2 >= screenW)
pos.x = screenW - _width / 2 - 1;
for (auto &linePosX : _posX)
linePosX = pos.x - linePosX / 2;
} else
fill(_posX.begin(), _posX.end(), pos.x);
// setup height and y position
_height = (int)lineCount * (font.imageSize(0).y * 4 / 3);
_posY = pos.y;
if (centered)
_posY -= _height / 2;
if (_posY < 0)
_posY = 0;
if (_posY + _height >= screenH)
_posY = screenH - _height;
}
void TextDrawRequest::draw() {
const Point spaceSize = _font.imageSize(0);
Point cursor(0, _posY);
for (uint i = 0; i < _lines.size(); i++) {
cursor.x = _posX[i];
for (auto ch : _lines[i]) {
const Point charSize = characterSize(_font, ch);
if (ch > ' ' && (uint)(ch - ' ') < _font.imageCount())
_font.drawCharacter(ch - ' ', Point(cursor.x, cursor.y), _color);
cursor.x += charSize.x;
}
cursor.y += spaceSize.y * 4 / 3;
}
}
FadeDrawRequest::FadeDrawRequest(FadeType type, float value, int8 order)
: IDrawRequest(order)
, _type(type)
, _value(value) {}
void FadeDrawRequest::draw() {
Color color;
const byte valueAsByte = (byte)(_value * 255);
switch (_type) {
case FadeType::ToBlack:
color = { 0, 0, 0, valueAsByte };
g_engine->renderer().setBlendMode(BlendMode::AdditiveAlpha);
break;
case FadeType::ToWhite:
color = { valueAsByte, valueAsByte, valueAsByte, valueAsByte };
g_engine->renderer().setBlendMode(BlendMode::Additive);
break;
default:
g_engine->game().unknownFadeType((int)_type);
return;
}
g_engine->renderer().setTexture(nullptr);
g_engine->renderer().quad(Vector2d(0, 0), as2D(Point(g_system->getWidth(), g_system->getHeight())), color);
}
struct FadeTask final : public Task {
FadeTask(Process &process, FadeType fadeType,
float from, float to,
uint32 duration, EasingType easingType,
int8 order,
PermanentFadeAction permanentFadeAction)
: Task(process)
, _fadeType(fadeType)
, _from(from)
, _to(to)
, _duration(duration)
, _easingType(easingType)
, _order(order)
, _permanentFadeAction(permanentFadeAction) {}
FadeTask(Process &process, Serializer &s)
: Task(process) {
FadeTask::syncGame(s);
}
TaskReturn run() override {
TASK_BEGIN;
if (_permanentFadeAction == PermanentFadeAction::UnsetFaded)
g_engine->globalUI().isPermanentFaded() = false;
_startTime = g_engine->getMillis();
while (g_engine->getMillis() - _startTime < _duration) {
draw((g_engine->getMillis() - _startTime) / (float)_duration);
TASK_YIELD(1);
}
draw(1.0f); // so that during a loading lag the screen is completly black/white
if (_permanentFadeAction == PermanentFadeAction::SetFaded)
g_engine->globalUI().isPermanentFaded() = true;
TASK_END;
}
void debugPrint() override {
uint32 remaining = g_engine->getMillis() - _startTime <= _duration
? _duration - (g_engine->getMillis() - _startTime)
: 0;
g_engine->console().debugPrintf("Fade (%d) from %.2f to %.2f with %ums remaining\n", (int)_fadeType, _from, _to, remaining);
}
void syncGame(Serializer &s) override {
Task::syncGame(s);
syncEnum(s, _fadeType);
syncEnum(s, _easingType);
syncEnum(s, _permanentFadeAction);
s.syncAsFloatLE(_from);
s.syncAsFloatLE(_to);
s.syncAsUint32LE(_startTime);
s.syncAsUint32LE(_duration);
s.syncAsSByte(_order);
}
const char *taskName() const override;
private:
void draw(float t) {
g_engine->drawQueue().add<FadeDrawRequest>(_fadeType, _from + (_to - _from) * ease(t, _easingType), _order);
}
FadeType _fadeType = {};
float _from = 0, _to = 0;
uint32 _startTime = 0, _duration = 0;
EasingType _easingType = {};
int8 _order = 0;
PermanentFadeAction _permanentFadeAction = {};
};
DECLARE_TASK(FadeTask)
Task *fade(Process &process, FadeType fadeType,
float from, float to,
int32 duration, EasingType easingType,
int8 order,
PermanentFadeAction permanentFadeAction) {
if (duration <= 0)
return new DelayTask(process, 0);
if (!process.isActiveForPlayer())
return new DelayTask(process, (uint32)duration);
return new FadeTask(process, fadeType, from, to, duration, easingType, order, permanentFadeAction);
}
BorderDrawRequest::BorderDrawRequest(Rect rect, Color color)
: IDrawRequest(-kForegroundOrderCount)
, _rect(rect)
, _color(color) {}
void BorderDrawRequest::draw() {
auto &renderer = g_engine->renderer();
renderer.setTexture(nullptr);
renderer.setBlendMode(BlendMode::AdditiveAlpha);
renderer.quad({ (float)_rect.left, (float)_rect.top }, { (float)_rect.width(), (float)_rect.height() }, _color);
}
DrawQueue::DrawQueue(IRenderer *renderer)
: _renderer(renderer)
, _allocator(1024) {
assert(renderer != nullptr);
}
void DrawQueue::clear() {
_allocator.deallocateAll();
memset(_requestsPerOrderCount, 0, sizeof(_requestsPerOrderCount));
memset(_lodBiasPerOrder, 0, sizeof(_lodBiasPerOrder));
}
void DrawQueue::addRequest(IDrawRequest *drawRequest) {
assert(drawRequest != nullptr && drawRequest->order() >= 0 && drawRequest->order() < kOrderCount);
auto order = drawRequest->order();
if (_requestsPerOrderCount[order] < kMaxDrawRequestsPerOrder)
_requestsPerOrder[order][_requestsPerOrderCount[order]++] = drawRequest;
else
g_engine->game().tooManyDrawRequests(order);
}
void DrawQueue::setLodBias(int8 orderFrom, int8 orderTo, float newLodBias) {
orderFrom = shiftAndClampOrder(orderFrom);
orderTo = shiftAndClampOrder(orderTo);
if (orderFrom <= orderTo) {
Common::fill(_lodBiasPerOrder + orderFrom, _lodBiasPerOrder + orderTo + 1, newLodBias);
}
}
void DrawQueue::draw() {
for (int8 order = kOrderCount - 1; order >= 0; order--) {
_renderer->setLodBias(_lodBiasPerOrder[order]);
for (uint8 requestI = 0; requestI < _requestsPerOrderCount[order]; requestI++) {
_requestsPerOrder[order][requestI]->draw();
_requestsPerOrder[order][requestI]->~IDrawRequest();
}
}
_allocator.deallocateAll();
}
BumpAllocator::BumpAllocator(size_t pageSize) : _pageSize(pageSize) {
allocatePage();
}
BumpAllocator::~BumpAllocator() {
for (auto page : _pages)
free(page);
}
void *BumpAllocator::allocateRaw(size_t size, size_t align) {
assert(size <= _pageSize);
uintptr_t page = (uintptr_t)_pages[_pageI];
uintptr_t top = page + _used;
top += align - 1;
top -= top % align;
if (page + _pageSize - top >= size) {
_used = top + size - page;
return (void *)top;
}
_used = 0;
_pageI++;
if (_pageI >= _pages.size())
allocatePage();
return allocateRaw(size, align);
}
void BumpAllocator::allocatePage() {
auto page = malloc(_pageSize);
if (page == nullptr)
error("Out of memory in BumpAllocator");
_pages.push_back(page);
}
void BumpAllocator::deallocateAll() {
_pageI = 0;
_used = 0;
}
}

View File

@@ -0,0 +1,485 @@
/* 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 ALCACHOFA_GRAPHICS_H
#define ALCACHOFA_GRAPHICS_H
#include "common/ptr.h"
#include "common/stream.h"
#include "common/serializer.h"
#include "common/rect.h"
#include "common/span.h"
#include "math/vector2d.h"
#include "graphics/managed_surface.h"
#include "alcachofa/camera.h"
#include "alcachofa/common.h"
namespace Alcachofa {
/**
* Because this gets confusing fast, here in tabular form
*
* | BlendMode | SrcColor | SrcAlpha | SrcBlend | DstBlend |
* |:-------------:|:---------------------------------|:----------|:---------|:-------------|
* | AdditiveAlpha | (1 - TintAlpha) * TexColor | TexAlpha | One | 1 - SrcAlpha |
* | Additive | (1 - TintAlpha) * TexColor | TexAlpha | One | One |
* | Multiply | (1 - TintAlpha) * TexColor | TexAlpha | DstColor | One |
* | Alpha | TexColor | TintAlpha | SrcAlpha | 1 - SrcAlpha |
* | Tinted | TintColor * TintAlpha * TexColor | TexAlpha | One | 1 - SrcAlpha |
*
*/
enum class BlendMode {
AdditiveAlpha, // Normal objects
Additive, // "Effect" objects, fades
Multiply, // Unused in Movie Adventure
Alpha, // Unused in Movie Adventure (used for debugging)
Tinted // Used for fonts
};
class Shape;
class ITexture {
public:
ITexture(Common::Point size);
virtual ~ITexture() {}
virtual void update(const Graphics::Surface &surface) = 0;
inline void update(const Graphics::ManagedSurface &surface) { update(surface.rawSurface()); }
inline Common::Point size() const { return _size; }
private:
Common::Point _size;
};
class IRenderer {
public:
virtual ~IRenderer() {}
virtual Common::ScopedPtr<ITexture> createTexture(int32 w, int32 h, bool withMipmaps = true) = 0;
virtual Graphics::PixelFormat getPixelFormat() const = 0;
virtual bool requiresPoTTextures() const = 0;
virtual void begin() = 0;
virtual void setTexture(ITexture *texture) = 0;
virtual void setBlendMode(BlendMode blendMode) = 0;
virtual void setLodBias(float lodBias) = 0;
virtual void setOutput(Graphics::Surface &surface) = 0;
virtual bool hasOutput() const = 0;
virtual void quad(
Math::Vector2d topLeft,
Math::Vector2d size,
Color color = kWhite,
Math::Angle rotation = Math::Angle(),
Math::Vector2d texMin = Math::Vector2d(0, 0),
Math::Vector2d texMax = Math::Vector2d(1, 1)) = 0;
virtual void end() = 0;
static IRenderer *createOpenGLRenderer(Common::Point resolution);
static IRenderer *createOpenGLRendererClassic(Common::Point resolution);
static IRenderer *createOpenGLRendererShaders(Common::Point resolution);
static IRenderer *createTinyGLRenderer(Common::Point resolution);
};
class IDebugRenderer : public virtual IRenderer {
public:
virtual void debugPolygon(
Common::Span<Math::Vector2d> points,
Color color = kDebugRed
) = 0;
virtual void debugPolyline(
Common::Span<Math::Vector2d> points,
Color color = kDebugRed
) = 0;
virtual void debugShape(
const Shape &shape,
Color color = kDebugRed
);
inline void debugPolyline(Common::Point a, Common::Point b, Color color = kDebugRed) {
Math::Vector2d points[] = { { (float)a.x, (float)a.y }, { (float)b.x, (float)b.y } };
debugPolygon({ points, 2 }, color);
}
};
enum class AnimationFolder {
Animations,
Masks,
Backgrounds
};
struct AnimationFrame {
Common::Point
_center, ///< the center is used for more than just drawing the animation frame
_offset; ///< the offset is only used for drawing the animation frame
uint32 _duration;
};
/**
* An animation contains one or more sprites which change their position and image during playback.
*
* Internally there is a single list of images. Every sprite ID is mapped to an index
* (via _spriteIndexMapping) which points to:
* 1. The fixed image base for that sprite
* 2. The image offset for that sprite for the current frame
* Image indices are unfortunately one-based
*
* As fonts are handled very differently they are split into a second class
*/
class AnimationBase {
protected:
AnimationBase(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
~AnimationBase();
void load();
void loadMissingAnimation();
void freeImages();
Graphics::ManagedSurface *readImage(Common::SeekableReadStream &stream) const;
Common::Point imageSize(int32 imageI) const;
inline bool isLoaded() const { return _isLoaded; }
static void fullBlend(
const Graphics::ManagedSurface &source,
Graphics::ManagedSurface &destination,
int offsetX,
int offsetY);
static constexpr const uint kMaxSpriteIDs = 256;
Common::String _fileName;
AnimationFolder _folder;
bool _isLoaded = false;
uint32 _totalDuration = 0;
int32 _spriteIndexMapping[kMaxSpriteIDs] = { 0 };
Common::Array<uint32>
_spriteOffsets, ///< index offset per sprite and animation frame
_spriteBases; ///< base index per sprite
Common::Array<AnimationFrame> _frames;
Common::Array<Graphics::ManagedSurface *> _images; ///< will contain nullptr for fake images
Common::Array<Common::Point> _imageOffsets;
};
/**
* Animations prerenders its sprites into a single texture for a set frame.
* This prerendering can be customized with a alpha to be premultiplied
*/
class Animation : private AnimationBase {
public:
Animation(Common::String fileName, AnimationFolder folder = AnimationFolder::Animations);
void load();
void freeImages();
using AnimationBase::isLoaded;
inline uint spriteCount() const { return _spriteBases.size(); }
inline uint frameCount() const { return _frames.size(); }
inline uint32 frameDuration(int32 frameI) const { return _frames[frameI]._duration; }
inline Common::Point frameCenter(int32 frameI) const { return _frames[frameI]._center; }
inline uint32 totalDuration() const { return _totalDuration; }
inline uint8 &premultiplyAlpha() { return _premultiplyAlpha; }
Common::Rect frameBounds(int32 frameI) const;
Common::Point totalFrameOffset(int32 frameI) const;
int32 frameAtTime(uint32 time) const;
int32 imageIndex(int32 frameI, int32 spriteI) const;
using AnimationBase::imageSize;
void outputRect2D(int32 frameI, float scale, Math::Vector2d &topLeft, Math::Vector2d &size) const;
void outputRect3D(int32 frameI, float scale, Math::Vector3d &topLeft, Math::Vector2d &size) const;
void overrideTexture(const Graphics::ManagedSurface &surface);
void draw2D(
int32 frameI,
Math::Vector2d topLeft,
float scale,
BlendMode blendMode,
Color color);
void draw3D(
int32 frameI,
Math::Vector3d topLeft,
float scale,
BlendMode blendMode,
Color color);
void drawEffect(
int32 frameI,
Math::Vector3d topLeft,
Math::Vector2d tiling,
Math::Vector2d texOffset,
BlendMode blendMode);
private:
Common::Rect spriteBounds(int32 frameI, int32 spriteI) const;
Common::Rect maxFrameBounds() const;
void prerenderFrame(int32 frameI);
int32_t _renderedFrameI = -1;
uint8 _premultiplyAlpha = 100, ///< in percent [0-100] not [0-255]
_renderedPremultiplyAlpha = 255;
Graphics::ManagedSurface _renderedSurface;
Common::ScopedPtr<ITexture> _renderedTexture;
};
class Font : private AnimationBase {
public:
Font(Common::String fileName);
void load();
void freeImages();
void drawCharacter(int32 imageI, Common::Point center, Color color);
using AnimationBase::isLoaded;
using AnimationBase::imageSize;
inline uint imageCount() const { return _images.size(); }
private:
Common::Array<Math::Vector2d> _texMins, _texMaxs;
Common::ScopedPtr<ITexture> _texture;
};
class Graphic {
public:
Graphic();
Graphic(Common::ReadStream &stream);
Graphic(const Graphic &other); // animation reference is taken, so keep other alive
Graphic &operator= (const Graphic &other);
inline Common::Point &topLeft() { return _topLeft; }
inline int8 &order() { return _order; }
inline int16 &scale() { return _scale; }
inline float &depthScale() { return _depthScale; }
inline Color &color() { return _color; }
inline int32 &frameI() { return _frameI; }
inline uint32 &lastTime() { return _lastTime; }
inline bool isPaused() const { return _isPaused; }
inline bool hasAnimation() const { return _animation != nullptr; }
inline Animation &animation() {
assert(_animation != nullptr && _animation->isLoaded());
return *_animation;
}
inline uint8 &premultiplyAlpha() {
assert(_animation != nullptr);
return _animation->premultiplyAlpha();
}
void loadResources();
void freeResources();
void update();
void start(bool looping);
void pause();
void reset();
void setAnimation(const Common::String &fileName, AnimationFolder folder);
void setAnimation(Animation *animation); ///< no memory ownership is given, but for prerendering it has to be mutable
void syncGame(Common::Serializer &serializer);
private:
friend class AnimationDrawRequest;
friend class SpecialEffectDrawRequest;
Common::ScopedPtr<Animation> _ownedAnimation;
Animation *_animation = nullptr;
Common::Point _topLeft;
int16 _scale = kBaseScale;
int8 _order = 0;
Color _color = kWhite;
bool _isPaused = true,
_isLooping = true;
uint32 _lastTime = 0; ///< either start time or played duration at pause
int32 _frameI = -1;
float _depthScale = 1.0f;
};
class IDrawRequest {
public:
IDrawRequest(int8 order);
virtual ~IDrawRequest() {}
inline int8 order() const { return _order; }
virtual void draw() = 0;
private:
const int8 _order;
};
class AnimationDrawRequest : public IDrawRequest {
public:
AnimationDrawRequest(
Graphic &graphic,
bool is3D,
BlendMode blendMode,
float lodBias = 0.0f);
AnimationDrawRequest(
Animation *animation,
int32 frameI,
Math::Vector2d center,
int8 order
);
void draw() override;
private:
bool _is3D;
Animation *_animation;
int32 _frameI;
Math::Vector3d _topLeft;
float _scale;
Color _color;
BlendMode _blendMode;
float _lodBias;
};
class SpecialEffectDrawRequest : public IDrawRequest {
public:
SpecialEffectDrawRequest(
Graphic &graphic,
Common::Point topLeft,
Common::Point bottomRight,
Math::Vector2d texOffset,
BlendMode blendMode);
void draw() override;
private:
Animation *_animation;
int32 _frameI;
Math::Vector3d _topLeft;
Math::Vector2d
_size,
_texOffset;
BlendMode _blendMode;
};
class TextDrawRequest : public IDrawRequest {
public:
TextDrawRequest(
Font &font,
const char *text,
Common::Point pos,
int maxWidth,
bool centered,
Color color,
int8 order);
inline Common::Point size() const { return { (int16)_width, (int16)_height }; }
void draw() override;
private:
static constexpr uint kMaxLines = 12;
using TextLine = Common::Span<const byte>; ///< byte to convert 128+ characters to image indices
Font &_font;
int _posY, _height, _width;
Color _color;
Common::Span<TextLine> _lines;
Common::Span<int> _posX;
TextLine _allLines[kMaxLines];
int _allPosX[kMaxLines];
};
enum class FadeType {
ToBlack,
ToWhite
// Originally there was a CrossFade, but it is unused for now and thus not implemented
};
enum class PermanentFadeAction {
Nothing,
SetFaded,
UnsetFaded
};
class FadeDrawRequest : public IDrawRequest {
public:
FadeDrawRequest(FadeType type, float value, int8 order);
void draw() override;
private:
FadeType _type;
float _value;
};
Task *fade(Process &process, FadeType fadeType,
float from, float to,
int32 duration, EasingType easingType,
int8 order,
PermanentFadeAction permanentFadeAction = PermanentFadeAction::Nothing);
class BorderDrawRequest : public IDrawRequest {
public:
BorderDrawRequest(Common::Rect rect, Color color);
void draw() override;
private:
Common::Rect _rect;
Color _color;
};
class BumpAllocator {
public:
BumpAllocator(size_t pageSize);
~BumpAllocator();
template<typename T, typename... Args>
inline T *allocate(Args&&... args) {
return new(allocateRaw(sizeof(T), alignof(T))) T(Common::forward<Args>(args)...);
}
void *allocateRaw(size_t size, size_t align);
void deallocateAll();
private:
void allocatePage();
const size_t _pageSize;
size_t _pageI = 0, _used = 0;
Common::Array<void *> _pages;
};
class DrawQueue {
public:
DrawQueue(IRenderer *renderer);
template<typename T, typename... Args>
inline void add(Args&&... args) {
addRequest(_allocator.allocate<T>(Common::forward<Args>(args)...));
}
inline BumpAllocator &allocator() { return _allocator; }
void clear();
void setLodBias(int8 orderFrom, int8 orderTo, float newLodBias);
void draw();
private:
void addRequest(IDrawRequest *drawRequest);
static constexpr const uint kMaxDrawRequestsPerOrder = 50;
IRenderer *const _renderer;
BumpAllocator _allocator;
IDrawRequest *_requestsPerOrder[kOrderCount][kMaxDrawRequestsPerOrder] = { { 0 } };
uint8 _requestsPerOrderCount[kOrderCount] = { 0 };
float _lodBiasPerOrder[kOrderCount] = { 0 };
};
}
#endif // ALCACHOFA_GRAPHICS_H

105
engines/alcachofa/input.cpp Normal file
View File

@@ -0,0 +1,105 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "alcachofa/input.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/metaengine.h"
using namespace Common;
namespace Alcachofa {
void Input::nextFrame() {
if (_debugInput != nullptr)
return _debugInput->nextFrame();
_wasMouseLeftPressed = false;
_wasMouseRightPressed = false;
_wasMouseLeftReleased = false;
_wasMouseRightReleased = false;
_wasMenuKeyPressed = false;
_wasInventoryKeyPressed = false;
updateMousePos3D(); // camera transformation might have changed
}
bool Input::handleEvent(const Common::Event &event) {
if (_debugInput != nullptr) {
auto result = _debugInput->handleEvent(event);
_mousePos2D = _debugInput->mousePos2D(); // even for debug input we want to e.g. draw a cursor
_mousePos3D = _debugInput->mousePos3D();
return result;
}
switch (event.type) {
case EVENT_LBUTTONDOWN:
_wasMouseLeftPressed = true;
_isMouseLeftDown = true;
return true;
case EVENT_LBUTTONUP:
_wasMouseLeftReleased = true;
_isMouseLeftDown = false;
return true;
case EVENT_RBUTTONDOWN:
_wasMouseRightPressed = true;
_isMouseRightDown = true;
return true;
case EVENT_RBUTTONUP:
_wasMouseRightReleased = true;
_isMouseRightDown = false;
return true;
case EVENT_MOUSEMOVE: {
_mousePos2D = event.mouse;
updateMousePos3D();
return true;
case EVENT_CUSTOM_ENGINE_ACTION_START:
switch ((EventAction)event.customType) {
case EventAction::InputMenu:
_wasMenuKeyPressed = true;
return true;
case EventAction::InputInventory:
_wasInventoryKeyPressed = true;
return true;
default:
return false;
}
}
default:
return false;
}
}
void Input::toggleDebugInput(bool debugMode) {
if (!debugMode) {
_debugInput.reset();
return;
}
nextFrame(); // resets frame-specific flags
_isMouseLeftDown = _isMouseRightDown = false;
if (_debugInput == nullptr)
_debugInput.reset(new Input());
}
void Input::updateMousePos3D() {
auto pos3D = g_engine->camera().transform2Dto3D({ (float)_mousePos2D.x, (float)_mousePos2D.y, kBaseScale });
_mousePos3D = { (int16)pos3D.x(), (int16)pos3D.y() };
}
}

71
engines/alcachofa/input.h Normal file
View File

@@ -0,0 +1,71 @@
/* 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 ALCACHOFA_INPUT_H
#define ALCACHOFA_INPUT_H
#include "common/events.h"
#include "common/ptr.h"
namespace Alcachofa {
class Input {
public:
inline bool wasMouseLeftPressed() const { return _wasMouseLeftPressed; }
inline bool wasMouseRightPressed() const { return _wasMouseRightPressed; }
inline bool wasAnyMousePressed() const { return _wasMouseLeftPressed || _wasMouseRightPressed; }
inline bool wasMouseLeftReleased() const { return _wasMouseLeftReleased; }
inline bool wasMouseRightReleased() const { return _wasMouseRightReleased; }
inline bool wasAnyMouseReleased() const { return _wasMouseLeftReleased || _wasMouseRightReleased; }
inline bool isMouseLeftDown() const { return _isMouseLeftDown; }
inline bool isMouseRightDown() const { return _isMouseRightDown; }
inline bool isAnyMouseDown() const { return _isMouseLeftDown || _isMouseRightDown; }
inline bool wasMenuKeyPressed() const { return _wasMenuKeyPressed; }
inline bool wasInventoryKeyPressed() const { return _wasInventoryKeyPressed; }
inline Common::Point mousePos2D() const { return _mousePos2D; }
inline Common::Point mousePos3D() const { return _mousePos3D; }
const Input &debugInput() const { scumm_assert(_debugInput != nullptr); return *_debugInput; }
void nextFrame();
bool handleEvent(const Common::Event &event);
void toggleDebugInput(bool debugMode); ///< Toggles input debug mode which blocks any input not retrieved with debugInput
private:
void updateMousePos3D();
bool
_wasMouseLeftPressed = false,
_wasMouseRightPressed = false,
_wasMouseLeftReleased = false,
_wasMouseRightReleased = false,
_isMouseLeftDown = false,
_isMouseRightDown = false,
_wasMenuKeyPressed = false,
_wasInventoryKeyPressed = false;
Common::Point
_mousePos2D,
_mousePos3D;
Common::ScopedPtr<Input> _debugInput;
};
}
#endif // ALCACHOFA_INPUT_H

375
engines/alcachofa/menu.cpp Normal file
View File

@@ -0,0 +1,375 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "gui/message.h"
#include "graphics/thumbnail.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/metaengine.h"
#include "alcachofa/menu.h"
#include "alcachofa/player.h"
#include "alcachofa/script.h"
using namespace Common;
using namespace Graphics;
namespace Alcachofa {
static void createThumbnail(ManagedSurface &surface) {
surface.create(kBigThumbnailWidth, kBigThumbnailHeight, g_engine->renderer().getPixelFormat());
}
static void convertToGrayscale(ManagedSurface &surface) {
// TODO: Support other pixel formats
assert(!surface.empty());
assert(surface.format == PixelFormat::createFormatRGBA32());
uint32 rgbMask = ~(uint32(0xff) << surface.format.aShift);
for (int y = 0; y < surface.h; y++) {
union {
uint32 *pixel;
uint8 *components;
};
pixel = (uint32 *)surface.getBasePtr(0, y);
for (int x = 0; x < surface.w; x++, pixel++) {
*pixel &= rgbMask;
byte gray = (byte)CLIP(0.29f * components[0] + 0.58f * components[1] + 0.11f * components[2], 0.0f, 255.0f);
*pixel =
(uint32(gray) << surface.format.rShift) |
(uint32(gray) << surface.format.gShift) |
(uint32(gray) << surface.format.bShift) |
(uint32(0xff) << surface.format.aShift);
}
}
}
Menu::Menu()
: _interactionSemaphore("menu")
, _saveFileMgr(g_system->getSavefileManager()) {}
void Menu::resetAfterLoad() {
_isOpen = false;
_openAtNextFrame = false;
_previousRoom = nullptr;
_bigThumbnail.free();
_selectedThumbnail.free();
}
void Menu::updateOpeningMenu() {
if (!_openAtNextFrame) {
if (g_engine->input().wasMenuKeyPressed() && g_engine->player().isAllowedToOpenMenu()) {
_openAtNextFrame = true;
createThumbnail(_bigThumbnail);
g_engine->renderer().setOutput(*_bigThumbnail.surfacePtr());
}
return;
}
_openAtNextFrame = false;
g_engine->sounds().pauseAll(true);
_millisBeforeMenu = g_engine->getMillis();
_previousRoom = g_engine->player().currentRoom();
_isOpen = true;
g_engine->player().changeRoom("MENUPRINCIPAL", true);
_savefiles = _saveFileMgr->listSavefiles(g_engine->getSaveStatePattern());
sort(_savefiles.begin(), _savefiles.end()); // the pattern ensures that the last file has the greatest slot
_selectedSavefileI = _savefiles.size();
updateSelectedSavefile(false);
g_engine->player().heldItem() = nullptr;
g_engine->scheduler().backupContext();
g_engine->camera().backup(1);
g_engine->camera().setPosition(Math::Vector3d(
g_system->getWidth() / 2.0f, g_system->getHeight() / 2.0f, 0.0f));
}
static int parseSavestateSlot(const String &filename) {
if (filename.size() < 5) // minimal name would be "t.###"
return 1;
return atoi(filename.c_str() + filename.size() - 3);
}
void Menu::updateSelectedSavefile(bool hasJustSaved) {
auto getButton = [] (const char *name) {
MenuButton *button = dynamic_cast<MenuButton *>(g_engine->player().currentRoom()->getObjectByName(name));
scumm_assert(button != nullptr);
return button;
};
bool isOldSavefile = _selectedSavefileI < _savefiles.size();
getButton("CARGAR")->isInteractable() = isOldSavefile;
getButton("ANTERIOR")->toggle(_selectedSavefileI > 0);
getButton("SIGUIENTE")->toggle(isOldSavefile);
if (hasJustSaved) {
// we just saved in-game so we also still have the correct thumbnail in memory
_selectedThumbnail.copyFrom(_bigThumbnail);
} else if (isOldSavefile) {
if (!tryReadOldSavefile()) {
_selectedSavefileDescription = String::format("Savestate %d",
parseSavestateSlot(_savefiles[_selectedSavefileI]));
createThumbnail(_selectedThumbnail);
}
} else {
// the unsaved gamestate is shown as grayscale
_selectedThumbnail.copyFrom(_bigThumbnail);
convertToGrayscale(_selectedThumbnail);
}
ObjectBase *captureObject = g_engine->player().currentRoom()->getObjectByName("Capture");
scumm_assert(captureObject);
Graphic *captureGraphic = captureObject->graphic();
scumm_assert(captureGraphic);
captureGraphic->animation().overrideTexture(_selectedThumbnail);
}
bool Menu::tryReadOldSavefile() {
auto savefile = ScopedPtr<InSaveFile>(
_saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]));
if (savefile == nullptr)
return false;
ExtendedSavegameHeader header;
if (!g_engine->getMetaEngine()->readSavegameHeader(savefile.get(), &header, true))
return false;
_selectedSavefileDescription = header.description;
MySerializer serializer(savefile.get(), nullptr);
if (!serializer.syncVersion((Serializer::Version)kCurrentSaveVersion) ||
!g_engine->syncThumbnail(serializer, &_selectedThumbnail))
return false;
return true;
}
void Menu::continueGame() {
assert(_previousRoom != nullptr);
_isOpen = false;
_bigThumbnail.free();
_selectedThumbnail.free();
g_engine->input().nextFrame(); // presumably to clear all was* flags
g_engine->player().changeRoom(_previousRoom->name(), true);
g_engine->sounds().pauseAll(false);
g_engine->camera().restore(1);
g_engine->scheduler().restoreContext();
g_engine->setMillis(_millisBeforeMenu);
}
void Menu::triggerMainMenuAction(MainMenuAction action) {
switch (action) {
case MainMenuAction::ContinueGame:
g_engine->menu().continueGame();
break;
case MainMenuAction::Save:
triggerSave();
break;
case MainMenuAction::Load: {
// we are in some update loop, let's load next frame upon event handling
// that should be safer
Event ev;
ev.type = EVENT_CUSTOM_ENGINE_ACTION_START;
ev.customType = (CustomEventType)EventAction::LoadFromMenu;
g_system->getEventManager()->pushEvent(ev);
}break;
case MainMenuAction::InternetMenu: {
GUI::MessageDialog dialog("Multiplayer is not implemented in this ScummVM version.");
dialog.runModal();
}break;
case MainMenuAction::OptionsMenu:
g_engine->menu().openOptionsMenu();
break;
case MainMenuAction::Exit:
case MainMenuAction::AlsoExit:
// implemented in AlcachofaEngine as it has its own event loop
g_engine->fadeExit();
break;
case MainMenuAction::NextSave:
if (_selectedSavefileI < _savefiles.size()) {
_selectedSavefileI++;
updateSelectedSavefile(false);
}
break;
case MainMenuAction::PrevSave:
if (_selectedSavefileI > 0) {
_selectedSavefileI--;
updateSelectedSavefile(false);
}
break;
case MainMenuAction::NewGame:
// this action might be unused just like the only room it would appear: MENUPRINCIPALINICIO
g_engine->player().isGameLoaded() = true;
g_engine->script().createProcess(MainCharacterKind::None, g_engine->world().initScriptName());
break;
default:
warning("Unknown main menu action: %d", (int32)action);
break;
}
}
void Menu::triggerLoad() {
auto *savefile = _saveFileMgr->openForLoading(_savefiles[_selectedSavefileI]);
auto result = g_engine->loadGameStream(savefile);
delete savefile;
if (result.getCode() != kNoError) {
GUI::MessageDialog dialog(result.getTranslatedDesc());
dialog.runModal();
return;
}
}
void Menu::triggerSave() {
String fileName;
if (_selectedSavefileI < _savefiles.size()) {
fileName = _savefiles[_selectedSavefileI]; // overwrite a previous save
} else {
// for a new savefile we figure out the next slot index
int nextSlot = _savefiles.empty()
? 1 // start at one to keep autosave alone
: parseSavestateSlot(_savefiles.back()) + 1;
fileName = g_engine->getSaveStateName(nextSlot);
_selectedSavefileDescription = String::format("Savestate %d", nextSlot);
}
Error error(kNoError);
auto savefile = ScopedPtr<OutSaveFile>(_saveFileMgr->openForSaving(fileName));
if (savefile == nullptr)
error = Error(kReadingFailed);
else
error = g_engine->saveGameStream(savefile.get());
if (error.getCode() == kNoError) {
g_engine->getMetaEngine()->appendExtendedSave(savefile.get(), g_engine->getTotalPlayTime(), _selectedSavefileDescription, false);
if (_selectedSavefileI >= _savefiles.size())
_savefiles.push_back(fileName);
updateSelectedSavefile(true);
} else {
GUI::MessageDialog dialog(error.getTranslatedDesc());
dialog.runModal();
}
}
void Menu::openOptionsMenu() {
setOptionsState();
g_engine->player().changeRoom("MENUOPCIONES", true);
}
void Menu::setOptionsState() {
Config &config = g_engine->config();
Room *optionsMenu = g_engine->world().getRoomByName("MENUOPCIONES");
scumm_assert(optionsMenu != nullptr);
auto getSlideButton = [&] (const char *name) {
SlideButton *slideButton = dynamic_cast<SlideButton *>(optionsMenu->getObjectByName(name));
scumm_assert(slideButton != nullptr);
return slideButton;
};
SlideButton
*slideMusicVolume = getSlideButton("Slider Musica"),
*slideSpeechVolume = getSlideButton("Slider Sonido");
slideMusicVolume->value() = config.musicVolume() / 255.0f;
slideSpeechVolume->value() = config.speechVolume() / 255.0f;
if (!config.bits32())
config.highQuality() = false;
auto getCheckBox = [&] (const char *name) {
CheckBox *checkBox = dynamic_cast<CheckBox *>(optionsMenu->getObjectByName(name));
scumm_assert(checkBox != nullptr);
return checkBox;
};
CheckBox
*checkSubtitlesOn = getCheckBox("Boton ON"),
*checkSubtitlesOff = getCheckBox("Boton OFF"),
*check32Bits = getCheckBox("Boton 32 Bits"),
*check16Bits = getCheckBox("Boton 16 Bits"),
*checkHighQuality = getCheckBox("Boton Alta"),
*checkLowQuality = getCheckBox("Boton Baja");
checkSubtitlesOn->isChecked() = config.subtitles();
checkSubtitlesOff->isChecked() = !config.subtitles();
check32Bits->isChecked() = config.bits32();
check16Bits->isChecked() = !config.bits32();
checkHighQuality->isChecked() = config.highQuality();
checkLowQuality->isChecked() = !config.highQuality();
checkHighQuality->toggle(config.bits32());
}
void Menu::triggerOptionsAction(OptionsMenuAction action) {
Config &config = g_engine->config();
switch (action) {
case OptionsMenuAction::SubtitlesOn:
config.subtitles() = true;
break;
case OptionsMenuAction::SubtitlesOff:
config.subtitles() = false;
break;
case OptionsMenuAction::HighQuality:
config.highQuality() = true;
break;
case OptionsMenuAction::LowQuality:
config.highQuality() = false;
break;
case OptionsMenuAction::Bits32:
config.bits32() = true;
config.highQuality() = true;
break;
case OptionsMenuAction::Bits16:
config.bits32() = false;
break;
case OptionsMenuAction::MainMenu:
continueMainMenu();
break;
default:
warning("Unknown options menu action: %d", (int32)action);
break;
}
setOptionsState();
}
void Menu::triggerOptionsValue(OptionsMenuValue valueId, float value) {
Config &config = g_engine->config();
switch (valueId) {
case OptionsMenuValue::Music:
config.musicVolume() = CLIP<uint8>((uint8)(value * 255), 0, 255);
break;
case OptionsMenuValue::Speech:
config.speechVolume() = CLIP<uint8>((uint8)(value * 255), 0, 255);
break;
default:
warning("Unknown options menu value: %d", (int32)valueId);
break;
}
setOptionsState();
}
void Menu::continueMainMenu() {
g_engine->config().saveToScummVM();
g_engine->syncSoundSettings();
g_engine->player().changeRoom(
g_engine->player().isGameLoaded() ? "MENUPRINCIPAL" : "MENUPRINCIPALINICIO",
true
);
updateSelectedSavefile(false);
}
const Graphics::Surface *Menu::getBigThumbnail() const {
return _bigThumbnail.empty() ? nullptr : &_bigThumbnail.rawSurface();
}
}

108
engines/alcachofa/menu.h Normal file
View File

@@ -0,0 +1,108 @@
/* 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 ALCACHOFA_MENU_H
#define ALCACHOFA_MENU_H
#include "common/savefile.h"
namespace Alcachofa {
class Room;
enum class MainMenuAction : int32 {
ContinueGame = 0,
Save,
Load,
InternetMenu,
OptionsMenu,
Exit,
NextSave,
PrevSave,
NewGame,
AlsoExit // there seems to be no difference to Exit
};
enum class OptionsMenuAction : int32 {
SubtitlesOn = 0,
SubtitlesOff,
HighQuality,
LowQuality,
Bits32,
Bits16,
MainMenu
};
enum class OptionsMenuValue : int32 {
Music = 0,
Speech = 1
};
class Menu {
public:
Menu();
inline bool isOpen() const { return _isOpen; }
inline uint32 millisBeforeMenu() const { return _millisBeforeMenu; }
inline Room *previousRoom() { return _previousRoom; }
inline FakeSemaphore &interactionSemaphore() { return _interactionSemaphore; }
void resetAfterLoad();
void updateOpeningMenu();
void triggerMainMenuAction(MainMenuAction action);
void triggerLoad();
void openOptionsMenu();
void triggerOptionsAction(OptionsMenuAction action);
void triggerOptionsValue(OptionsMenuValue valueId, float value);
// if we do still have a big thumbnail, any autosaves, ScummVM-saves, ingame-saves
// do not have to render themselves, they can just reuse the one we have.
// as such - may return nullptr
const Graphics::Surface *getBigThumbnail() const;
private:
void triggerSave();
void updateSelectedSavefile(bool hasJustSaved);
bool tryReadOldSavefile();
void continueGame();
void continueMainMenu();
void setOptionsState();
bool
_isOpen = false,
_openAtNextFrame = false;
uint32
_millisBeforeMenu = 0,
_selectedSavefileI = 0;
Room *_previousRoom = nullptr;
FakeSemaphore _interactionSemaphore; // to prevent ScummVM loading during button clicks
Common::String _selectedSavefileDescription = "<unset>";
Common::Array<Common::String> _savefiles;
Graphics::ManagedSurface
_bigThumbnail, // big because it is for the in-game menu, not for ScummVM
_selectedThumbnail;
Common::SaveFileManager *_saveFileMgr;
};
}
#endif // ALCACHOFA_MENU_H

View File

@@ -0,0 +1,129 @@
/* 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/translation.h"
#include "backends/keymapper/keymapper.h"
#include "backends/keymapper/action.h"
#include "backends/keymapper/standard-actions.h"
#include "alcachofa/metaengine.h"
#include "alcachofa/alcachofa.h"
using namespace Common;
using namespace Graphics;
using namespace Alcachofa;
namespace Alcachofa {
static const ADExtraGuiOptionsMap optionsList[] = {
{
GAMEOPTION_HIGH_QUALITY,
{
_s("High Quality"),
_s("Toggles some optional graphical effects"),
"high_quality",
true,
0,
0
}
},
{
GAMEOPTION_32BITS,
{
_s("32 Bits"),
_s("Whether to render the game in 16-bit color"),
"32_bits",
true,
0,
0
}
},
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
} // End of namespace Alcachofa
const char *AlcachofaMetaEngine::getName() const {
return "alcachofa";
}
const ADExtraGuiOptionsMap *AlcachofaMetaEngine::getAdvancedExtraGuiOptions() const {
return Alcachofa::optionsList;
}
Error AlcachofaMetaEngine::createInstance(OSystem *syst, Engine **engine, const AlcachofaGameDescription *desc) const {
*engine = new Alcachofa::AlcachofaEngine(syst, desc);
return kNoError;
}
bool AlcachofaMetaEngine::hasFeature(MetaEngineFeature f) const {
return checkExtendedSaves(f) ||
(f == kSupportsLoadingDuringStartup);
}
KeymapArray AlcachofaMetaEngine::initKeymaps(const char *target) const {
Keymap *keymap = new Keymap(Keymap::kKeymapTypeGame, "alcachofa-default", _("Default keymappings"));
Action *act;
act = new Action(kStandardActionLeftClick, _("Activate"));
act->setLeftClickEvent();
act->addDefaultInputMapping("MOUSE_LEFT");
act->addDefaultInputMapping("JOY_A");
keymap->addAction(act);
act = new Action(kStandardActionRightClick, _("Look at"));
act->setRightClickEvent();
act->addDefaultInputMapping("MOUSE_RIGHT");
act->addDefaultInputMapping("JOY_B");
keymap->addAction(act);
act = new Action("MENU", _("Menu"));
act->setCustomEngineActionEvent((CustomEventType)EventAction::InputMenu);
act->addDefaultInputMapping("ESCAPE");
act->addDefaultInputMapping("JOY_START");
keymap->addAction(act);
act = new Action("INVENTORY", _("Inventory"));
act->setCustomEngineActionEvent((CustomEventType)EventAction::InputInventory);
act->addDefaultInputMapping("SPACE");
act->addDefaultInputMapping("JOY_B");
keymap->addAction(act);
return Keymap::arrayOf(keymap);
}
void AlcachofaMetaEngine::getSavegameThumbnail(Surface &surf) {
if (Alcachofa::g_engine != nullptr) {
Surface bigThumbnail;
Alcachofa::g_engine->getSavegameThumbnail(bigThumbnail);
if (bigThumbnail.getPixels() != nullptr)
surf = *bigThumbnail.scale(kSmallThumbnailWidth, kSmallThumbnailHeight, true);
bigThumbnail.free();
}
// if not, ScummVM will output an appropriate warning
}
#if PLUGIN_ENABLED_DYNAMIC(ALCACHOFA)
REGISTER_PLUGIN_DYNAMIC(ALCACHOFA, PLUGIN_TYPE_ENGINE, AlcachofaMetaEngine);
#else
REGISTER_PLUGIN_STATIC(ALCACHOFA, PLUGIN_TYPE_ENGINE, AlcachofaMetaEngine);
#endif

View File

@@ -0,0 +1,59 @@
/* 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 ALCACHOFA_METAENGINE_H
#define ALCACHOFA_METAENGINE_H
#include "alcachofa/detection.h"
namespace Alcachofa {
enum class EventAction {
LoadFromMenu,
InputMenu,
InputInventory
};
}
class AlcachofaMetaEngine : public AdvancedMetaEngine<Alcachofa::AlcachofaGameDescription> {
public:
const char *getName() const override;
Common::Error createInstance(OSystem *syst, Engine **engine, const Alcachofa::AlcachofaGameDescription *desc) const override;
/**
* Determine whether the engine supports the specified MetaEngine feature.
*
* Used by e.g. the launcher to determine whether to enable the Load button.
*/
bool hasFeature(MetaEngineFeature f) const override;
const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override;
Common::KeymapArray initKeymaps(const char *target) const override;
void getSavegameThumbnail(Graphics::Surface &thumb) override;
};
#endif // ALCACHOFA_METAENGINE_H

View File

@@ -0,0 +1,58 @@
MODULE := engines/alcachofa
MODULE_OBJS = \
alcachofa.o \
camera.o \
common.o \
console.o \
game.o \
game-movie-adventure.o \
game-objects.o \
general-objects.o \
global-ui.o \
graphics.o \
graphics-opengl-base.o \
input.o \
menu.o \
metaengine.o \
player.o \
rooms.o \
scheduler.o \
script.o \
shape.o \
sounds.o \
ui-objects.o
ifdef USE_OPENGL_GAME
MODULE_OBJS += graphics-opengl.o
else # create_project cannot handle else and ifdef on the same line
ifdef USE_OPENGL_SHADERS
MODULE_OBJS += graphics-opengl.o
endif
endif
ifdef USE_OPENGL_GAME
MODULE_OBJS += \
graphics-opengl-classic.o
endif
ifdef USE_OPENGL_SHADERS
MODULE_OBJS += \
graphics-opengl-shaders.o
endif
ifdef USE_TINYGL
MODULE_OBJS += \
graphics-tinygl.o
endif
# This module can be built as a plugin
ifeq ($(ENABLE_ALCACHOFA), DYNAMIC_PLUGIN)
PLUGIN := 1
endif
# Include common rules
include $(srcdir)/rules.mk
# Detection objects
DETECT_OBJS += $(MODULE)/detection.o

605
engines/alcachofa/objects.h Normal file
View File

@@ -0,0 +1,605 @@
/* 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 ALCACHOFA_OBJECTS_H
#define ALCACHOFA_OBJECTS_H
#include "alcachofa/shape.h"
#include "alcachofa/graphics.h"
#include "common/serializer.h"
namespace Alcachofa {
class Room;
class Process;
struct Task;
class ObjectBase {
public:
static constexpr const char *kClassName = "CObjetoBase";
ObjectBase(Room *room, const char *name);
ObjectBase(Room *room, Common::ReadStream &stream);
virtual ~ObjectBase() {}
inline const Common::String &name() const { return _name; }
inline Room *&room() { return _room; }
inline Room *room() const { return _room; }
inline bool isEnabled() const { return _isEnabled; }
virtual void toggle(bool isEnabled);
virtual void draw();
virtual void drawDebug();
virtual void update();
virtual void loadResources();
virtual void freeResources();
virtual void syncGame(Common::Serializer &serializer);
virtual Graphic *graphic();
virtual Shape *shape();
virtual const char *typeName() const;
private:
Common::String _name;
bool _isEnabled = true;
Room *_room = nullptr;
};
class PointObject : public ObjectBase {
public:
static constexpr const char *kClassName = "CObjetoPunto";
PointObject(Room *room, Common::ReadStream &stream);
inline Common::Point &position() { return _pos; }
inline Common::Point position() const { return _pos; }
const char *typeName() const override;
private:
Common::Point _pos;
};
enum class GraphicObjectType : byte
{
Normal,
NormalPosterize, // the posterization is not actually applied in the original engine
Effect
};
class GraphicObject : public ObjectBase {
public:
static constexpr const char *kClassName = "CObjetoGrafico";
GraphicObject(Room *room, Common::ReadStream &stream);
~GraphicObject() override {}
void draw() override;
void drawDebug() override;
void loadResources() override;
void freeResources() override;
void syncGame(Common::Serializer &serializer) override;
Graphic *graphic() override;
const char *typeName() const override;
Task *animate(Process &process);
protected:
GraphicObject(Room *room, const char *name);
Graphic _graphic;
GraphicObjectType _type;
int32 _posterizeAlpha;
};
class SpecialEffectObject final : public GraphicObject {
public:
static constexpr const char *kClassName = "CObjetoGraficoMuare";
SpecialEffectObject(Room *room, Common::ReadStream &stream);
void draw() override;
const char *typeName() const override;
private:
static constexpr const float kShiftSpeed = 1 / 256.0f;
Common::Point _topLeft, _bottomRight;
Math::Vector2d _texShift;
};
class ShapeObject : public ObjectBase {
public:
ShapeObject(Room *room, Common::ReadStream &stream);
~ShapeObject() override {}
inline int8 order() const { return _order; }
inline bool isNewlySelected() const { return _isNewlySelected; }
inline bool wasSelected() const { return _wasSelected; }
void update() override;
void syncGame(Common::Serializer &serializer) override;
Shape *shape() override;
virtual CursorType cursorType() const;
virtual void onHoverStart();
virtual void onHoverEnd();
virtual void onHoverUpdate();
virtual void onClick();
const char *typeName() const override;
void markSelected();
protected:
void updateSelection();
// original inconsistency: base class has member that is read by the sub classes
int8 _order = 0;
private:
Shape _shape;
CursorType _cursorType;
bool _isNewlySelected = false,
_wasSelected = false;
};
class PhysicalObject : public ShapeObject {
public:
PhysicalObject(Room *room, Common::ReadStream &stream);
const char *typeName() const override;
};
class MenuButton : public PhysicalObject {
public:
static constexpr const char *kClassName = "CBotonMenu";
MenuButton(Room *room, Common::ReadStream &stream);
~MenuButton() override {}
inline int32 actionId() const { return _actionId; }
inline bool &isInteractable() { return _isInteractable; }
void draw() override;
void update() override;
void loadResources() override;
void freeResources() override;
void onHoverUpdate() override;
void onClick() override;
virtual void trigger();
const char *typeName() const override;
private:
bool
_isInteractable = true,
_isClicked = false,
_triggerNextFrame = false;
int32 _actionId;
Graphic
_graphicNormal,
_graphicHovered,
_graphicClicked,
_graphicDisabled;
FakeLock _interactionLock;
};
// some of the UI elements are only used for the multiplayer menus
// so are currently not needed
class InternetMenuButton final : public MenuButton {
public:
static constexpr const char *kClassName = "CBotonMenuInternet";
InternetMenuButton(Room *room, Common::ReadStream &stream);
const char *typeName() const override;
};
class OptionsMenuButton final : public MenuButton {
public:
static constexpr const char *kClassName = "CBotonMenuOpciones";
OptionsMenuButton(Room *room, Common::ReadStream &stream);
void update() override;
void trigger() override;
const char *typeName() const override;
};
class MainMenuButton final : public MenuButton {
public:
static constexpr const char *kClassName = "CBotonMenuPrincipal";
MainMenuButton(Room *room, Common::ReadStream &stream);
void update() override;
void trigger() override;
const char *typeName() const override;
};
class PushButton final : public PhysicalObject {
public:
static constexpr const char *kClassName = "CPushButton";
PushButton(Room *room, Common::ReadStream &stream);
const char *typeName() const override;
private:
bool _alwaysVisible;
Graphic _graphic1, _graphic2;
int32 _actionId;
};
class EditBox final : public PhysicalObject {
public:
static constexpr const char *kClassName = "CEditBox";
EditBox(Room *room, Common::ReadStream &stream);
const char *typeName() const override;
private:
int32 i1;
Common::Point p1;
Common::String _labelId;
bool b1;
int32 i3, i4, i5,
_fontId;
};
class CheckBox : public PhysicalObject {
public:
static constexpr const char *kClassName = "CCheckBox";
CheckBox(Room *room, Common::ReadStream &stream);
~CheckBox() override {}
inline bool &isChecked() { return _isChecked; }
inline int32 actionId() const { return _actionId; }
void draw() override;
void update() override;
void loadResources() override;
void freeResources() override;
void onHoverUpdate() override;
void onClick() override;
virtual void trigger();
const char *typeName() const override;
private:
bool
_isChecked = false,
_wasClicked = false;
Graphic
_graphicUnchecked,
_graphicChecked,
_graphicHovered,
_graphicClicked;
int32 _actionId = 0;
uint32 _clickTime = 0;
};
class SlideButton final : public ObjectBase {
public:
static constexpr const char *kClassName = "CSlideButton";
SlideButton(Room *room, Common::ReadStream &stream);
~SlideButton() override {}
inline float &value() { return _value; }
void draw() override;
void update() override;
void loadResources() override;
void freeResources() override;
const char *typeName() const override;
private:
bool isMouseOver() const;
float _value = 0;
int32 _valueId;
Common::Point _minPos, _maxPos;
Graphic
_graphicIdle,
_graphicHovered,
_graphicClicked;
};
class CheckBoxAutoAdjustNoise final : public CheckBox {
public:
static constexpr const char *kClassName = "CCheckBoxAutoAjustarRuido";
CheckBoxAutoAdjustNoise(Room *room, Common::ReadStream &stream);
const char *typeName() const override;
};
class IRCWindow final : public ObjectBase {
public:
static constexpr const char *kClassName = "CVentanaIRC";
IRCWindow(Room *room, Common::ReadStream &stream);
const char *typeName() const override;
private:
Common::Point _p1, _p2;
};
class MessageBox final : public ObjectBase {
public:
static constexpr const char *kClassName = "CMessageBox";
MessageBox(Room *room, Common::ReadStream &stream);
~MessageBox() override {}
const char *typeName() const override;
private:
Graphic
_graph1,
_graph2,
_graph3,
_graph4,
_graph5;
};
class VoiceMeter final : public GraphicObject {
public:
static constexpr const char *kClassName = "CVuMeter";
VoiceMeter(Room *room, Common::ReadStream &stream);
const char *typeName() const override;
};
class Item : public GraphicObject { //-V690
public:
static constexpr const char *kClassName = "CObjetoInventario";
Item(Room *room, Common::ReadStream &stream);
Item(const Item &other);
// no copy-assign operator as it is non-sensical, the copy ctor is a special case for item-handling
void draw() override;
const char *typeName() const override;
void trigger();
};
class ITriggerableObject {
public:
ITriggerableObject(Common::ReadStream &stream);
virtual ~ITriggerableObject() {}
inline Direction interactionDirection() const { return _interactionDirection; }
inline Common::Point interactionPoint() const { return _interactionPoint; }
virtual void trigger(const char *action) = 0;
protected:
void onClick();
Common::Point _interactionPoint;
Direction _interactionDirection = Direction::Right;
};
class InteractableObject : public PhysicalObject, public ITriggerableObject {
public:
static constexpr const char *kClassName = "CObjetoTipico";
InteractableObject(Room *room, Common::ReadStream &stream);
~InteractableObject() override {}
void drawDebug() override;
void onClick() override;
void trigger(const char *action) override;
void toggle(bool isEnabled) override;
const char *typeName() const override;
private:
Common::String _relatedObject;
};
class Door final : public InteractableObject {
public:
static constexpr const char *kClassName = "CPuerta";
Door(Room *room, Common::ReadStream &stream);
inline const Common::String &targetRoom() const { return _targetRoom; }
inline const Common::String &targetObject() const { return _targetObject; }
inline Direction characterDirection() const { return _characterDirection; }
CursorType cursorType() const override;
void onClick() override;
void trigger(const char *action) override;
const char *typeName() const override;
private:
Common::String _targetRoom, _targetObject;
Direction _characterDirection;
uint32 _lastClickTime = 0;
};
class Character : public ShapeObject, public ITriggerableObject {
public:
static constexpr const char *kClassName = "CPersonaje";
Character(Room *room, Common::ReadStream &stream);
~Character() override {}
void update() override;
void draw() override;
void drawDebug() override;
void loadResources() override;
void freeResources() override;
void syncGame(Common::Serializer &serializer) override;
Graphic *graphic() override;
void onClick() override;
void trigger(const char *action) override;
const char *typeName() const override;
Task *sayText(Process &process, int32 dialogId);
void resetTalking();
void talkUsing(ObjectBase *talkObject);
Task *animate(Process &process, ObjectBase *animateObject);
Task *lerpLodBias(Process &process, float targetLodBias, int32 durationMs);
inline float &lodBias() { return _lodBias; }
inline bool &isSpeaking() { return _isSpeaking; }
protected:
friend struct SayTextTask;
friend struct AnimateCharacterTask;
void syncObjectAsString(Common::Serializer &serializer, ObjectBase *&object);
void updateTalkingAnimation();
Graphic _graphicNormal, _graphicTalking;
bool _isTalking = false; ///< as in "in the process of saying a line"
bool _isSpeaking = true; ///< as in "actively moving their mouth to produce sounds", used in updateTalkingAnimation
int _curDialogId = -1;
float _lodBias = 0.0f;
ObjectBase
*_curAnimateObject = nullptr,
*_curTalkingObject = nullptr;
};
class WalkingCharacter : public Character {
public:
static constexpr const char *kClassName = "CPersonajeAnda";
WalkingCharacter(Room *room, Common::ReadStream &stream);
~WalkingCharacter() override {}
inline bool isWalking() const { return _isWalking; }
inline Common::Point position() const { return _currentPos; }
inline float stepSizeFactor() const { return _stepSizeFactor; }
void update() override;
void draw() override;
void drawDebug() override;
void loadResources() override;
void freeResources() override;
void syncGame(Common::Serializer &serializer) override;
virtual void walkTo(
Common::Point target,
Direction endDirection = Direction::Invalid,
ITriggerableObject *activateObject = nullptr,
const char *activateAction = nullptr);
void stopWalking(Direction direction = Direction::Invalid);
void setPosition(Common::Point target);
const char *typeName() const override;
Task *waitForArrival(Process &process);
protected:
virtual void onArrived();
void updateWalking();
void updateWalkingAnimation();
inline Animation *currentAnimationOf(Common::ScopedPtr<Animation> *const animations) {
Animation *animation = animations[(int)_direction].get();
if (animation == nullptr)
animation = animations[0].get();
assert(animation != nullptr);
return animation;
}
inline Animation *walkingAnimation() { return currentAnimationOf(_walkingAnimations); }
inline Animation *talkingAnimation() { return currentAnimationOf(_talkingAnimations); }
Common::ScopedPtr<Animation>
_walkingAnimations[kDirectionCount],
_talkingAnimations[kDirectionCount];
int32
_lastWalkAnimFrame = -1,
_walkedDistance = 0,
_curPathPointI = -1;
float _stepSizeFactor = 0.0f;
Common::Point
_sourcePos,
_currentPos;
bool _isWalking = false;
Direction
_direction = Direction::Right,
_endWalkingDirection = Direction::Invalid;
Common::Stack<Common::Point> _pathPoints;
};
struct DialogMenuLine {
int32 _dialogId = -1;
int32 _yPosition = 0;
int32 _returnValue = 0;
};
class MainCharacter final : public WalkingCharacter {
public:
static constexpr const char *kClassName = "CPersonajePrincipal";
MainCharacter(Room *room, Common::ReadStream &stream);
~MainCharacter() override;
inline MainCharacterKind kind() const { return _kind; }
inline ObjectBase *&currentlyUsing() { return _currentlyUsingObject; }
inline ObjectBase *currentlyUsing() const { return _currentlyUsingObject; }
inline Color &color() { return _color; }
inline uint8 &alphaPremultiplier() { return _alphaPremultiplier; }
inline FakeSemaphore &semaphore() { return _semaphore; }
bool isBusy() const;
void update() override;
void draw() override;
void syncGame(Common::Serializer &serializer) override;
const char *typeName() const override;
void walkTo(
Common::Point target,
Direction endDirection = Direction::Invalid,
ITriggerableObject *activateObject = nullptr,
const char *activateAction = nullptr) override;
void walkToMouse();
bool clearTargetIf(const ITriggerableObject *target);
void clearInventory();
bool hasItem(const Common::String &name) const;
void pickup(const Common::String &name, bool putInHand);
void drop(const Common::String &name);
void addDialogLine(int32 dialogId);
void setLastDialogReturnValue(int32 returnValue);
Task *dialogMenu(Process &process);
void resetUsingObjectAndDialogMenu();
protected:
void onArrived() override;
private:
friend class Inventory;
friend struct DialogMenuTask;
Item *getItemByName(const Common::String &name) const;
void drawInner();
Common::Array<Item *> _items;
Common::Array<DialogMenuLine> _dialogLines;
ObjectBase *_currentlyUsingObject = nullptr;
MainCharacterKind _kind;
FakeSemaphore _semaphore;
ITriggerableObject *_activateObject = nullptr;
const char *_activateAction = nullptr;
Color _color = kWhite;
uint8 _alphaPremultiplier = 255;
};
class Background final : public GraphicObject {
public:
Background(Room *room, const Common::String &animationFileName, int16 scale);
const char *typeName() const override;
};
class FloorColor final : public ObjectBase {
public:
static constexpr const char *kClassName = "CSueloColor";
FloorColor(Room *room, Common::ReadStream &stream);
~FloorColor() override {}
void update() override;
void drawDebug() override;
Shape *shape() override;
const char *typeName() const override;
private:
FloorColorShape _shape;
};
}
#endif // ALCACHOFA_OBJECTS_H

View File

@@ -0,0 +1,395 @@
/* 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 "alcachofa/player.h"
#include "alcachofa/script.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/menu.h"
using namespace Common;
namespace Alcachofa {
Player::Player()
: _activeCharacter(&g_engine->world().mortadelo())
, _semaphore("player") {
const auto &cursorPath = g_engine->world().getGlobalAnimationName(GlobalAnimationKind::Cursor);
_cursorAnimation.reset(new Animation(cursorPath));
_cursorAnimation->load();
}
void Player::preUpdate() {
_selectedObject = nullptr;
_cursorFrameI = 0;
}
void Player::postUpdate() {
if (g_engine->input().wasAnyMouseReleased())
_pressedObject = nullptr;
}
void Player::resetCursor() {
_cursorFrameI = 0;
}
void Player::updateCursor() {
if (g_engine->menu().isOpen() || !_isGameLoaded)
_cursorFrameI = 0;
else if (_selectedObject == nullptr)
_cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7;
else {
auto type = _selectedObject->cursorType();
switch (type) {
case CursorType::LeaveUp:
_cursorFrameI = 8;
break;
case CursorType::LeaveRight:
_cursorFrameI = 10;
break;
case CursorType::LeaveDown:
_cursorFrameI = 12;
break;
case CursorType::LeaveLeft:
_cursorFrameI = 14;
break;
case CursorType::WalkTo:
_cursorFrameI = 6;
break;
case CursorType::Point:
default:
_cursorFrameI = 0;
break;
}
if (_cursorFrameI != 0) {
if (g_engine->input().isAnyMouseDown() && _pressedObject == _selectedObject)
_cursorFrameI++;
} else if (g_engine->input().isMouseLeftDown())
_cursorFrameI = 2;
else if (g_engine->input().isMouseRightDown())
_cursorFrameI = 4;
}
}
void Player::drawCursor(bool forceDefaultCursor) {
Point cursorPos = g_engine->input().mousePos2D();
if (_heldItem == nullptr || forceDefaultCursor) {
if (forceDefaultCursor)
_cursorFrameI = 0;
g_engine->drawQueue().add<AnimationDrawRequest>(_cursorAnimation.get(), _cursorFrameI, as2D(cursorPos), -10);
} else {
auto itemGraphic = _heldItem->graphic();
assert(itemGraphic != nullptr);
auto &animation = itemGraphic->animation();
auto frameOffset = animation.totalFrameOffset(0);
auto imageSize = animation.imageSize(animation.imageIndex(0, 0));
cursorPos -= frameOffset + imageSize / 2;
g_engine->drawQueue().add<AnimationDrawRequest>(&animation, 0, as2D(cursorPos), -kForegroundOrderCount);
}
}
void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera, bool isTemporary) {
debugC(1, kDebugGameplay, "Change room to %s", targetRoomName.c_str());
// original would be to always free all resources from globalRoom, inventory, GlobalUI
// We don't do that, it is unnecessary, all resources would be loaded right after
// Instead we just keep resources loaded for all global rooms and during inventory/room transitions
if (targetRoomName.equalsIgnoreCase("SALIR")) {
_currentRoom = nullptr;
return; // exiting game entirely
}
if (_currentRoom != nullptr) {
g_engine->scheduler().killProcessByName("ACTUALIZAR_" + _currentRoom->name());
bool keepResources =
_currentRoom->name().equalsIgnoreCase(targetRoomName) ||
_currentRoom->name().equalsIgnoreCase("inventario");
if (targetRoomName.equalsIgnoreCase("inventario")) {
keepResources = true;
if (!_isInTemporaryRoom)
_roomBeforeInventory = _currentRoom;
}
if (!keepResources)
_currentRoom->freeResources();
}
// this fixes a bug with all original games where changing the room in the inventory (e.g. iFOTO in aventura de cine)
// would overwrite the actual game room thus returning from the inventory one would be stuck in the temporary room
// If we know that a transition is temporary we prevent that and only remember the real game room
_isInTemporaryRoom = isTemporary;
_currentRoom = g_engine->world().getRoomByName(targetRoomName.c_str());
if (_currentRoom == nullptr) // no good way to recover, leaving-the-room actions might already prevent further progress
error("Invalid room name: %s", targetRoomName.c_str());
if (!_didLoadGlobalRooms) {
_didLoadGlobalRooms = true;
g_engine->world().inventory().loadResources();
g_engine->world().globalRoom().loadResources();
}
_currentRoom->loadResources(); // if we kept resources we loop over a couple noops, that is fine.
if (resetCamera)
g_engine->camera().resetRotationAndScale();
WalkingCharacter *followTarget = g_engine->camera().followTarget();
if (followTarget != nullptr)
g_engine->camera().setFollow(followTarget, true);
_pressedObject = _selectedObject = nullptr;
}
void Player::changeRoomToBeforeInventory() {
assert(_roomBeforeInventory != nullptr);
changeRoom(_roomBeforeInventory->name(), true);
_roomBeforeInventory = nullptr;
}
MainCharacter *Player::inactiveCharacter() const {
if (_activeCharacter == nullptr)
return nullptr;
return &g_engine->world().getOtherMainCharacterByKind(activeCharacterKind());
}
FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) {
static FakeSemaphore dummySemaphore("dummy");
switch (kind) {
case MainCharacterKind::None:
return _semaphore;
case MainCharacterKind::Mortadelo:
return g_engine->world().mortadelo().semaphore();
case MainCharacterKind::Filemon:
return g_engine->world().filemon().semaphore();
default:
assert(false && "Invalid main character kind");
return dummySemaphore;
}
}
void Player::triggerObject(ObjectBase *object, const char *action) {
assert(object != nullptr && action != nullptr);
if (_activeCharacter->isBusy() || _activeCharacter->currentlyUsing() != nullptr)
return;
debugC(1, kDebugGameplay, "Trigger object %s %s with %s", object->typeName(), object->name().c_str(), action);
if (strcmp(action, "MIRAR") == 0 || inactiveCharacter()->currentlyUsing() == object) {
action = "MIRAR";
_activeCharacter->currentlyUsing() = nullptr;
} else
_activeCharacter->currentlyUsing() = object;
auto &script = g_engine->script();
if (script.createProcess(activeCharacterKind(), object->name(), action, ScriptFlags::AllowMissing) != nullptr)
return;
_activeCharacter->currentlyUsing() = nullptr;
if (scumm_stricmp(action, "MIRAR") == 0)
script.createProcess(activeCharacterKind(), "DefectoMirar");
//else if (action[0] == 'i' && object->name()[0] == 'i')
// This case can happen if you combine two objects without procedure, the original engine
// would attempt to start the procedure "DefectoObjeto" which does not exist
// (this should be revised when working on further games)
else
script.createProcess(activeCharacterKind(), "DefectoUsar");
}
struct DoorTask final : public Task {
DoorTask(Process &process, const Door *door, FakeLock &&lock)
: Task(process)
, _lock(move(lock))
, _sourceDoor(door)
, _character(g_engine->player().activeCharacter())
, _player(g_engine->player())
, _targetObject(nullptr)
, _targetDirection(Direction::Invalid) {
findTarget();
process.name() = String::format("Door to %s %s",
_targetRoom == nullptr ? "<null>" : _targetRoom->name().c_str(),
_targetObject == nullptr ? "<null>" : _targetObject->name().c_str());
}
DoorTask(Process &process, Serializer &s)
: Task(process)
, _player(g_engine->player()) {
DoorTask::syncGame(s);
}
TaskReturn run() override {
TASK_BEGIN;
if (_targetRoom == nullptr || _targetObject == nullptr)
return TaskReturn::finish(1);
_musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore());
if (g_engine->sounds().musicID() != _targetRoom->musicID())
g_engine->sounds().fadeMusic();
TASK_WAIT(1, fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5));
_player.changeRoom(_targetRoom->name(), true); //-V779
if (_targetRoom->fixedCameraOnEntering())
g_engine->camera().setPosition(as2D(_targetObject->interactionPoint()));
else {
_character->room() = _targetRoom;
_character->setPosition(_targetObject->interactionPoint());
_character->stopWalking(_targetDirection);
g_engine->camera().setFollow(_character, true);
}
g_engine->sounds().setMusicToRoom(_targetRoom->musicID());
_musicLock.release();
if (g_engine->script().createProcess(_character->kind(), "ENTRAR_" + _targetRoom->name(), ScriptFlags::AllowMissing))
TASK_YIELD(2);
else
TASK_WAIT(3, fade(process(), FadeType::ToBlack, 1, 0, 500, EasingType::Out, -5));
TASK_END;
}
void debugPrint() override {
g_engine->console().debugPrintf("%s\n", process().name().c_str());
}
void syncGame(Serializer &s) override {
assert(s.isSaving() || (_lock.isReleased() && _musicLock.isReleased()));
Task::syncGame(s);
syncObjectAsString(s, _sourceDoor);
syncObjectAsString(s, _character);
bool hasMusicLock = !_musicLock.isReleased();
s.syncAsByte(hasMusicLock);
if (s.isLoading() && hasMusicLock)
_musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore());
_lock = FakeLock("door", _character->semaphore());
findTarget();
}
const char *taskName() const override;
private:
void findTarget() {
_targetRoom = g_engine->world().getRoomByName(_sourceDoor->targetRoom().c_str());
if (_targetRoom == nullptr) {
g_engine->game().unknownDoorTargetRoom(_sourceDoor->targetRoom());
return;
}
_targetObject = dynamic_cast<InteractableObject *>(_targetRoom->getObjectByName(_sourceDoor->targetObject().c_str()));
if (_targetObject == nullptr) {
g_engine->game().unknownDoorTargetDoor(_sourceDoor->targetRoom(), _sourceDoor->targetObject());
return;
}
_targetDirection = _sourceDoor->characterDirection();
}
FakeLock _lock, _musicLock;
const Door *_sourceDoor = nullptr;
const InteractableObject *_targetObject = nullptr;
Direction _targetDirection = {};
Room *_targetRoom = nullptr;
MainCharacter *_character = nullptr;
Player &_player;
};
DECLARE_TASK(DoorTask)
void Player::triggerDoor(const Door *door) {
_heldItem = nullptr;
if (g_engine->game().shouldTriggerDoor(door)) {
FakeLock lock("door", _activeCharacter->semaphore());
g_engine->scheduler().createProcess<DoorTask>(activeCharacterKind(), door, move(lock));
}
}
// the last dialog character mechanic seems like a hack in the original engine
// all talking characters (see SayText kernel call) are added to a fixed-size
// rolling queue and stopped upon killProcesses
void Player::addLastDialogCharacter(Character *character) {
auto lastDialogCharactersEnd = _lastDialogCharacters + kMaxLastDialogCharacters;
if (Common::find(_lastDialogCharacters, lastDialogCharactersEnd, character) != lastDialogCharactersEnd)
return;
_lastDialogCharacters[_nextLastDialogCharacter++] = character;
_nextLastDialogCharacter %= kMaxLastDialogCharacters;
}
void Player::stopLastDialogCharacters() {
// originally only the isTalking flag is reset, but this seems a bit safer so unless we find a bug
for (int i = 0; i < kMaxLastDialogCharacters; i++) {
auto character = _lastDialogCharacters[i];
if (character != nullptr)
character->resetTalking();
}
}
void Player::setActiveCharacter(MainCharacterKind kind) {
scumm_assert(kind == MainCharacterKind::Mortadelo || kind == MainCharacterKind::Filemon);
_activeCharacter = &g_engine->world().getMainCharacterByKind(kind);
}
bool Player::isAllowedToOpenMenu() {
return
isGameLoaded() &&
!g_engine->menu().isOpen() &&
g_engine->sounds().musicSemaphore().isReleased() &&
!g_engine->script().variable("prohibirESC") &&
!_isInTemporaryRoom; // we cannot reliably store this state across multiple room changes
}
void Player::syncGame(Serializer &s) {
auto characterKind = activeCharacterKind();
syncEnum(s, characterKind);
switch (characterKind) {
case MainCharacterKind::None:
_activeCharacter = nullptr;
break;
case MainCharacterKind::Mortadelo:
case MainCharacterKind::Filemon:
_activeCharacter = &g_engine->world().getMainCharacterByKind(characterKind);
break;
default:
error("Invalid character kind in savestate: %d", (int)characterKind);
}
FakeSemaphore::sync(s, _semaphore);
String roomName;
if (s.isSaving()) {
roomName =
g_engine->menu().isOpen() ? g_engine->menu().previousRoom()->name() // save from in-game menu
: _roomBeforeInventory != nullptr ? _roomBeforeInventory->name() // save from ScummVM while in inventory
: currentRoom()->name(); // save from ScumnmVM global menu or autosave in normal gameplay
}
s.syncString(roomName);
if (s.isLoading()) {
_selectedObject = nullptr;
_pressedObject = nullptr;
_heldItem = nullptr;
_nextLastDialogCharacter = 0;
_isGameLoaded = true;
_roomBeforeInventory = nullptr;
_isInTemporaryRoom = false;
fill(_lastDialogCharacters, _lastDialogCharacters + kMaxLastDialogCharacters, nullptr);
changeRoom(roomName, true);
}
}
}

View File

@@ -0,0 +1,85 @@
/* 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 ALCACHOFA_PLAYER_H
#define ALCACHOFA_PLAYER_H
#include "alcachofa/rooms.h"
namespace Alcachofa {
class Player {
public:
Player();
inline Room *currentRoom() const { return _currentRoom; }
inline MainCharacter *activeCharacter() const { return _activeCharacter; }
inline ShapeObject *&selectedObject() { return _selectedObject; }
inline void *&pressedObject() { return _pressedObject; }
inline Item *&heldItem() { return _heldItem; }
inline FakeSemaphore &semaphore() { return _semaphore; }
MainCharacter *inactiveCharacter() const;
FakeSemaphore &semaphoreFor(MainCharacterKind kind);
inline bool &isGameLoaded() { return _isGameLoaded; }
inline MainCharacterKind activeCharacterKind() const {
return _activeCharacter == nullptr ? MainCharacterKind::None : _activeCharacter->kind();
}
void preUpdate();
void postUpdate();
void updateCursor();
void drawCursor(bool forceDefaultCursor = false);
void resetCursor();
void changeRoom(const Common::String &targetRoomName, bool resetCamera, bool isTemporary = false);
void changeRoomToBeforeInventory();
void triggerObject(ObjectBase *object, const char *action);
void triggerDoor(const Door *door);
void addLastDialogCharacter(Character *character);
void stopLastDialogCharacters();
void setActiveCharacter(MainCharacterKind kind);
bool isAllowedToOpenMenu();
void syncGame(Common::Serializer &s);
private:
static constexpr const int kMaxLastDialogCharacters = 4;
Common::ScopedPtr<Animation> _cursorAnimation;
FakeSemaphore _semaphore;
Room *_currentRoom = nullptr,
*_roomBeforeInventory = nullptr;
MainCharacter *_activeCharacter;
ShapeObject *_selectedObject = nullptr;
void *_pressedObject = nullptr; // terrible but GlobalUI wants to store a Graphic pointer
Item *_heldItem = nullptr;
int32 _cursorFrameI = 0;
bool
_isGameLoaded = true,
_didLoadGlobalRooms = false,
_isInTemporaryRoom = false;
Character *_lastDialogCharacters[kMaxLastDialogCharacters] = { nullptr };
int _nextLastDialogCharacter = 0;
};
}
#endif // ALCACHOFA_PLAYER_H

802
engines/alcachofa/rooms.cpp Normal file
View File

@@ -0,0 +1,802 @@
/* 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 "alcachofa/alcachofa.h"
#include "alcachofa/rooms.h"
#include "alcachofa/script.h"
#include "alcachofa/global-ui.h"
#include "alcachofa/menu.h"
#include "common/file.h"
using namespace Common;
namespace Alcachofa {
Room::Room(World *world, SeekableReadStream &stream) : Room(world, stream, false) {}
static ObjectBase *readRoomObject(Room *room, const String &type, ReadStream &stream) {
if (type == ObjectBase::kClassName)
return new ObjectBase(room, stream);
else if (type == PointObject::kClassName)
return new PointObject(room, stream);
else if (type == GraphicObject::kClassName)
return new GraphicObject(room, stream);
else if (type == SpecialEffectObject::kClassName)
return new SpecialEffectObject(room, stream);
else if (type == Item::kClassName)
return new Item(room, stream);
else if (type == PhysicalObject::kClassName)
return new PhysicalObject(room, stream);
else if (type == MainMenuButton::kClassName)
return new MainMenuButton(room, stream);
else if (type == InternetMenuButton::kClassName)
return new InternetMenuButton(room, stream);
else if (type == OptionsMenuButton::kClassName)
return new OptionsMenuButton(room, stream);
else if (type == EditBox::kClassName)
return new EditBox(room, stream);
else if (type == PushButton::kClassName)
return new PushButton(room, stream);
else if (type == CheckBox::kClassName)
return new CheckBox(room, stream);
else if (type == CheckBoxAutoAdjustNoise::kClassName)
return new CheckBoxAutoAdjustNoise(room, stream);
else if (type == SlideButton::kClassName)
return new SlideButton(room, stream);
else if (type == IRCWindow::kClassName)
return new IRCWindow(room, stream);
else if (type == MessageBox::kClassName)
return new MessageBox(room, stream);
else if (type == VoiceMeter::kClassName)
return new VoiceMeter(room, stream);
else if (type == InteractableObject::kClassName)
return new InteractableObject(room, stream);
else if (type == Door::kClassName)
return new Door(room, stream);
else if (type == Character::kClassName)
return new Character(room, stream);
else if (type == WalkingCharacter::kClassName)
return new WalkingCharacter(room, stream);
else if (type == MainCharacter::kClassName)
return new MainCharacter(room, stream);
else if (type == FloorColor::kClassName)
return new FloorColor(room, stream);
else
return nullptr; // handled in Room::Room
}
Room::Room(World *world, SeekableReadStream &stream, bool hasUselessByte)
: _world(world) {
_name = readVarString(stream);
_musicId = (int)stream.readByte();
_characterAlphaTint = stream.readByte();
auto backgroundScale = stream.readSint16LE();
_floors[0] = PathFindingShape(stream);
_floors[1] = PathFindingShape(stream);
_fixedCameraOnEntering = readBool(stream);
PathFindingShape _(stream); // unused path finding area
_characterAlphaPremultiplier = stream.readByte();
if (hasUselessByte)
stream.readByte();
uint32 objectEnd = stream.readUint32LE();
while (objectEnd > 0) {
const auto type = readVarString(stream);
auto object = readRoomObject(this, type, stream);
if (object == nullptr) {
g_engine->game().unknownRoomObject(type);
stream.seek(objectEnd, SEEK_SET);
} else if (stream.pos() < objectEnd) {
g_engine->game().notEnoughObjectDataRead(_name.c_str(), stream.pos(), objectEnd);
stream.seek(objectEnd, SEEK_SET);
} else if (stream.pos() > objectEnd) // this is probably not recoverable
error("Read past the object data (%u > %lld) in room %s", objectEnd, (long long int)stream.pos(), _name.c_str());
if (object != nullptr)
_objects.push_back(object);
objectEnd = stream.readUint32LE();
}
if (g_engine->game().doesRoomHaveBackground(this))
_objects.push_back(new Background(this, _name, backgroundScale));
if (!_floors[0].empty())
_activeFloorI = 0;
}
Room::~Room() {
for (auto *object : _objects)
delete object;
}
ObjectBase *Room::getObjectByName(const char *name) const {
for (auto *object : _objects) {
if (object->name().equalsIgnoreCase(name))
return object;
}
return nullptr;
}
void Room::update() {
if (g_engine->isDebugModeActive())
return;
updateScripts();
if (g_engine->player().currentRoom() == this) {
updateRoomBounds();
g_engine->globalUI().updateClosingInventory();
if (!updateInput())
return;
}
if (!g_engine->menu().isOpen() &&
g_engine->player().currentRoom() != &g_engine->world().inventory())
world().globalRoom().updateObjects();
if (g_engine->player().currentRoom() == this)
updateObjects();
}
void Room::draw() {
g_engine->camera().update();
drawObjects();
world().globalRoom().drawObjects();
g_engine->globalUI().drawScreenStates();
g_engine->drawQueue().draw();
drawDebug();
world().globalRoom().drawDebug();
}
void Room::updateScripts() {
g_engine->game().updateScriptVariables();
if (!g_engine->scheduler().hasProcessWithName("ACTUALIZAR_" + _name))
g_engine->script().createProcess(MainCharacterKind::None, "ACTUALIZAR_" + _name, ScriptFlags::AllowMissing | ScriptFlags::IsBackground);
g_engine->scheduler().run();
}
bool Room::updateInput() {
auto &player = g_engine->player();
auto &input = g_engine->input();
if (player.heldItem() != nullptr && !player.activeCharacter()->isBusy() && input.wasMouseRightPressed()) {
player.heldItem() = nullptr;
return false;
}
bool canInteract = !player.activeCharacter()->isBusy();
// A complicated network condition can prevent interaction at this point
if (g_engine->menu().isOpen() || !player.isGameLoaded())
canInteract = true;
if (canInteract) {
player.resetCursor();
if (!g_engine->globalUI().updateChangingCharacter() &&
!g_engine->globalUI().updateOpeningInventory()) {
updateInteraction();
player.updateCursor();
}
player.drawCursor();
}
if (player.currentRoom() == this) {
g_engine->globalUI().drawChangingButton();
g_engine->menu().updateOpeningMenu();
}
return player.currentRoom() == this;
}
void Room::updateInteraction() {
auto &player = g_engine->player();
auto &input = g_engine->input();
player.selectedObject() = world().globalRoom().getSelectedObject(getSelectedObject());
if (player.selectedObject() == nullptr) {
if (input.wasMouseLeftPressed() && _activeFloorI >= 0 &&
player.activeCharacter()->room() == this &&
player.pressedObject() == nullptr) {
player.activeCharacter()->walkToMouse();
g_engine->camera().setFollow(player.activeCharacter());
}
} else {
player.selectedObject()->markSelected();
if (input.wasAnyMousePressed())
player.pressedObject() = player.selectedObject();
}
}
void Room::updateRoomBounds() {
auto background = getObjectByName("Background");
auto graphic = background == nullptr ? nullptr : background->graphic();
if (graphic != nullptr) {
auto bgSize = graphic->animation().imageSize(0);
/* This fixes a bug where if the background image is invalid the original engine
* would not update the background size. This would be around 1024,768 due to
* previous rooms in the bug instances I found.
*/
if (bgSize == Point(0, 0))
bgSize = Point(1024, 768);
g_engine->camera().setRoomBounds(bgSize, graphic->scale());
}
}
void Room::updateObjects() {
const auto *previousRoom = g_engine->player().currentRoom();
for (auto *object : _objects) {
object->update();
if (g_engine->player().currentRoom() != previousRoom)
return;
}
}
void Room::drawObjects() {
for (auto *object : _objects) {
object->draw();
}
}
void Room::drawDebug() {
auto renderer = dynamic_cast<IDebugRenderer *>(&g_engine->renderer());
if (renderer == nullptr || !g_engine->console().isAnyDebugDrawingOn())
return;
for (auto *object : _objects) {
if (object->room() == g_engine->player().currentRoom())
object->drawDebug();
}
if (_activeFloorI < 0)
return;
auto &floor = _floors[_activeFloorI];
if (g_engine->console().showFloor())
renderer->debugShape(floor, kDebugBlue);
if (g_engine->console().showFloorEdges()) {
auto &camera = g_engine->camera();
for (uint polygonI = 0; polygonI < floor.polygonCount(); polygonI++) {
auto polygon = floor.at(polygonI);
for (uint pointI = 0; pointI < polygon._points.size(); pointI++) {
int32 targetI = floor.edgeTarget(polygonI, pointI);
if (targetI < 0)
continue;
Point a = camera.transform3Dto2D(polygon._points[pointI]);
Point b = camera.transform3Dto2D(polygon._points[(pointI + 1) % polygon._points.size()]);
Point source = (a + b) / 2;
Point target = camera.transform3Dto2D(floor.at((uint)targetI).midPoint());
renderer->debugPolyline(source, target, kDebugLightBlue);
}
}
}
}
void Room::loadResources() {
for (auto *object : _objects)
object->loadResources();
// this fixes some camera backups not working when closing the inventory
if (g_engine->player().currentRoom() == this)
updateRoomBounds();
}
void Room::freeResources() {
for (auto *object : _objects)
object->freeResources();
}
void Room::syncGame(Serializer &serializer) {
serializer.syncAsSByte(_activeFloorI);
for (auto *object : _objects)
object->syncGame(serializer);
}
void Room::toggleActiveFloor() {
_activeFloorI ^= 1;
}
ShapeObject *Room::getSelectedObject(ShapeObject *best) const {
for (auto object : _objects) {
auto shape = object->shape();
auto shapeObject = dynamic_cast<ShapeObject *>(object);
if (!object->isEnabled() ||
shape == nullptr || shapeObject == nullptr ||
object->room() != g_engine->player().currentRoom() || // e.g. a main character that is in another room
!shape->contains(g_engine->input().mousePos3D()))
continue;
if (best == nullptr || shapeObject->order() < best->order())
best = shapeObject;
}
return best;
}
OptionsMenu::OptionsMenu(World *world, SeekableReadStream &stream)
: Room(world, stream, true) {}
bool OptionsMenu::updateInput() {
if (!Room::updateInput())
return false;
auto currentSelectedObject = g_engine->player().selectedObject();
if (currentSelectedObject == nullptr) {
if (_lastSelectedObject == nullptr) {
if (_idleArm != nullptr)
_idleArm->toggle(true);
return true;
}
_lastSelectedObject->markSelected();
} else
_lastSelectedObject = currentSelectedObject;
if (_idleArm != nullptr)
_idleArm->toggle(false);
return true;
}
void OptionsMenu::loadResources() {
Room::loadResources();
_lastSelectedObject = nullptr;
_currentSlideButton = nullptr;
_idleArm = getObjectByName("Brazo");
}
void OptionsMenu::clearLastSelectedObject() {
_lastSelectedObject = nullptr;
}
ConnectMenu::ConnectMenu(World *world, SeekableReadStream &stream)
: Room(world, stream, true) {}
ListenMenu::ListenMenu(World *world, SeekableReadStream &stream)
: Room(world, stream, true) {}
Inventory::Inventory(World *world, SeekableReadStream &stream)
: Room(world, stream, true) {}
Inventory::~Inventory() {
// No need to delete items, they are room objects and thus deleted in Room::~Room
}
bool Inventory::updateInput() {
auto &player = g_engine->player();
auto &input = g_engine->input();
auto *hoveredItem = getHoveredItem();
if (!player.activeCharacter()->isBusy())
player.drawCursor(0);
if (hoveredItem != nullptr && !player.activeCharacter()->isBusy()) {
if ((input.wasMouseLeftPressed() && player.heldItem() == nullptr) ||
(input.wasMouseLeftReleased() && player.heldItem() != nullptr) ||
input.wasMouseRightReleased()) {
hoveredItem->trigger();
player.pressedObject() = nullptr;
}
g_engine->drawQueue().add<TextDrawRequest>(
g_engine->globalUI().generalFont(),
g_engine->world().getLocalizedName(hoveredItem->name()),
input.mousePos2D() + Point(0, -50),
-1, true, kWhite, -kForegroundOrderCount + 1);
}
const bool userWantsToCloseInventory =
closeInventoryTriggerBounds().contains(input.mousePos2D()) ||
input.wasMenuKeyPressed() ||
input.wasInventoryKeyPressed();
if (!player.activeCharacter()->isBusy() &&
userWantsToCloseInventory) {
player.changeRoomToBeforeInventory();
close();
}
if (!player.activeCharacter()->isBusy() &&
hoveredItem == nullptr &&
input.wasMouseRightReleased()) {
player.heldItem() = nullptr;
return false;
}
return player.currentRoom() == this;
}
Item *Inventory::getHoveredItem() {
auto mousePos = g_engine->input().mousePos2D();
for (auto item : _items) {
if (!item->isEnabled())
continue;
if (g_engine->player().heldItem() != nullptr &&
g_engine->player().heldItem()->name().equalsIgnoreCase(item->name()))
continue;
auto graphic = item->graphic();
assert(graphic != nullptr);
auto bounds = graphic->animation().frameBounds(0);
auto totalOffset = graphic->animation().totalFrameOffset(0);
auto delta = mousePos - graphic->topLeft() - totalOffset;
if (delta.x >= 0 && delta.y >= 0 && delta.x <= bounds.width() && delta.y <= bounds.height())
return item;
}
return nullptr;
}
void Inventory::initItems() {
auto &mortadelo = world().mortadelo();
auto &filemon = world().filemon();
for (auto object : _objects) {
auto item = dynamic_cast<Item *>(object);
if (item == nullptr)
continue;
_items.push_back(item);
mortadelo._items.push_back(new Item(*item));
filemon._items.push_back(new Item(*item));
}
}
void Inventory::updateItemsByActiveCharacter() {
auto *character = g_engine->player().activeCharacter();
assert(character != nullptr);
for (auto *item : _items)
item->toggle(character->hasItem(item->name()));
}
void Inventory::drawAsOverlay(int32 scrollY) {
for (auto object : _objects) {
auto graphic = object->graphic();
if (graphic == nullptr)
continue;
int16 oldY = graphic->topLeft().y;
int8 oldOrder = graphic->order();
graphic->topLeft().y += scrollY;
graphic->order() = -kForegroundOrderCount;
if (object->name().equalsIgnoreCase("Background"))
graphic->order()++;
object->draw();
graphic->topLeft().y = oldY;
graphic->order() = oldOrder;
}
}
void Inventory::open() {
g_engine->camera().backup(1);
g_engine->player().changeRoom(name(), true);
updateItemsByActiveCharacter();
}
void Inventory::close() {
g_engine->camera().restore(1);
g_engine->globalUI().startClosingInventory();
}
void Room::debugPrint(bool withObjects) const {
auto &console = g_engine->console();
console.debugPrintf(" %s\n", _name.c_str());
if (!withObjects)
return;
for (auto *object : _objects) {
console.debugPrintf("\t%20s %-32s %s\n",
object->typeName(),
object->name().c_str(),
object->isEnabled() ? "" : "disabled");
}
}
static constexpr const char *kMapFiles[] = {
"MAPAS/MAPA5.EMC",
"MAPAS/MAPA4.EMC",
"MAPAS/MAPA3.EMC",
"MAPAS/MAPA2.EMC",
"MAPAS/MAPA1.EMC",
"MAPAS/GLOBAL.EMC",
nullptr
};
World::World() {
for (auto *itMapFile = kMapFiles; *itMapFile != nullptr; itMapFile++) {
if (loadWorldFile(*itMapFile))
_loadedMapCount++;
}
loadLocalizedNames();
loadDialogLines();
_globalRoom = getRoomByName("GLOBAL");
if (_globalRoom == nullptr)
error("Could not find GLOBAL room");
_inventory = dynamic_cast<Inventory *>(getRoomByName("INVENTARIO"));
if (_inventory == nullptr)
error("Could not find INVENTARIO");
_filemon = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("FILEMON"));
if (_filemon == nullptr)
error("Could not find FILEMON");
_mortadelo = dynamic_cast<MainCharacter *>(_globalRoom->getObjectByName("MORTADELO"));
if (_mortadelo == nullptr)
error("Could not find MORTADELO");
_inventory->initItems();
}
World::~World() {
for (auto *room : _rooms)
delete room;
}
MainCharacter &World::getMainCharacterByKind(MainCharacterKind kind) const {
switch (kind) {
case MainCharacterKind::Mortadelo:
return *_mortadelo;
case MainCharacterKind::Filemon:
return *_filemon;
default:
error("Invalid character kind given to getMainCharacterByKind");
}
}
MainCharacter &World::getOtherMainCharacterByKind(MainCharacterKind kind) const {
switch (kind) {
case MainCharacterKind::Mortadelo:
return *_filemon;
case MainCharacterKind::Filemon:
return *_mortadelo;
default:
error("Invalid character kind given to getOtherMainCharacterByKind");
}
}
Room *World::getRoomByName(const char *name) const {
assert(name != nullptr);
if (*name == '\0')
return nullptr;
for (auto *room : _rooms) {
if (room->name().equalsIgnoreCase(name))
return room;
}
return nullptr;
}
ObjectBase *World::getObjectByName(const char *name) const {
ObjectBase *result = nullptr;
if (g_engine->player().currentRoom() != nullptr)
result = g_engine->player().currentRoom()->getObjectByName(name);
if (result == nullptr)
result = globalRoom().getObjectByName(name);
if (result == nullptr)
result = inventory().getObjectByName(name);
return result;
}
ObjectBase *World::getObjectByName(MainCharacterKind character, const char *name) const {
if (character == MainCharacterKind::None)
return getObjectByName(name);
const auto &player = g_engine->player();
ObjectBase *result = nullptr;
if (player.activeCharacterKind() == character && player.currentRoom() != player.activeCharacter()->room())
result = player.currentRoom()->getObjectByName(name);
if (result == nullptr)
result = player.activeCharacter()->room()->getObjectByName(name);
if (result == nullptr)
result = globalRoom().getObjectByName(name);
if (result == nullptr)
result = inventory().getObjectByName(name);
return result;
}
ObjectBase *World::getObjectByNameFromAnyRoom(const char *name) const {
assert(name != nullptr);
if (*name == '\0')
return nullptr;
for (auto *room : _rooms) {
ObjectBase *result = room->getObjectByName(name);
if (result != nullptr)
return result;
}
return nullptr;
}
void World::toggleObject(MainCharacterKind character, const char *objName, bool isEnabled) {
ObjectBase *object = getObjectByName(character, objName);
if (object == nullptr)
object = getObjectByNameFromAnyRoom(objName);
if (object == nullptr) // I would have liked an error for this, but original inconsistencies...
warning("Tried to toggle unknown object: %s", objName);
else
object->toggle(isEnabled);
}
const Common::String &World::getGlobalAnimationName(GlobalAnimationKind kind) const {
int kindI = (int)kind;
assert(kindI >= 0 && kindI < (int)GlobalAnimationKind::Count);
return _globalAnimationNames[kindI];
}
const char *World::getLocalizedName(const String &name) const {
const char *localizedName;
return _localizedNames.tryGetVal(name.c_str(), localizedName)
? localizedName
: name.c_str();
}
const char *World::getDialogLine(int32 dialogId) const {
if (dialogId < 0 || (uint)dialogId >= _dialogLines.size())
error("Invalid dialog line index %d", dialogId);
return _dialogLines[dialogId];
}
static Room *readRoom(World *world, SeekableReadStream &stream) {
const auto type = readVarString(stream);
if (type == Room::kClassName)
return new Room(world, stream);
else if (type == OptionsMenu::kClassName)
return new OptionsMenu(world, stream);
else if (type == ConnectMenu::kClassName)
return new ConnectMenu(world, stream);
else if (type == ListenMenu::kClassName)
return new ListenMenu(world, stream);
else if (type == Inventory::kClassName)
return new Inventory(world, stream);
else {
g_engine->game().unknownRoomType(type);
return nullptr;
}
}
bool World::loadWorldFile(const char *path) {
Common::File file;
if (!file.open(path)) {
// this is not necessarily an error, apparently the demos just have less
// chapter files. Being a demo is then also stored in some script vars
warning("Could not open world file %s\n", path);
return false;
}
// the first chunk seems to be debug symbols and/or info about the file structure
// it is ignored in the published game.
auto startOffset = file.readUint32LE();
file.seek(startOffset, SEEK_SET);
skipVarString(file); // some more unused strings related to development files?
skipVarString(file);
skipVarString(file);
skipVarString(file);
skipVarString(file);
skipVarString(file);
_initScriptName = readVarString(file);
skipVarString(file); // would be _updateScriptName, but it is never called
for (int i = 0; i < (int)GlobalAnimationKind::Count; i++)
_globalAnimationNames[i] = readVarString(file);
uint32 roomEnd = file.readUint32LE();
while (roomEnd > 0) {
Room *room = readRoom(this, file);
if (room != nullptr)
_rooms.push_back(room);
if (file.pos() < roomEnd) {
g_engine->game().notEnoughRoomDataRead(path, file.pos(), roomEnd);
file.seek(roomEnd, SEEK_SET);
} else if (file.pos() > roomEnd) // this surely is not recoverable
error("Read past the room data for world %s", path);
roomEnd = file.readUint32LE();
}
return true;
}
/**
* @brief Behold the incredible encryption of text files:
* - first 32 bytes are cipher
* - next byte is the XOR key
* - next 4 bytes are garbage
* - rest of the file is cipher
*/
static void loadEncryptedFile(const char *path, Array<char> &output) {
constexpr uint kHeaderSize = 32;
File file;
if (!file.open(path))
error("Could not open text file %s", path);
output.resize(file.size() - 4 - 1 + 1); // garbage bytes, key and we add a zero terminator for safety
if (file.read(output.data(), kHeaderSize) != kHeaderSize)
error("Could not read text file header");
char key = file.readSByte();
uint remainingSize = output.size() - kHeaderSize - 1;
if (!file.skip(4) || file.read(output.data() + kHeaderSize, remainingSize) != remainingSize)
error("Could not read text file body");
for (auto &ch : output)
ch ^= key;
output.back() = '\0'; // one for good measure and a zero-terminator
}
static char *trimLeading(char *start, char *end) {
while (start < end && isSpace(*start))
start++;
return start;
}
static char *skipWord(char *start, char *end) {
while (start < end && !isSpace(*start))
start++;
return start;
}
static char *trimTrailing(char *start, char *end) {
while (start < end && isSpace(end[-1]))
end--;
return end;
}
void World::loadLocalizedNames() {
loadEncryptedFile("Textos/OBJETOS.nkr", _namesChunk);
char *lineStart = _namesChunk.begin(), *fileEnd = _namesChunk.end() - 1;
while (lineStart < fileEnd) {
char *lineEnd = find(lineStart, fileEnd, '\n');
char *keyEnd = find(lineStart, lineEnd, '#');
if (keyEnd == lineStart || keyEnd == lineEnd || keyEnd + 1 == lineEnd)
error("Invalid localized name line separator");
char *valueEnd = trimTrailing(keyEnd + 1, lineEnd);
*keyEnd = 0;
*valueEnd = 0;
if (valueEnd == keyEnd + 1) {
// happens in the english version of Movie Adventure
warning("Empty localized name for %s", lineStart);
}
_localizedNames[lineStart] = keyEnd + 1;
lineStart = lineEnd + 1;
}
}
void World::loadDialogLines() {
/* This "encrypted" file contains lines in any of the following formats:
* Name 123, "This is the dialog line"\r\n
* Name 123, "This is the dialog line\r\n
* Name 123 This is the dialog line \r\n
*
* - The ID does not have to be correct, it is ignored by the original engine.
* - We only need the dialog line and insert null-terminators where appropriate.
*/
loadEncryptedFile("Textos/DIALOGOS.nkr", _dialogChunk);
char *lineStart = _dialogChunk.begin(), *fileEnd = _dialogChunk.end() - 1;
while (lineStart < fileEnd) {
char *lineEnd = find(lineStart, fileEnd, '\n');
char *cursor = trimLeading(lineStart, lineEnd); // space before the name
cursor = skipWord(cursor, lineEnd); // the name
cursor = trimLeading(cursor, lineEnd); // space between dialog id
cursor = skipWord(cursor, lineEnd); // the dialog id
cursor = trimLeading(cursor, lineEnd); // space between id and line
char *dialogLineEnd = trimTrailing(cursor, lineEnd);
if (*cursor == '\"')
cursor++;
if (dialogLineEnd > cursor && dialogLineEnd[-1] == '\"')
dialogLineEnd--;
if (cursor >= dialogLineEnd) {
if (cursor > dialogLineEnd)
g_engine->game().invalidDialogLine(_dialogLines.size());
cursor = lineStart; // store an empty string
dialogLineEnd = lineStart;
}
*dialogLineEnd = 0;
_dialogLines.push_back(cursor);
lineStart = lineEnd + 1;
}
}
void World::syncGame(Serializer &s) {
for (Room *room : _rooms)
room->syncGame(s);
}
}

214
engines/alcachofa/rooms.h Normal file
View File

@@ -0,0 +1,214 @@
/* 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 ALCACHOFA_ROOMS_H
#define ALCACHOFA_ROOMS_H
#include "alcachofa/objects.h"
namespace Alcachofa {
class World;
class Room {
public:
static constexpr const char *kClassName = "CHabitacion";
Room(World *world, Common::SeekableReadStream &stream);
virtual ~Room();
inline World &world() { return *_world; }
inline const Common::String &name() const { return _name; }
inline const PathFindingShape *activeFloor() const {
return _activeFloorI < 0 ? nullptr : &_floors[_activeFloorI];
}
inline int8 orderAt(Common::Point query) const {
return _activeFloorI < 0 ? 49 : activeFloor()->orderAt(query);
}
inline float depthAt(Common::Point query) const {
return _activeFloorI < 0 ? 1 : activeFloor()->depthAt(query);
}
inline uint8 characterAlphaTint() const { return _characterAlphaTint; }
inline uint8 characterAlphaPremultiplier() const { return _characterAlphaPremultiplier; }
inline bool fixedCameraOnEntering() const { return _fixedCameraOnEntering; }
inline int musicID() const { return _musicId; }
using ObjectIterator = Common::Array<ObjectBase *>::const_iterator;
inline ObjectIterator beginObjects() const { return _objects.begin(); }
inline ObjectIterator endObjects() const { return _objects.end(); }
void update();
void draw();
virtual bool updateInput();
virtual void loadResources();
virtual void freeResources();
virtual void syncGame(Common::Serializer &serializer);
ObjectBase *getObjectByName(const char *name) const;
void toggleActiveFloor();
void debugPrint(bool withObjects) const;
protected:
Room(World *world, Common::SeekableReadStream &stream, bool hasUselessByte);
void updateScripts();
void updateRoomBounds();
void updateInteraction();
void updateObjects();
void drawObjects();
void drawDebug();
ShapeObject *getSelectedObject(ShapeObject *best = nullptr) const;
World *_world;
Common::String _name;
PathFindingShape _floors[2];
bool _fixedCameraOnEntering;
int8 _activeFloorI = -1;
int _musicId = -1;
uint8
_characterAlphaTint,
_characterAlphaPremultiplier; ///< for some reason in percent instead of 0-255
Common::Array<ObjectBase *> _objects;
};
class OptionsMenu final : public Room {
public:
static constexpr const char *kClassName = "CHabitacionMenuOpciones";
OptionsMenu(World *world, Common::SeekableReadStream &stream);
bool updateInput() override;
void loadResources() override;
void clearLastSelectedObject(); // to reset arm animation
inline SlideButton *&currentSlideButton() { return _currentSlideButton; }
private:
ShapeObject *_lastSelectedObject = nullptr;
ObjectBase *_idleArm = nullptr;
SlideButton *_currentSlideButton = nullptr;
};
class ConnectMenu final : public Room {
public:
static constexpr const char *kClassName = "CHabitacionConectar";
ConnectMenu(World *world, Common::SeekableReadStream &stream);
};
class ListenMenu final : public Room {
public:
static constexpr const char *kClassName = "CHabitacionEsperar";
ListenMenu(World *world, Common::SeekableReadStream &stream);
};
class Inventory final : public Room {
public:
static constexpr const char *kClassName = "CInventario";
Inventory(World *world, Common::SeekableReadStream &stream);
~Inventory() override;
bool updateInput() override;
void initItems();
void updateItemsByActiveCharacter();
void drawAsOverlay(int32 scrollY);
void open();
void close();
private:
Item *getHoveredItem();
Common::Array<Item *> _items;
};
enum class GlobalAnimationKind {
GeneralFont = 0,
DialogFont,
Cursor,
MortadeloIcon,
FilemonIcon,
InventoryIcon,
MortadeloDisabledIcon, // only used for multiplayer
FilemonDisabledIcon,
InventoryDisabledIcon,
Count
};
class World final {
public:
World();
~World();
// reference-returning queries will error if the object does not exist
using RoomIterator = Common::Array<const Room *>::const_iterator;
inline RoomIterator beginRooms() const { return _rooms.begin(); }
inline RoomIterator endRooms() const { return _rooms.end(); }
inline Room &globalRoom() const { assert(_globalRoom != nullptr); return *_globalRoom; }
inline Inventory &inventory() const { assert(_inventory != nullptr); return *_inventory; }
inline MainCharacter &filemon() const { assert(_filemon != nullptr); return *_filemon; }
inline MainCharacter &mortadelo() const { assert(_mortadelo != nullptr); return *_mortadelo; }
inline const Common::String &initScriptName() const { return _initScriptName; }
inline uint8 loadedMapCount() const { return _loadedMapCount; }
inline bool somebodyUsing(ObjectBase *object) const {
return filemon().currentlyUsing() == object ||
mortadelo().currentlyUsing() == object;
}
MainCharacter &getMainCharacterByKind(MainCharacterKind kind) const;
MainCharacter &getOtherMainCharacterByKind(MainCharacterKind kind) const;
Room *getRoomByName(const char *name) const;
ObjectBase *getObjectByName(const char *name) const;
ObjectBase *getObjectByName(MainCharacterKind character, const char *name) const;
ObjectBase *getObjectByNameFromAnyRoom(const char *name) const;
const Common::String &getGlobalAnimationName(GlobalAnimationKind kind) const;
const char *getLocalizedName(const Common::String &name) const;
const char *getDialogLine(int32 dialogId) const;
void toggleObject(MainCharacterKind character, const char *objName, bool isEnabled);
void syncGame(Common::Serializer &s);
private:
bool loadWorldFile(const char *path);
void loadLocalizedNames();
void loadDialogLines();
// the default Hash<const char*> works on the characters, but the default EqualTo compares pointers...
struct StringEqualTo {
bool operator()(const char *a, const char *b) const { return strcmp(a, b) == 0; }
};
Common::Array<Room *> _rooms;
Common::String _globalAnimationNames[(int)GlobalAnimationKind::Count];
Common::String _initScriptName;
Room *_globalRoom;
Inventory *_inventory;
MainCharacter *_filemon, *_mortadelo;
uint8 _loadedMapCount = 0;
Common::HashMap<const char *, const char *,
Common::Hash<const char *>,
StringEqualTo> _localizedNames;
Common::Array<const char *> _dialogLines;
Common::Array<char> _namesChunk, _dialogChunk; ///< holds the memory for localizedNames / dialogLines
};
}
#endif // ALCACHOFA_ROOMS_H

View File

@@ -0,0 +1,351 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "alcachofa/scheduler.h"
#include "common/system.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/menu.h"
using namespace Common;
namespace Alcachofa {
TaskReturn::TaskReturn() {
_type = TaskReturnType::Yield;
_returnValue = 0;
_taskToWaitFor = nullptr;
}
TaskReturn TaskReturn::finish(int32 returnValue) {
TaskReturn r;
r._type = TaskReturnType::Finished;
r._returnValue = returnValue;
return r;
}
TaskReturn TaskReturn::waitFor(Task *task) {
assert(task != nullptr);
TaskReturn r;
r._type = TaskReturnType::Waiting;
r._taskToWaitFor = task;
return r;
}
Task::Task(Process &process) : _process(process) {}
Task *Task::delay(uint32 millis) {
return new DelayTask(process(), millis);
}
void Task::syncGame(Serializer &s) {
s.syncAsUint32LE(_stage);
}
void Task::syncObjectAsString(Serializer &s, ObjectBase *&object, bool optional) const {
String objectName, roomName;
if (object != nullptr) {
roomName = object->room()->name();
objectName = object->name();
}
s.syncString(roomName);
s.syncString(objectName);
if (s.isSaving())
return;
Room *room = g_engine->world().getRoomByName(roomName.c_str());
object = room == nullptr ? nullptr : room->getObjectByName(objectName.c_str());
if (object == nullptr) // main characters are not linked by the room they are in
object = g_engine->world().globalRoom().getObjectByName(objectName.c_str());
if (object == nullptr && !optional)
error("Invalid object name \"%s\" in room \"%s\" in savestate for task %s",
objectName.c_str(), roomName.c_str(), taskName());
}
void Task::errorForUnexpectedObjectType(const ObjectBase *base) const {
// Implemented as separate function in order to access ObjectBase methods
error("Unexpected type of object %s in savestate for task %s (got a %s)",
base->name().c_str(), taskName(), base->typeName());
}
Process::Process(ProcessId pid, MainCharacterKind characterKind)
: _pid(pid)
, _character(characterKind)
, _name("Unnamed process") {}
Process::Process(Serializer &s)
: _pid(0)
, _character(MainCharacterKind::None) {
syncGame(s);
}
Process::~Process() {
while (!_tasks.empty())
delete _tasks.pop();
}
bool Process::isActiveForPlayer() const {
return _character == MainCharacterKind::None || _character == g_engine->player().activeCharacterKind();
}
TaskReturnType Process::run() {
while (!_tasks.empty()) {
TaskReturn ret = _tasks.top()->run();
switch (ret.type()) {
case TaskReturnType::Yield:
return TaskReturnType::Yield;
case TaskReturnType::Waiting:
_tasks.push(ret.taskToWaitFor());
break;
case TaskReturnType::Finished:
_lastReturnValue = ret.returnValue();
delete _tasks.pop();
break;
default:
assert(false && "Invalid task return type");
return TaskReturnType::Finished;
}
}
return TaskReturnType::Finished;
}
void Process::debugPrint() {
auto *debugger = g_engine->getDebugger();
const char *characterName;
switch (_character) {
case MainCharacterKind::None:
characterName = " <none>";
break;
case MainCharacterKind::Filemon:
characterName = " Filemon";
break;
case MainCharacterKind::Mortadelo:
characterName = "Mortadelo";
break;
default:
characterName = "<invalid>";
break;
}
debugger->debugPrintf("pid: %3u char: %s ret: %2d \"%s\"\n", _pid, characterName, _lastReturnValue, _name.c_str());
for (uint i = 0; i < _tasks.size(); i++) {
debugger->debugPrintf(" %u: ", i);
_tasks[i]->debugPrint();
}
}
#define DEFINE_TASK(TaskName) \
extern Task *constructTask_##TaskName(Process &process, Serializer &s);
#include "alcachofa/tasks.h"
static Task *readTask(Process &process, Serializer &s) {
assert(s.isLoading());
String taskName;
s.syncString(taskName);
#define DEFINE_TASK(TaskName) \
if (taskName == #TaskName) \
return constructTask_##TaskName(process, s);
#include "alcachofa/tasks.h"
error("Invalid task type in savestate: %s", taskName.c_str());
}
void Process::syncGame(Serializer &s) {
s.syncAsUint32LE(_pid);
syncEnum(s, _character);
s.syncString(_name);
s.syncAsSint32LE(_lastReturnValue);
uint count = _tasks.size();
s.syncAsUint32LE(count);
if (s.isLoading()) {
assert(_tasks.empty());
for (uint i = 0; i < count; i++)
_tasks.push(readTask(*this, s));
} else {
String taskName;
for (uint i = 0; i < count; i++) {
taskName = _tasks[i]->taskName();
s.syncString(taskName);
_tasks[i]->syncGame(s);
}
}
}
static void killProcessesForIn(MainCharacterKind characterKind, Array<Process *> &processes, uint firstIndex) {
assert(firstIndex <= processes.size());
for (uint i = 0; i < processes.size() - firstIndex; i++) {
Process **process = &processes[processes.size() - 1 - i];
if ((*process)->character() == characterKind || characterKind == MainCharacterKind::None) {
delete *process;
processes.erase(process);
i--; // underflow is fine here
}
}
}
Scheduler::~Scheduler() {
killAllProcesses();
killProcessesForIn(MainCharacterKind::None, _backupProcesses, 0);
}
Process *Scheduler::createProcessInternal(MainCharacterKind character) {
Process *process = new Process(_nextPid++, character);
processesToRunNext().push_back(process);
return process;
}
void Scheduler::run() {
assert(processesToRun().empty()); // otherwise we somehow left normal flow
_currentArrayI = (_currentArrayI + 1) % 2;
// processesToRun() can be modified during loop so do not replace with iterators
for (_currentProcessI = 0; _currentProcessI < processesToRun().size(); _currentProcessI++) {
Process *process = processesToRun()[_currentProcessI];
auto ret = process->run();
if (ret == TaskReturnType::Finished)
delete process;
else
processesToRunNext().push_back(process);
}
processesToRun().clear();
_currentProcessI = UINT_MAX;
}
void Scheduler::backupContext() {
assert(processesToRun().empty());
_backupProcesses.push_back(processesToRunNext());
processesToRunNext().clear();
}
void Scheduler::restoreContext() {
assert(processesToRun().empty());
processesToRunNext().push_back(_backupProcesses);
_backupProcesses.clear();
}
void Scheduler::killAllProcesses() {
killProcessesForIn(MainCharacterKind::None, _processArrays[0], 0);
killProcessesForIn(MainCharacterKind::None, _processArrays[1], 0);
}
void Scheduler::killAllProcessesFor(MainCharacterKind characterKind) {
// this method can be called during run() so be careful
killProcessesForIn(characterKind, processesToRunNext(), 0);
killProcessesForIn(characterKind, processesToRun(), _currentProcessI == UINT_MAX ? 0 : _currentProcessI + 1);
}
static Process **getProcessByName(Array<Process *> &_processes, const String &name) {
for (auto &process : _processes) {
if (process->name() == name)
return &process;
}
return nullptr;
}
void Scheduler::killProcessByName(const String &name) {
Process **process = getProcessByName(processesToRunNext(), name);
if (process != nullptr) {
delete *process;
processesToRunNext().erase(process);
}
}
bool Scheduler::hasProcessWithName(const String &name) {
assert(processesToRun().empty());
return getProcessByName(processesToRunNext(), name) != nullptr;
}
void Scheduler::debugPrint() {
auto &console = g_engine->console();
bool didPrintSomething = false;
if (!processesToRun().empty()) {
console.debugPrintf("Currently running processes:\n");
for (uint32 i = 0; i < processesToRun().size(); i++) {
if (_currentProcessI == UINT_MAX || i > _currentProcessI)
console.debugPrintf(" ");
else if (i < _currentProcessI)
console.debugPrintf("# ");
else
console.debugPrintf("> ");
processesToRun()[i]->debugPrint();
}
didPrintSomething = true;
}
if (!processesToRunNext().empty()) {
if (didPrintSomething)
console.debugPrintf("\n");
console.debugPrintf("Scheduled processes:\n");
for (auto *process : processesToRunNext()) {
console.debugPrintf(" ");
process->debugPrint();
}
didPrintSomething = true;
}
if (!_backupProcesses.empty()) {
if (didPrintSomething)
console.debugPrintf("\n");
console.debugPrintf("Backed up processes:\n");
for (auto *process : _backupProcesses) {
console.debugPrintf(" ");
process->debugPrint();
}
didPrintSomething = true;
}
if (!didPrintSomething)
console.debugPrintf("No processes running or backed up\n");
}
void Scheduler::prepareSyncGame(Serializer &s) {
if (s.isLoading()) {
killAllProcesses();
killProcessesForIn(MainCharacterKind::None, _backupProcesses, 0);
}
}
void Scheduler::syncGame(Serializer &s) {
assert(_currentProcessI == UINT_MAX); // let's not sync during ::run
assert(s.isSaving() || _backupProcesses.empty());
Common::Array<Process *> *processes = s.isSaving() && g_engine->menu().isOpen()
? &_backupProcesses
: &processesToRunNext();
// we only sync the backupProcesses as these are the ones pertaining to the gameplay
// the other arrays would be used for the menu
s.syncAsUint32LE(_nextPid);
uint32 count = processes->size();
s.syncAsUint32LE(count);
if (s.isLoading()) {
processes->reserve(count);
for (uint32 i = 0; i < count; i++)
processes->push_back(new Process(s));
} else {
for (Process *process : *processes)
process->syncGame(s);
}
}
}

View File

@@ -0,0 +1,227 @@
/* 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 ALCACHOFA_SCHEDULER_H
#define ALCACHOFA_SCHEDULER_H
#include "alcachofa/common.h"
#include "common/stack.h"
#include "common/str.h"
#include "common/type_traits.h"
namespace Alcachofa {
/* Tasks are generally written as coroutines however the common coroutines
* cannot be used for two reasons:
* 1. The scheduler is too limited in managing when to run what coroutines
* E.g. for the inventory/menu we need to pause a set of coroutines and
* continue them later on
* 2. We need to save and load the state of coroutines
* For this we either write the state machine ourselves or we use
* the following careful macros where the state ID is explicitly written
* This way it is stable and if it has to change we can migrate
* savestates upon loading.
*
* Tasks are usually private, so in order to load them they:
* - need a constructor MyPrivateTask(Process &, Serializer &)
* - need call the macro DECLARE_TASK(MyPrivateTask)
* - they have to listed in tasks.h
*/
struct Task;
class Process;
class ObjectBase;
enum class TaskReturnType {
Yield,
Finished,
Waiting
};
struct TaskReturn {
static inline TaskReturn yield() { return {}; }
static TaskReturn finish(int32 returnValue);
static TaskReturn waitFor(Task *task);
inline TaskReturnType type() const { return _type; }
inline int32 returnValue() const {
assert(_type == TaskReturnType::Finished);
return _returnValue;
}
inline Task *taskToWaitFor() const {
assert(_type == TaskReturnType::Waiting);
return _taskToWaitFor;
}
private:
TaskReturn();
TaskReturnType _type;
union {
int32 _returnValue;
Task *_taskToWaitFor;
};
};
struct Task {
Task(Process &process);
virtual ~Task() {}
virtual TaskReturn run() = 0;
virtual void debugPrint() = 0;
virtual void syncGame(Common::Serializer &s);
virtual const char *taskName() const = 0; // implemented by DECLARE_TASK
inline Process &process() const { return _process; }
protected:
Task *delay(uint32 millis);
void syncObjectAsString(Common::Serializer &s, ObjectBase *&object, bool optional = false) const;
template<class TObject>
void syncObjectAsString(Common::Serializer &s, TObject *&object, bool optional = false) const {
// We could add is_const and therefore true_type, false_type, integral_constant
// or we could just use const_cast and promise that we won't modify the object itself
ObjectBase *base = const_cast<Common::remove_const_t<TObject> *>(object);
syncObjectAsString(s, base, optional);
object = dynamic_cast<TObject *>(base);
if (object == nullptr && base != nullptr)
errorForUnexpectedObjectType(base);
}
uint32 _stage = 0;
private:
void errorForUnexpectedObjectType(const ObjectBase *base) const;
Process &_process;
};
// implemented in alcachofa.cpp to prevent a compiler warning when
// the declaration of the construct function comes after the definition
struct DelayTask final : public Task {
DelayTask(Process &process, uint32 millis);
DelayTask(Process &process, Common::Serializer &s);
TaskReturn run() override;
void debugPrint() override;
void syncGame(Common::Serializer &s) override;
const char *taskName() const override;
private:
uint32 _endTime = 0;
};
#define DECLARE_TASK(TaskName) \
extern Task *constructTask_##TaskName(Process &process, Serializer &s) { \
return new TaskName(process, s); \
} \
const char *TaskName::taskName() const { \
return #TaskName; \
}
#define TASK_BEGIN \
switch(_stage) { \
case 0:; \
#define TASK_END \
TASK_RETURN(0); \
default: assert(false && "Invalid line in task"); \
} return TaskReturn::finish(0)
#define TASK_INTERNAL_BREAK(stage, ret) \
do { \
_stage = stage; \
return ret; \
case stage:; \
} while(0)
#define TASK_YIELD(stage) TASK_INTERNAL_BREAK((stage), TaskReturn::yield())
#define TASK_WAIT(stage, task) TASK_INTERNAL_BREAK((stage), TaskReturn::waitFor(task))
#define TASK_RETURN(value) \
do { \
_stage = UINT_MAX; \
return TaskReturn::finish(value); \
} while(0)
using ProcessId = uint32;
class Process {
public:
Process(ProcessId pid, MainCharacterKind characterKind);
Process(Common::Serializer &s);
~Process();
inline ProcessId pid() const { return _pid; }
inline MainCharacterKind &character() { return _character; } // is changed in changeCharacter
inline MainCharacterKind character() const { return _character; }
inline int32 returnValue() const { return _lastReturnValue; }
inline Common::String &name() { return _name; }
bool isActiveForPlayer() const; ///< and thus should e.g. draw subtitles or effects
TaskReturnType run();
void debugPrint();
void syncGame(Common::Serializer &s);
private:
friend class Scheduler;
ProcessId _pid;
MainCharacterKind _character;
Common::Stack<Task *> _tasks;
Common::String _name;
int32 _lastReturnValue = 0;
};
class Scheduler {
public:
~Scheduler();
void run();
void backupContext();
void restoreContext();
void killAllProcesses();
void killAllProcessesFor(MainCharacterKind characterKind);
void killProcessByName(const Common::String &name);
bool hasProcessWithName(const Common::String &name);
void debugPrint();
void prepareSyncGame(Common::Serializer &s);
void syncGame(Common::Serializer &s);
template<typename TTask, typename... TaskArgs>
Process *createProcess(MainCharacterKind character, TaskArgs&&... args) {
Process *process = createProcessInternal(character);
process->_tasks.push(new TTask(*process, Common::forward<TaskArgs>(args)...));
return process;
}
private:
Process *createProcessInternal(MainCharacterKind character);
inline Common::Array<Process *> &processesToRun() { return _processArrays[_currentArrayI]; }
inline Common::Array<Process *> &processesToRunNext() { return _processArrays[!_currentArrayI]; }
Common::Array<Process *> _processArrays[2];
Common::Array<Process *> _backupProcesses;
uint8 _currentArrayI = 0;
ProcessId _nextPid = 1;
uint _currentProcessI = UINT_MAX;
};
}
#endif // ALCACHOFA_SCHEDULER_H

View File

@@ -0,0 +1,118 @@
/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef ALCACHOFA_SCRIPT_DEBUG_H
#define ALCACHOFA_SCRIPT_DEBUG_H
namespace Alcachofa {
static const char *const ScriptOpNames[] = {
"Nop",
"Dup",
"PushAddr",
"PushValue",
"Deref",
"PopN",
"Store",
"LoadString",
"ScriptCall",
"KernelCall",
"JumpIfFalse",
"JumpIfTrue",
"Jump",
"Negate",
"BooleanNot",
"Mul",
"Add",
"Sub",
"Less",
"Greater",
"LessEquals",
"GreaterEquals",
"Equals",
"NotEquals",
"BitAnd",
"BitOr",
"ReturnValue",
"ReturnVoid",
"Crash"
};
static const char *const KernelCallNames[] = {
"Nop",
"PlayVideo",
"PlaySound",
"PlayMusic",
"StopMusic",
"WaitForMusicToEnd",
"ShowCenterBottomText",
"StopAndTurn",
"StopAndTurnMe",
"ChangeCharacter",
"SayText",
"Go",
"Put",
"ChangeCharacterRoom",
"KillProcesses",
"LerpCharacterLodBias",
"On",
"Off",
"Pickup",
"CharacterPickup",
"Drop",
"CharacterDrop",
"Delay",
"HadNoMousePressFor",
"Fork",
"Animate",
"AnimateCharacter",
"AnimateTalking",
"ChangeRoom",
"ToggleRoomFloor",
"SetDialogLineReturn",
"DialogMenu",
"ClearInventory",
"FadeType0",
"FadeType1",
"LerpWorldLodBias",
"FadeType2",
"SetActiveTextureSet",
"SetMaxCamSpeedFactor",
"WaitCamStopping",
"CamFollow",
"CamShake",
"LerpCamXY",
"LerpCamZ",
"LerpCamScale",
"LerpCamToObjectWithScale",
"LerpCamToObjectResettingZ",
"LerpCamRotation",
"FadeIn",
"FadeOut",
"FadeIn2",
"FadeOut2",
"LerpCamXYZ",
"LerpCamToObjectKeepingZ"
};
}
#endif // ALCACHOFA_SCRIPT_DEBUG_H

1007
engines/alcachofa/script.cpp Normal file

File diff suppressed because it is too large Load Diff

191
engines/alcachofa/script.h Normal file
View File

@@ -0,0 +1,191 @@
/* 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 ALCACHOFA_SCRIPT_H
#define ALCACHOFA_SCRIPT_H
#include "alcachofa/common.h"
#include "common/hashmap.h"
#include "common/span.h"
#include "common/stream.h"
#include "common/system.h"
namespace Alcachofa {
class Process;
// the ScriptOp and ScriptKernelTask enums represent the *implemented* order
// the specific Game instance maps the version-specific op codes to our order
enum class ScriptOp {
Nop,
Dup,
PushAddr,
PushDynAddr,
PushValue,
Deref,
Pop1,
PopN,
Store,
LoadString,
ScriptCall,
KernelCall,
JumpIfFalse,
JumpIfTrue,
Jump,
Negate,
BooleanNot,
Mul,
Add,
Sub,
Less,
Greater,
LessEquals,
GreaterEquals,
Equals,
NotEquals,
BitAnd,
BitOr,
ReturnValue,
ReturnVoid,
Crash
};
enum class ScriptKernelTask {
Nop = 0,
PlayVideo,
PlaySound,
PlayMusic,
StopMusic,
WaitForMusicToEnd,
ShowCenterBottomText,
StopAndTurn,
StopAndTurnMe,
ChangeCharacter,
SayText,
Go,
Put,
ChangeCharacterRoom,
KillProcesses,
LerpCharacterLodBias,
On,
Off,
Pickup,
CharacterPickup,
Drop,
CharacterDrop,
Delay,
HadNoMousePressFor,
Fork,
Animate,
AnimateCharacter,
AnimateTalking,
ChangeRoom,
ToggleRoomFloor,
SetDialogLineReturn,
DialogMenu,
ClearInventory,
FadeType0,
FadeType1,
LerpWorldLodBias,
FadeType2,
SetActiveTextureSet,
SetMaxCamSpeedFactor,
WaitCamStopping,
CamFollow,
CamShake,
LerpCamXY,
LerpCamZ,
LerpCamScale,
LerpCamToObjectWithScale,
LerpCamToObjectResettingZ,
LerpCamRotation,
FadeIn,
FadeOut,
FadeIn2,
FadeOut2,
LerpCamXYZ,
LerpCamToObjectKeepingZ,
SheriffTakesCharacter, ///< some special-case V1 tasks, unknown yet
ChangeDoor,
Disguise
};
enum class ScriptFlags {
None = 0,
AllowMissing = (1 << 0),
IsBackground = (1 << 1)
};
inline ScriptFlags operator | (ScriptFlags a, ScriptFlags b) {
return (ScriptFlags)(((uint)a) | ((uint)b));
}
inline bool operator & (ScriptFlags a, ScriptFlags b) {
return ((uint)a) & ((uint)b);
}
struct ScriptInstruction {
ScriptInstruction(Common::ReadStream &stream);
int32 _op; ///< int32 because it still has to be mapped using a game-specific translation table
int32 _arg;
};
class Script {
public:
Script();
void syncGame(Common::Serializer &s);
int32 variable(const char *name) const;
int32 &variable(const char *name);
Process *createProcess(
MainCharacterKind character,
const Common::String &procedure,
ScriptFlags flags = ScriptFlags::None);
Process *createProcess(
MainCharacterKind character,
const Common::String &behavior,
const Common::String &action,
ScriptFlags flags = ScriptFlags::None);
bool hasProcedure(const Common::String &behavior, const Common::String &action) const;
bool hasProcedure(const Common::String &procedure) const;
using VariableNameIterator = Common::HashMap<Common::String, uint32>::const_iterator;
inline VariableNameIterator beginVariables() const { return _variableNames.begin(); }
inline VariableNameIterator endVariables() const { return _variableNames.end(); }
inline bool hasVariable(const char *name) const { return _variableNames.contains(name); }
void setScriptTimer(bool reset);
private:
friend struct ScriptTask;
friend struct ScriptTimerTask;
Common::HashMap<Common::String, uint32> _variableNames;
Common::HashMap<Common::String, uint32> _procedures;
Common::Array<ScriptInstruction> _instructions;
Common::Array<int32> _variables;
Common::SpanOwner<Common::Span<char>> _strings;
uint32 _scriptTimer = 0;
};
}
#endif // ALCACHOFA_SCRIPT_H

679
engines/alcachofa/shape.cpp Normal file
View File

@@ -0,0 +1,679 @@
/* 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 "alcachofa/shape.h"
using namespace Common;
using namespace Math;
namespace Alcachofa {
static int sideOfLine(Point a, Point b, Point q) {
return (b.x - a.x) * (q.y - a.y) - (b.y - a.y) * (q.x - a.x);
}
static bool lineIntersects(Point a1, Point b1, Point a2, Point b2) {
return (sideOfLine(a1, b1, a2) > 0) != (sideOfLine(a1, b1, b2) > 0);
}
static bool segmentsIntersect(Point a1, Point b1, Point a2, Point b2) {
// as there are a number of special cases to consider,
// this method is a direct translation of the original engine
// rather than using common Math:: code
if (a2.x > b2.x) {
if (a1.x > b1.x)
return lineIntersects(b1, a1, b2, a2) && lineIntersects(b2, a2, b1, a1); //-V764
else
return lineIntersects(a1, b1, b2, a2) && lineIntersects(b2, a2, a1, b1); //-V764
} else {
if (a1.x > b1.x)
return lineIntersects(b1, a1, a2, b2) && lineIntersects(a2, b2, b1, a1); //-V764
else
return lineIntersects(a1, b1, a2, b2) && lineIntersects(a2, b2, a1, b1);
}
}
EdgeDistances::EdgeDistances(Point edgeA, Point edgeB, Point query) {
Vector2d
a = as2D(edgeA),
b = as2D(edgeB),
q = as2D(query);
float edgeLength = a.getDistanceTo(b);
Vector2d edgeDir = (b - a) / edgeLength;
Vector2d edgeNormal(-edgeDir.getY(), edgeDir.getX());
_edgeLength = edgeLength;
_onEdge = edgeDir.dotProduct(q - a);
_toEdge = abs(edgeNormal.dotProduct(q) - edgeNormal.dotProduct(a));
}
bool Polygon::contains(Point query) const {
switch (_points.size()) {
case 0:
return false;
case 1:
return query == _points[0];
case 2:
return edgeDistances(0, query)._toEdge < 2.0f;
default:
// we assume that the polygon is convex
for (uint i = 1; i < _points.size(); i++) {
if (sideOfLine(_points[i - 1], _points[i], query) < 0)
return false;
}
return sideOfLine(_points[_points.size() - 1], _points[0], query) >= 0;
}
}
bool Polygon::intersectsEdge(uint startPointI, Point a, Point b) const {
assert(startPointI < _points.size());
uint endPointI = (startPointI + 1) % _points.size();
return segmentsIntersect(_points[startPointI], _points[endPointI], a, b);
}
EdgeDistances Polygon::edgeDistances(uint startPointI, Point query) const {
assert(startPointI < _points.size());
uint endPointI = startPointI + 1 == _points.size() ? 0 : startPointI + 1;
return EdgeDistances(_points[startPointI], _points[endPointI], query);
}
static Point wiggleOnToLine(Point a, Point b, Point q) {
// due to rounding errors contains(bestPoint) might be false for on-edge closest points, let's fix that
// maybe there is a more mathematical solution to this, but it suffices for now
if (sideOfLine(a, b, q) >= 0) return q;
if (sideOfLine(a, b, q + Point(+1, 0)) >= 0) return q + Point(+1, 0);
if (sideOfLine(a, b, q + Point(-1, 0)) >= 0) return q + Point(-1, 0);
if (sideOfLine(a, b, q + Point(0, +1)) >= 0) return q + Point(0, +1);
if (sideOfLine(a, b, q + Point(0, -1)) >= 0) return q + Point(0, -1);
assert(false && "More than two pixels means some more serious math error occured");
return q;
}
Point Polygon::closestPointTo(Point query, float &distanceSqr) const {
assert(_points.size() > 0);
Common::Point bestPoint = {};
distanceSqr = std::numeric_limits<float>::infinity();
for (uint i = 0; i < _points.size(); i++) {
auto edgeDists = edgeDistances(i, query);
if (edgeDists._onEdge < 0.0f) {
float pointDistSqr = as2D(query - _points[i]).getSquareMagnitude();
if (pointDistSqr < distanceSqr) {
bestPoint = _points[i];
distanceSqr = pointDistSqr;
}
}
if (edgeDists._onEdge >= 0.0f && edgeDists._onEdge <= edgeDists._edgeLength) {
float edgeDistSqr = powf(edgeDists._toEdge, 2.0f);
if (edgeDistSqr < distanceSqr) {
distanceSqr = edgeDistSqr;
uint j = (i + 1) % _points.size();
bestPoint = _points[i] + (_points[j] - _points[i]) * (edgeDists._onEdge / edgeDists._edgeLength);
bestPoint = wiggleOnToLine(_points[i], _points[j], bestPoint);
}
}
}
return bestPoint;
}
Point Polygon::midPoint() const {
assert(_points.size() > 0);
Common::Point sum = {};
for (uint i = 0; i < _points.size(); i++)
sum += _points[i];
return sum / (int16)_points.size();
}
static float depthAtForLine(Point a, Point b, Point q, int8 depthA, int8 depthB) {
return (sqrtf(a.sqrDist(q)) / a.sqrDist(b) * depthB + depthA) * 0.01f;
}
static float depthAtForConvex(const PathFindingPolygon &p, Point q) {
float sumDepths = 0, sumDistances = 0;
for (uint i = 0; i < p._points.size(); i++) {
uint j = i + 1 == p._points.size() ? 0 : i + 1;
auto distances = p.edgeDistances(i, q);
float depthOnEdge = p._pointDepths[i] + distances._onEdge * (p._pointDepths[j] - p._pointDepths[i]) / distances._edgeLength;
if (distances._toEdge < epsilon) // q is directly on the edge
return depthOnEdge * 0.01f;
sumDepths += 1 / distances._toEdge * depthOnEdge;
sumDistances += 1 / distances._toEdge;
}
return sumDistances < epsilon ? 0
: sumDepths / sumDistances * 0.01f;
}
float PathFindingPolygon::depthAt(Point query) const {
switch (_points.size()) {
case 0:
case 1:
return 1.0f;
case 2:
return depthAtForLine(_points[0], _points[1], query, _pointDepths[0], _pointDepths[1]);
default:
return depthAtForConvex(*this, query);
}
}
uint PathFindingPolygon::findSharedPoints(
const PathFindingPolygon &other,
Common::Span<SharedPoint> sharedPoints) const {
uint count = 0;
for (uint outerI = 0; outerI < _points.size(); outerI++) {
for (uint innerI = 0; innerI < other._points.size(); innerI++) {
if (_points[outerI] == other._points[innerI]) {
assert(count < sharedPoints.size());
sharedPoints[count++] = { outerI, innerI };
}
}
}
return count;
}
static Color colorAtForLine(Point a, Point b, Point q, Color colorA, Color colorB) {
// I highly suspect RGB calculation being very bugged, so for now I just ignore and only calc alpha
float phase = sqrtf(q.sqrDist(a)) / a.sqrDist(b);
colorA.a += phase * colorB.a;
return colorA;
}
static Color colorAtForConvex(const FloorColorPolygon &p, Point query) {
// This is a quite literal translation of the original engine
// There may very well be a better way than this...
float weights[FloorColorShape::kPointsPerPolygon];
fill(weights, weights + FloorColorShape::kPointsPerPolygon, 0.0f);
for (uint i = 0; i < p._points.size(); i++) {
EdgeDistances distances = p.edgeDistances(i, query);
float edgeWeight = distances._toEdge * ABS(distances._onEdge) / distances._edgeLength;
if (distances._edgeLength > 1) {
weights[i] += edgeWeight;
weights[i + 1 == p._points.size() ? 0 : i + 1] += edgeWeight;
}
}
float weightSum = 0;
for (uint i = 0; i < p._points.size(); i++)
weightSum += weights[i];
for (uint i = 0; i < p._points.size(); i++) {
if (weights[i] < epsilon)
return p._pointColors[i];
weights[i] = weightSum / weights[i];
}
weightSum = 0;
for (uint i = 0; i < p._points.size(); i++)
weightSum += weights[i];
for (uint i = 0; i < p._points.size(); i++)
weights[i] /= weightSum;
float r = 0, g = 0, b = 0, a = 0.5f;
for (uint i = 0; i < p._points.size(); i++) {
r += p._pointColors[i].r * weights[i];
g += p._pointColors[i].g * weights[i];
b += p._pointColors[i].b * weights[i];
a += p._pointColors[i].a * weights[i];
}
return {
(byte)MIN(255, MAX(0, (int)r)),
(byte)MIN(255, MAX(0, (int)g)),
(byte)MIN(255, MAX(0, (int)b)),
(byte)MIN(255, MAX(0, (int)a)),
};
}
Color FloorColorPolygon::colorAt(Point query) const {
switch (_points.size()) {
case 0:
return kWhite;
case 1:
return { 255, 255, 255, _pointColors[0].a };
case 2:
return colorAtForLine(_points[0], _points[1], query, _pointColors[0], _pointColors[1]);
default:
return colorAtForConvex(*this, query);
}
}
Shape::Shape() {}
Shape::Shape(ReadStream &stream) {
byte complexity = stream.readByte();
uint8 pointsPerPolygon;
if (complexity > 3)
error("Invalid shape complexity %d", complexity);
else if (complexity == 3)
pointsPerPolygon = 0; // read in per polygon
else
pointsPerPolygon = 1 << complexity;
int polygonCount = stream.readUint16LE();
_polygons.reserve(polygonCount);
_points.reserve(MIN(3, (int)pointsPerPolygon) * polygonCount);
for (int i = 0; i < polygonCount; i++) {
auto pointCount = pointsPerPolygon == 0
? stream.readByte()
: pointsPerPolygon;
for (int j = 0; j < pointCount; j++)
_points.push_back(readPoint(stream));
addPolygon(pointCount);
}
}
uint Shape::addPolygon(uint maxCount) {
// Common functionality of shapes is that polygons are reduced
// so that the first point is not duplicated
uint firstI = empty() ? 0 : _polygons.back().first + _polygons.back().second;
uint newCount = maxCount;
if (maxCount > 1) {
for (newCount = 1; newCount < maxCount; newCount++) {
if (_points[firstI + newCount] == _points[firstI])
break;
}
_points.resize(firstI + newCount);
}
_polygons.push_back({ firstI, newCount });
return newCount;
}
Polygon Shape::at(uint index) const {
auto range = _polygons[index];
Polygon p;
p._index = index;
p._points = Span<const Point>(_points.data() + range.first, range.second);
return p;
}
int32 Shape::polygonContaining(Point query) const {
for (uint i = 0; i < _polygons.size(); i++) {
if (at(i).contains(query))
return (int32)i;
}
return -1;
}
bool Shape::contains(Point query) const {
return polygonContaining(query) >= 0;
}
Point Shape::closestPointTo(Point query, int32 &polygonI) const {
assert(_polygons.size() > 0);
float bestDistanceSqr = std::numeric_limits<float>::infinity();
Point bestPoint = {};
for (uint i = 0; i < _polygons.size(); i++) {
float curDistanceSqr = std::numeric_limits<float>::infinity();
Point curPoint = at(i).closestPointTo(query, curDistanceSqr);
if (curDistanceSqr < bestDistanceSqr) {
bestDistanceSqr = curDistanceSqr;
bestPoint = curPoint;
polygonI = (int32)i;
}
}
return bestPoint;
}
void Shape::setAsRectangle(const Rect &rect) {
_polygons.resize(1);
_polygons[0] = { 0, 4 };
_points.resize(4);
_points[0] = { rect.left, rect.top };
_points[1] = { rect.right, rect.top };
_points[2] = { rect.right, rect.bottom };
_points[3] = { rect.left, rect.bottom };
}
PathFindingShape::PathFindingShape() {}
PathFindingShape::PathFindingShape(ReadStream &stream) {
auto polygonCount = stream.readUint16LE();
_polygons.reserve(polygonCount);
_polygonOrders.reserve(polygonCount);
_points.reserve(polygonCount * kPointsPerPolygon);
_pointDepths.reserve(polygonCount * kPointsPerPolygon);
for (int i = 0; i < polygonCount; i++) {
for (uint j = 0; j < kPointsPerPolygon; j++)
_points.push_back(readPoint(stream));
_polygonOrders.push_back(stream.readSByte());
for (uint j = 0; j < kPointsPerPolygon; j++)
_pointDepths.push_back(stream.readByte());
uint pointCount = addPolygon(kPointsPerPolygon);
assert(pointCount <= kPointsPerPolygon);
_pointDepths.resize(_points.size());
}
setupLinks();
initializeFloydWarshall();
calculateFloydWarshall();
}
PathFindingPolygon PathFindingShape::at(uint index) const {
auto range = _polygons[index];
PathFindingPolygon p;
p._index = index;
p._points = Span<const Point>(_points.data() + range.first, range.second);
p._pointDepths = Span<const uint8>(_pointDepths.data() + range.first, range.second);
p._order = _polygonOrders[index];
return p;
}
int8 PathFindingShape::orderAt(Point query) const {
int32 polygon = polygonContaining(query);
return polygon < 0 ? 49 : _polygonOrders[polygon];
}
float PathFindingShape::depthAt(Point query) const {
int32 polygon = polygonContaining(query);
return polygon < 0 ? 1.0f : at(polygon).depthAt(query);
}
PathFindingShape::LinkPolygonIndices::LinkPolygonIndices() {
Common::fill(_points, _points + kPointsPerPolygon, LinkIndex(-1, -1));
}
static Pair<int32, int32> orderPoints(const Polygon &polygon, int32 point1, int32 point2) {
if (point1 > point2) {
int32 tmp = point1;
point1 = point2;
point2 = tmp;
}
const int32 maxPointI = polygon._points.size() - 1;
if (point1 == 0 && point2 == maxPointI)
return { maxPointI, 0 };
return { point1, point2 };
}
void PathFindingShape::setupLinks() {
// just a heuristic, each polygon will be attached to at least one other
_linkPoints.reserve(polygonCount() * 3);
_linkIndices.resize(polygonCount());
_targetQuads.resize(polygonCount() * kPointsPerPolygon);
Common::fill(_targetQuads.begin(), _targetQuads.end(), -1);
Pair<uint, uint> sharedPoints[2];
for (uint outerI = 0; outerI < polygonCount(); outerI++) {
const auto outer = at(outerI);
for (uint innerI = outerI + 1; innerI < polygonCount(); innerI++) {
const auto inner = at(innerI);
uint sharedPointCount = outer.findSharedPoints(inner, { sharedPoints, 2 });
if (sharedPointCount > 0)
setupLinkPoint(outer, inner, sharedPoints[0]);
if (sharedPointCount > 1) {
auto outerPoints = orderPoints(outer, sharedPoints[0].first, sharedPoints[1].first);
auto innerPoints = orderPoints(inner, sharedPoints[0].second, sharedPoints[1].second);
setupLinkEdge(outer, inner, outerPoints, innerPoints);
setupLinkPoint(outer, inner, sharedPoints[1]);
}
}
}
}
void PathFindingShape::setupLinkPoint(
const PathFindingPolygon &outer,
const PathFindingPolygon &inner,
PathFindingPolygon::SharedPoint pointI) {
auto &outerLink = _linkIndices[outer._index]._points[pointI.first];
auto &innerLink = _linkIndices[inner._index]._points[pointI.second];
if (outerLink.first < 0) {
outerLink.first = _linkPoints.size();
_linkPoints.push_back(outer._points[pointI.first]);
}
innerLink.first = outerLink.first;
}
void PathFindingShape::setupLinkEdge(
const PathFindingPolygon &outer,
const PathFindingPolygon &inner,
PathFindingShape::LinkIndex outerP,
PathFindingShape::LinkIndex innerP) {
_targetQuads[outer._index * kPointsPerPolygon + outerP.first] = inner._index;
_targetQuads[inner._index * kPointsPerPolygon + innerP.first] = outer._index;
auto &outerLink = _linkIndices[outer._index]._points[outerP.first];
auto &innerLink = _linkIndices[inner._index]._points[innerP.first];
if (outerLink.second < 0) {
outerLink.second = _linkPoints.size();
_linkPoints.push_back((outer._points[outerP.first] + outer._points[outerP.second]) / 2);
}
innerLink.second = outerLink.second;
}
void PathFindingShape::initializeFloydWarshall() {
_distanceMatrix.resize(_linkPoints.size() * _linkPoints.size());
_previousTarget.resize(_linkPoints.size() * _linkPoints.size());
Common::fill(_distanceMatrix.begin(), _distanceMatrix.end(), UINT_MAX);
Common::fill(_previousTarget.begin(), _previousTarget.end(), -1);
// every linkpoint is the shortest path to itself
for (uint i = 0; i < _linkPoints.size(); i++) {
_distanceMatrix[i * _linkPoints.size() + i] = 0;
_previousTarget[i * _linkPoints.size() + i] = i;
}
// every linkpoint to linkpoint within the same polygon *is* the shortest path
// between them. Therefore these are our initial paths for Floyd-Warshall
for (const auto &linkPolygon : _linkIndices) {
for (uint i = 0; i < 2 * kPointsPerPolygon; i++) {
LinkIndex linkFrom = linkPolygon._points[i / 2];
int32 linkFromI = (i % 2) ? linkFrom.second : linkFrom.first;
if (linkFromI < 0)
continue;
for (uint j = i + 1; j < 2 * kPointsPerPolygon; j++) {
LinkIndex linkTo = linkPolygon._points[j / 2];
int32 linkToI = (j % 2) ? linkTo.second : linkTo.first;
if (linkToI >= 0) {
const int32 linkFromFullI = linkFromI * _linkPoints.size() + linkToI;
const int32 linkToFullI = linkToI * _linkPoints.size() + linkFromI;
_distanceMatrix[linkFromFullI] = _distanceMatrix[linkToFullI] =
(uint)sqrtf(_linkPoints[linkFromI].sqrDist(_linkPoints[linkToI]) + 0.5f);
_previousTarget[linkFromFullI] = linkFromI;
_previousTarget[linkToFullI] = linkToI;
}
}
}
}
}
void PathFindingShape::calculateFloydWarshall() {
const auto distance = [&] (uint a, uint b) -> uint &{
return _distanceMatrix[a * _linkPoints.size() + b];
};
const auto previousTarget = [&] (uint a, uint b) -> int32 &{
return _previousTarget[a * _linkPoints.size() + b];
};
for (uint over = 0; over < _linkPoints.size(); over++) {
for (uint from = 0; from < _linkPoints.size(); from++) {
for (uint to = 0; to < _linkPoints.size(); to++) {
if (distance(from, over) != UINT_MAX && distance(over, to) != UINT_MAX &&
distance(from, over) + distance(over, to) < distance(from, to)) {
distance(from, to) = distance(from, over) + distance(over, to);
previousTarget(from, to) = previousTarget(over, to);
}
}
}
}
// in the game all floors should be fully connected
assert(find(_previousTarget.begin(), _previousTarget.end(), -1) == _previousTarget.end());
}
bool PathFindingShape::findPath(Point from, Point to_, Stack<Point> &path) const {
Point to = to_; // we might want to correct it
path.clear();
int32 fromContaining = polygonContaining(from);
if (fromContaining < 0)
return false;
int32 toContaining = polygonContaining(to);
if (toContaining < 0) {
to = closestPointTo(to, toContaining);
assert(toContaining >= 0);
}
//if (canGoStraightThrough(from, to, fromContaining, toContaining)) {
if (canGoStraightThrough(from, to, fromContaining, toContaining)) {
path.push(to);
return true;
}
floydWarshallPath(from, to, fromContaining, toContaining, path);
return true;
}
int32 PathFindingShape::edgeTarget(uint polygonI, uint pointI) const {
assert(polygonI < polygonCount() && pointI < kPointsPerPolygon);
uint fullI = polygonI * kPointsPerPolygon + pointI;
return _targetQuads[fullI];
}
bool PathFindingShape::canGoStraightThrough(
Point from, Point to,
int32 fromContainingI, int32 toContainingI) const {
int32 lastContainingI = -1;
while (fromContainingI != toContainingI) {
auto toContaining = at(toContainingI);
bool foundPortal = false;
for (uint i = 0; i < toContaining._points.size(); i++) {
int32 target = edgeTarget((uint)toContainingI, i);
if (target < 0 || target == lastContainingI)
continue;
if (toContaining.intersectsEdge(i, from, to)) {
foundPortal = true;
lastContainingI = toContainingI;
toContainingI = target;
break;
}
}
if (!foundPortal)
return false;
}
return true;
}
void PathFindingShape::floydWarshallPath(
Point from, Point to,
int32 fromContaining, int32 toContaining,
Stack<Point> &path) const {
path.push(to);
// first find the tuple of link points to be used
uint fromLink = UINT_MAX, toLink = UINT_MAX, bestDistance = UINT_MAX;
const auto &fromIndices = _linkIndices[fromContaining];
const auto &toIndices = _linkIndices[toContaining];
for (uint i = 0; i < 2 * kPointsPerPolygon; i++) {
const auto &curFromPoint = fromIndices._points[i / 2];
int32 curFromLink = (i % 2) ? curFromPoint.second : curFromPoint.first;
if (curFromLink < 0)
continue;
uint curFromDistance = (uint)sqrtf(from.sqrDist(_linkPoints[curFromLink]) + 0.5f);
for (uint j = 0; j < 2 * kPointsPerPolygon; j++) {
const auto &curToPoint = toIndices._points[j / 2];
int32 curToLink = (j % 2) ? curToPoint.second : curToPoint.first;
if (curToLink < 0)
continue;
uint totalDistance =
curFromDistance +
_distanceMatrix[curFromLink * _linkPoints.size() + curToLink] +
(uint)sqrtf(to.sqrDist(_linkPoints[curToLink]) + 0.5f);
if (totalDistance < bestDistance) {
bestDistance = totalDistance;
fromLink = curFromLink;
toLink = curToLink;
}
}
}
assert(fromLink != UINT_MAX && toLink != UINT_MAX);
// then walk the matrix back to reconstruct the path
while (fromLink != toLink) {
path.push(_linkPoints[toLink]);
toLink = _previousTarget[fromLink * _linkPoints.size() + toLink];
assert(toLink < _linkPoints.size());
}
path.push(_linkPoints[fromLink]);
}
bool PathFindingShape::findEvadeTarget(
Point centerTarget,
float depthScale, float minDistSqr,
Point &evadeTarget) const {
for (float tryDistBase = 60; tryDistBase < 250; tryDistBase += 10) {
for (int tryAngleI = 0; tryAngleI < 6; tryAngleI++) {
const float tryAngle = tryAngleI / 3.0f * M_PI + deg2rad(30.0f);
const float tryDist = tryDistBase * depthScale;
const Point tryPos = evadeTarget + Point(
(int16)(cosf(tryAngle) * tryDist),
(int16)(sinf(tryAngle) * tryDist));
if (contains(tryPos) && tryPos.sqrDist(centerTarget) > minDistSqr) {
evadeTarget = tryPos;
return true;
}
}
}
return false;
}
FloorColorShape::FloorColorShape() {}
FloorColorShape::FloorColorShape(ReadStream &stream) {
auto polygonCount = stream.readUint16LE();
_polygons.reserve(polygonCount);
_points.reserve(polygonCount * kPointsPerPolygon);
_pointColors.reserve(polygonCount * kPointsPerPolygon);
for (int i = 0; i < polygonCount; i++) {
for (uint j = 0; j < kPointsPerPolygon; j++)
_points.push_back(readPoint(stream));
// For the colors the alpha channel is not used so we store the brightness into it instead
// Brightness is store 0-100, but we can scale it up here
int firstColorI = _pointColors.size();
_pointColors.resize(_pointColors.size() + kPointsPerPolygon);
for (uint j = 0; j < kPointsPerPolygon; j++)
_pointColors[firstColorI + j].a = (uint8)MIN(255, stream.readByte() * 255 / 100);
for (uint j = 0; j < kPointsPerPolygon; j++) {
_pointColors[firstColorI + j].r = stream.readByte();
_pointColors[firstColorI + j].g = stream.readByte();
_pointColors[firstColorI + j].b = stream.readByte();
stream.readByte(); // second alpha value is ignored
}
stream.readByte(); // unused byte per polygon
uint pointCount = addPolygon(kPointsPerPolygon);
assert(pointCount <= kPointsPerPolygon);
_pointColors.resize(_points.size());
}
}
FloorColorPolygon FloorColorShape::at(uint index) const {
auto range = _polygons[index];
FloorColorPolygon p;
p._index = index;
p._points = Span<const Point>(_points.data() + range.first, range.second);
p._pointColors = Span<const Color>(_pointColors.data() + range.first, range.second);
return p;
}
OptionalColor FloorColorShape::colorAt(Point query) const {
int32 polygon = polygonContaining(query);
return polygon < 0
? OptionalColor(false, kClear)
: OptionalColor(true, at(polygon).colorAt(query));
}
}

258
engines/alcachofa/shape.h Normal file
View File

@@ -0,0 +1,258 @@
/* 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 ALCACHOFA_SHAPE_H
#define ALCACHOFA_SHAPE_H
#include "common/stream.h"
#include "common/array.h"
#include "common/stack.h"
#include "common/rect.h"
#include "common/span.h"
#include "common/util.h"
#include "math/vector2d.h"
#include "alcachofa/common.h"
namespace Alcachofa {
struct EdgeDistances {
EdgeDistances(Common::Point edgeA, Common::Point edgeB, Common::Point query);
float _edgeLength;
float _onEdge;
float _toEdge;
};
struct Polygon {
uint _index;
Common::Span<const Common::Point> _points;
bool contains(Common::Point query) const;
bool intersectsEdge(uint startPointI, Common::Point a, Common::Point b) const;
EdgeDistances edgeDistances(uint startPointI, Common::Point query) const;
Common::Point closestPointTo(Common::Point query, float &distanceSqr) const;
inline Common::Point closestPointTo(Common::Point query) const {
float dummy;
return closestPointTo(query, dummy);
}
Common::Point midPoint() const;
};
struct PathFindingPolygon : Polygon {
Common::Span<const uint8> _pointDepths;
int8 _order;
using SharedPoint = Common::Pair<uint, uint>;
float depthAt(Common::Point query) const;
uint findSharedPoints(const PathFindingPolygon &other, Common::Span<SharedPoint> sharedPoints) const;
};
struct FloorColorPolygon : Polygon {
Common::Span<const Color> _pointColors;
Color colorAt(Common::Point query) const;
};
template<class TShape, typename TPolygon>
struct PolygonIterator {
using difference_type = uint;
using value_type = TPolygon;
using my_type = PolygonIterator<TShape, TPolygon>;
inline value_type operator*() const {
return _shape.at(_index);
}
inline my_type &operator++() {
assert(_index < _shape.polygonCount());
_index++;
return *this;
}
inline my_type operator++(int) {
assert(_index < _shape.polygonCount());
auto tmp = *this;
++*this;
return tmp;
}
inline bool operator==(const my_type &it) const {
return &this->_shape == &it._shape && this->_index == it._index;
}
inline bool operator!=(const my_type &it) const {
return &this->_shape != &it._shape || this->_index != it._index;
}
private:
friend typename Common::remove_const_t<TShape>;
PolygonIterator(const TShape &shape, uint index = 0)
: _shape(shape)
, _index(index) {
}
const TShape &_shape;
uint _index;
};
class Shape {
public:
using iterator = PolygonIterator<Shape, Polygon>;
Shape();
Shape(Common::ReadStream &stream);
inline Common::Point firstPoint() const { return _points.empty() ? Common::Point() : _points[0]; }
inline uint polygonCount() const { return _polygons.size(); }
inline bool empty() const { return polygonCount() == 0; }
inline iterator begin() const { return { *this, 0 }; }
inline iterator end() const { return { *this, polygonCount() }; }
Polygon at(uint index) const;
int32 polygonContaining(Common::Point query) const;
bool contains(Common::Point query) const;
Common::Point closestPointTo(Common::Point query, int32 &polygonI) const;
inline Common::Point closestPointTo(Common::Point query) const {
int32 dummy;
return closestPointTo(query, dummy);
}
void setAsRectangle(const Common::Rect &rect);
protected:
uint addPolygon(uint maxCount);
using PolygonRange = Common::Pair<uint, uint>;
Common::Array<PolygonRange> _polygons;
Common::Array<Common::Point> _points;
};
/**
* @brief Path finding is based on the Shape class with the invariant that
* every polygon is a convex polygon with at most four points.
* Equal points of different quads link them together, for shared edges we
* add an additional link point in the center of the edge.
*
* The resulting graph is processed using Floyd-Warshall to precalculate for
* the actual path finding. Additionally we check whether a character can
* walk straight through an edge instead of following the link points.
*/
class PathFindingShape final : public Shape {
public:
using iterator = PolygonIterator<PathFindingShape, PathFindingPolygon>;
static constexpr const uint kPointsPerPolygon = 4;
PathFindingShape();
PathFindingShape(Common::ReadStream &stream);
inline iterator begin() const { return { *this, 0 }; }
inline iterator end() const { return { *this, polygonCount() }; }
PathFindingPolygon at(uint index) const;
int8 orderAt(Common::Point query) const;
float depthAt(Common::Point query) const;
bool findPath(
Common::Point from,
Common::Point to,
Common::Stack<Common::Point> &path) const;
int32 edgeTarget(uint polygonI, uint pointI) const;
bool findEvadeTarget(
Common::Point centerTarget,
float depthScale,
float minDistSqr,
Common::Point &evadeTarget) const;
private:
using LinkIndex = Common::Pair<int32, int32>;
void setupLinks();
void setupLinkPoint(
const PathFindingPolygon &outer,
const PathFindingPolygon &inner,
PathFindingPolygon::SharedPoint pointI);
void setupLinkEdge(
const PathFindingPolygon &outer,
const PathFindingPolygon &inner,
LinkIndex outerP, LinkIndex innerP);
void initializeFloydWarshall();
void calculateFloydWarshall();
bool canGoStraightThrough(
Common::Point from,
Common::Point to,
int32 fromContaining, int32 toContaining) const;
void floydWarshallPath(
Common::Point from,
Common::Point to,
int32 fromContaining, int32 toContaining,
Common::Stack<Common::Point> &path) const;
Common::Array<uint8> _pointDepths;
Common::Array<int8> _polygonOrders;
/**
* These are the edges in the graph, they are either points
* that are shared by two polygons or artificial points in
* the center of a shared edge
*/
Common::Array<Common::Point> _linkPoints;
/**
* For each point of each polygon the index (or -1) to
* the corresponding link point. The second point is the
* index to the artifical center point
*/
struct LinkPolygonIndices {
LinkPolygonIndices();
LinkIndex _points[kPointsPerPolygon];
};
Common::Array<LinkPolygonIndices> _linkIndices;
/**
* For the going-straight-through-edges check we need
* to know for each shared edge (defined by the starting point)
* into which quad we will walk.
*/
Common::Array<int32> _targetQuads;
Common::Array<uint> _distanceMatrix; ///< for Floyd-Warshall
Common::Array<int32> _previousTarget; ///< for Floyd-Warshall
};
using OptionalColor = Common::Pair<bool, Color>;
class FloorColorShape final : public Shape {
public:
using iterator = PolygonIterator<FloorColorShape, FloorColorPolygon>;
static constexpr const uint kPointsPerPolygon = 4;
FloorColorShape();
FloorColorShape(Common::ReadStream &stream);
inline iterator begin() const { return { *this, 0 }; }
inline iterator end() const { return { *this, polygonCount() }; }
FloorColorPolygon at(uint index) const;
OptionalColor colorAt(Common::Point query) const;
private:
Common::Array<Color> _pointColors;
};
}
#endif // ALCACHOFA_SHAPE_H

View File

@@ -0,0 +1,397 @@
/* 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 "alcachofa/sounds.h"
#include "alcachofa/rooms.h"
#include "alcachofa/alcachofa.h"
#include "alcachofa/detection.h"
#include "common/file.h"
#include "common/substream.h"
#include "audio/audiostream.h"
#include "audio/decoders/wave.h"
#include "audio/decoders/adpcm.h"
#include "audio/decoders/raw.h"
using namespace Common;
using namespace Audio;
namespace Alcachofa {
void Sounds::Playback::fadeOut(uint32 duration) {
_fadeStart = g_system->getMillis();
_fadeDuration = MAX<uint32>(duration, 1);
}
Sounds::Sounds()
: _mixer(g_system->getMixer())
, _musicSemaphore("music") {
assert(_mixer != nullptr);
}
Sounds::~Sounds() {
_mixer->stopAll();
}
Sounds::Playback *Sounds::getPlaybackById(SoundHandle id) {
auto itPlayback = find_if(_playbacks.begin(), _playbacks.end(),
[&] (const Playback &playback) { return playback._handle == id; });
return itPlayback == _playbacks.end() ? nullptr : itPlayback;
}
void Sounds::update() {
if (_isMusicPlaying && !isAlive(_musicSoundID)) {
if (_nextMusicID < 0)
fadeMusic();
else
startMusic(_nextMusicID);
}
for (uint i = _playbacks.size(); i > 0; i--) {
Playback &playback = _playbacks[i - 1];
if (!_mixer->isSoundHandleActive(playback._handle))
_playbacks.erase(_playbacks.begin() + i - 1);
else if (playback._fadeDuration != 0) {
if (g_system->getMillis() >= playback._fadeStart + playback._fadeDuration) {
_mixer->stopHandle(playback._handle);
_playbacks.erase(_playbacks.begin() + i - 1);
} else {
byte newVolume = (g_system->getMillis() - playback._fadeStart) * Mixer::kMaxChannelVolume / playback._fadeDuration;
_mixer->setChannelVolume(playback._handle, Mixer::kMaxChannelVolume - newVolume);
}
}
}
}
static AudioStream *loadSND(File *file) {
// SND files are just WAV files with removed headers
const uint32 endOfFormat = file->readUint32LE() + 2 * sizeof(uint32);
if (endOfFormat < 24)
error("Invalid SND format size");
uint16 format = file->readUint16LE();
uint16 channels = file->readUint16LE();
uint32 freq = file->readUint32LE();
file->skip(sizeof(uint32)); // bytesPerSecond, unnecessary for us
uint16 bytesPerBlock = file->readUint16LE();
uint16 bitsPerSample = file->readUint16LE();
if (endOfFormat >= 2 * sizeof(uint32) + 20) {
file->skip(sizeof(uint16)); // size of extra data
uint16 extra = file->readUint16LE();
bytesPerBlock = 4 * channels * ((extra + 14) / 8);
}
file->seek(endOfFormat, SEEK_SET);
auto subStream = new SeekableSubReadStream(file, (uint32)file->pos(), (uint32)file->size(), DisposeAfterUse::YES);
if (format == 1 && channels <= 2 && (bitsPerSample == 8 || bitsPerSample == 16))
return makeRawStream(subStream, (int)freq,
(channels == 2 ? FLAG_STEREO : 0) |
(bitsPerSample == 16 ? FLAG_16BITS | FLAG_LITTLE_ENDIAN : FLAG_UNSIGNED));
else if (format == 17 && channels <= 2)
return makeADPCMStream(subStream, DisposeAfterUse::YES, 0, kADPCMMSIma, (int)freq, (int)channels, (uint32)bytesPerBlock);
else {
delete subStream;
g_engine->game().invalidSNDFormat(format, channels, freq, bitsPerSample);
return nullptr;
}
}
static AudioStream *openAudio(const char *fileName) {
String path = String::format("Sonidos/%s.SND", fileName);
File *file = new File();
if (file->open(path.c_str()))
return file->size() == 0 // Movie Adventure has some null-size audio files, they are treated like infinite silence
? makeSilentAudioStream(8000, false)
: loadSND(file);
path.setChar('W', path.size() - 3);
path.setChar('A', path.size() - 2);
path.setChar('V', path.size() - 1);
if (file->open(path.c_str()))
return makeWAVStream(file, DisposeAfterUse::YES);
delete file;
g_engine->game().missingSound(fileName);
return nullptr;
}
SoundHandle Sounds::playSoundInternal(const char *fileName, byte volume, Mixer::SoundType type) {
AudioStream *stream = openAudio(fileName);
if (stream == nullptr && (type == Mixer::kSpeechSoundType || type == Mixer::kMusicSoundType)) {
/* If voice files are missing, the player could still read the subtitle
* For this we return infinite silent audio which the user has to skip
* But only do this for speech as there is no skipping for sound effects
* so those would live on forever and block up mixer channels
* Music is fine as well as we clean up the music playack explicitly
*/
stream = makeSilentAudioStream(8000, false);
}
if (stream == nullptr)
return {};
Array<int16> samples;
SeekableAudioStream *seekStream = dynamic_cast<SeekableAudioStream *>(stream);
if (type == Mixer::kSpeechSoundType && seekStream != nullptr) {
// for lip-sync we need access to the samples so we decode the entire stream now
int sampleCount = seekStream->getLength().totalNumberOfFrames();
if (sampleCount > 0) {
// we actually got a length
samples.resize((uint)sampleCount);
sampleCount = seekStream->readBuffer(samples.data(), sampleCount);
if (sampleCount < 0)
samples.clear();
samples.resize((uint)sampleCount); // we might have gotten less samples
} else {
// we did not, now it is getting inefficient
const int bufferSize = 2048;
int16 buffer[bufferSize];
int chunkSampleCount;
do {
chunkSampleCount = seekStream->readBuffer(buffer, bufferSize);
if (chunkSampleCount <= 0)
break;
samples.resize(samples.size() + chunkSampleCount);
copy(buffer, buffer + chunkSampleCount, samples.data() + sampleCount);
sampleCount += chunkSampleCount;
} while (chunkSampleCount >= bufferSize);
}
if (sampleCount > 0) {
stream = makeRawStream(
(byte *)samples.data(),
samples.size() * sizeof(int16),
seekStream->getRate(),
FLAG_16BITS |
#ifdef SCUMM_LITTLE_ENDIAN
FLAG_LITTLE_ENDIAN | // readBuffer returns native endian
#endif
(seekStream->isStereo() ? FLAG_STEREO : 0),
DisposeAfterUse::NO);
delete seekStream;
}
}
SoundHandle handle;
_mixer->playStream(type, &handle, stream, -1, volume);
Playback playback;
playback._handle = handle;
playback._type = type;
playback._inputRate = stream->getRate();
playback._samples = Common::move(samples);
_playbacks.push_back(Common::move(playback));
return handle;
}
SoundHandle Sounds::playVoice(const String &fileName, byte volume) {
debugC(1, kDebugSounds, "Play voice: %s at %d", fileName.c_str(), (int)volume);
return playSoundInternal(fileName.c_str(), volume, Mixer::kSpeechSoundType);
}
SoundHandle Sounds::playSFX(const String &fileName, byte volume) {
debugC(1, kDebugSounds, "Play SFX: %s at %d", fileName.c_str(), (int)volume);
return playSoundInternal(fileName.c_str(), volume, Mixer::kSFXSoundType);
}
void Sounds::stopAll() {
debugC(1, kDebugSounds, "Stop all sounds");
_mixer->stopAll();
_playbacks.clear();
}
void Sounds::stopVoice() {
debugC(1, kDebugSounds, "Stop all voices");
for (uint i = _playbacks.size(); i > 0; i--) {
if (_playbacks[i - 1]._type == Mixer::kSpeechSoundType) {
_mixer->stopHandle(_playbacks[i - 1]._handle);
_playbacks.erase(_playbacks.begin() + i - 1);
}
}
}
void Sounds::pauseAll(bool paused) {
_mixer->pauseAll(paused);
}
bool Sounds::isAlive(SoundHandle id) {
Playback *playback = getPlaybackById(id);
return playback != nullptr && _mixer->isSoundHandleActive(playback->_handle);
}
void Sounds::setVolume(SoundHandle id, byte volume) {
Playback *playback = getPlaybackById(id);
if (playback != nullptr)
_mixer->setChannelVolume(playback->_handle, volume);
}
void Sounds::setAppropriateVolume(SoundHandle id,
MainCharacterKind processCharacterKind,
Character *speakingCharacter) {
static constexpr byte kAlmostMaxVolume = Mixer::kMaxChannelVolume * 9 / 10;
auto &player = g_engine->player();
auto processCharacter = processCharacterKind == MainCharacterKind::None ? nullptr
: &g_engine->world().getMainCharacterByKind(processCharacterKind);
byte newVolume;
if (processCharacter == nullptr || processCharacter == player.activeCharacter())
newVolume = Mixer::kMaxChannelVolume;
else if (speakingCharacter != nullptr && speakingCharacter->room() == player.currentRoom())
newVolume = kAlmostMaxVolume;
else if (processCharacter->room() == player.currentRoom())
newVolume = kAlmostMaxVolume;
else
newVolume = 0;
setVolume(id, newVolume);
}
void Sounds::fadeOut(SoundHandle id, uint32 duration) {
Playback *playback = getPlaybackById(id);
if (playback != nullptr)
playback->fadeOut(duration);
}
void Sounds::fadeOutVoiceAndSFX(uint32 duration) {
for (auto &playback : _playbacks) {
if (playback._type == Mixer::kSpeechSoundType || playback._type == Mixer::kSFXSoundType)
playback.fadeOut(duration);
}
}
bool Sounds::isNoisy(SoundHandle id, float windowSize, float minDifferences) {
assert(windowSize > 0 && minDifferences > 0);
const Playback *playback = getPlaybackById(id);
if (playback == nullptr ||
playback->_samples.empty() ||
!_mixer->isSoundHandleActive(playback->_handle))
return false;
minDifferences *= windowSize;
uint windowSizeInSamples = (uint)(windowSize * 0.001f * playback->_inputRate);
uint samplePosition = (uint)_mixer->getElapsedTime(playback->_handle)
.convertToFramerate(playback->_inputRate)
.totalNumberOfFrames();
uint endPosition = MIN(playback->_samples.size(), samplePosition + windowSizeInSamples);
if (samplePosition >= endPosition)
return false;
/* While both ScummVM and the original engine use signed int16 samples
* For this noise detection the samples are reinterpret as uint16
* This causes changes going through zero to be much more significant.
*/
float sumOfDifferences = 0;
const uint16 *samplePtr = (const uint16 *)playback->_samples.data();
for (uint i = samplePosition; i < endPosition - 1; i++)
// cast to int before to not be constrained by uint16
sumOfDifferences += ABS((int)samplePtr[i + 1] - samplePtr[i]);
return sumOfDifferences / 256.0f >= minDifferences;
}
void Sounds::startMusic(int musicId) {
debugC(2, kDebugSounds, "startMusic %d", musicId);
assert(musicId >= 0);
fadeMusic();
constexpr size_t kBufferSize = 16;
char filenameBuffer[kBufferSize];
snprintf(filenameBuffer, kBufferSize, "T%d", musicId);
_musicSoundID = playSoundInternal(filenameBuffer, Mixer::kMaxChannelVolume, Mixer::kMusicSoundType);
_isMusicPlaying = true;
_nextMusicID = musicId;
}
void Sounds::queueMusic(int musicId) {
debugC(2, kDebugSounds, "queueMusic %d", musicId);
_nextMusicID = musicId;
}
void Sounds::fadeMusic(uint32 duration) {
debugC(2, kDebugSounds, "fadeMusic");
fadeOut(_musicSoundID, duration);
_isMusicPlaying = false;
_nextMusicID = -1;
_musicSoundID = {};
}
void Sounds::setMusicToRoom(int roomMusicId) {
// Alcachofa Soft used IDs > 200 to mean "no change in music"
if (roomMusicId == _nextMusicID || roomMusicId > 200) {
debugC(1, kDebugSounds, "setMusicToRoom: from %d to %d, not executed", _nextMusicID, roomMusicId);
return;
}
debugC(1, kDebugSounds, "setMusicToRoom: from %d to %d", _nextMusicID, roomMusicId);
if (roomMusicId > 0)
startMusic(roomMusicId);
else
fadeMusic();
}
Task *Sounds::waitForMusicToEnd(Process &process) {
return new WaitForMusicTask(process);
}
PlaySoundTask::PlaySoundTask(Process &process, SoundHandle SoundHandle)
: Task(process)
, _soundHandle(SoundHandle) {}
PlaySoundTask::PlaySoundTask(Process &process, Serializer &s)
: Task(process)
, _soundHandle({}) {
// playing sounds are not persisted in the savestates,
// this task will stop at the next frame
syncGame(s);
}
TaskReturn PlaySoundTask::run() {
auto &sounds = g_engine->sounds();
if (sounds.isAlive(_soundHandle)) {
sounds.setAppropriateVolume(_soundHandle, process().character(), nullptr);
return TaskReturn::yield();
} else
return TaskReturn::finish(1);
}
void PlaySoundTask::debugPrint() {
// unfortunately SoundHandle is not castable to something we could display here safely
g_engine->console().debugPrintf("PlaySound\n");
}
DECLARE_TASK(PlaySoundTask)
WaitForMusicTask::WaitForMusicTask(Process &process)
: Task(process)
, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) {}
WaitForMusicTask::WaitForMusicTask(Process &process, Serializer &s)
: Task(process)
, _lock("wait-for-music", g_engine->sounds().musicSemaphore()) {
syncGame(s);
}
TaskReturn WaitForMusicTask::run() {
g_engine->sounds().queueMusic(-1);
return g_engine->sounds().isMusicPlaying()
? TaskReturn::yield()
: TaskReturn::finish(0);
}
void WaitForMusicTask::debugPrint() {
g_engine->console().debugPrintf("WaitForMusic\n");
}
DECLARE_TASK(WaitForMusicTask)
}

108
engines/alcachofa/sounds.h Normal file
View File

@@ -0,0 +1,108 @@
/* 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 ALCACHOFA_SOUNDS_H
#define ALCACHOFA_SOUNDS_H
#include "alcachofa/scheduler.h"
#include "audio/mixer.h"
#include "audio/audiostream.h"
namespace Alcachofa {
class Character;
using ::Audio::SoundHandle;
class Sounds {
public:
Sounds();
~Sounds();
void update();
SoundHandle playVoice(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
SoundHandle playSFX(const Common::String &fileName, byte volume = Audio::Mixer::kMaxChannelVolume);
void stopAll();
void stopVoice();
void pauseAll(bool paused);
void fadeOut(SoundHandle id, uint32 duration);
void fadeOutVoiceAndSFX(uint32 duration);
bool isAlive(SoundHandle id);
void setVolume(SoundHandle id, byte volume);
void setAppropriateVolume(SoundHandle id,
MainCharacterKind processCharacter,
Character *speakingCharacter);
bool isNoisy(SoundHandle id, float windowSize, float minDifferences); ///< used for lip-sync
void startMusic(int musicId);
void queueMusic(int musicId);
void fadeMusic(uint32 duration = 500);
void setMusicToRoom(int roomMusicId);
Task *waitForMusicToEnd(Process &processd);
inline bool isMusicPlaying() const { return _isMusicPlaying; }
inline int musicID() const { return _nextMusicID; }
inline FakeSemaphore &musicSemaphore() { return _musicSemaphore; }
private:
struct Playback {
void fadeOut(uint32 duration);
Audio::SoundHandle _handle;
Audio::Mixer::SoundType _type = Audio::Mixer::SoundType::kPlainSoundType;
uint32 _fadeStart = 0,
_fadeDuration = 0;
int _inputRate = 0;
Common::Array<int16> _samples; ///< might not be filled, only voice samples are preloaded for lip-sync
};
Playback *getPlaybackById(SoundHandle id);
SoundHandle playSoundInternal(const char *fileName, byte volume, Audio::Mixer::SoundType type);
Common::Array<Playback> _playbacks;
Audio::Mixer *_mixer;
SoundHandle _musicSoundID = {}; // we use another soundID to reuse fading
bool _isMusicPlaying = false;
int _nextMusicID = -1;
FakeSemaphore _musicSemaphore;
};
struct PlaySoundTask final : public Task {
PlaySoundTask(Process &process, SoundHandle soundHandle);
PlaySoundTask(Process &process, Common::Serializer &s);
TaskReturn run() override;
void debugPrint() override;
const char *taskName() const override;
private:
SoundHandle _soundHandle;
};
struct WaitForMusicTask final : public Task {
WaitForMusicTask(Process &process);
WaitForMusicTask(Process &process, Common::Serializer &s);
TaskReturn run() override;
void debugPrint() override;
const char *taskName() const override;
private:
FakeLock _lock;
};
}
#endif // ALCACHOFA_SOUNDS_H

56
engines/alcachofa/tasks.h Normal file
View File

@@ -0,0 +1,56 @@
/* 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/>.
*
*/
/* The task loading works as follows:
* - the task has a constructor MyPrivateTask(Process &, Serializer &)
* - DECLARE_TASK implements a global function to call that constructor
* void constructTask_MyPrivateTask(Process &, Serializer
* - in Process::syncGame we first forward-declare that function
* - we go through every task to compare task name and call the factory
*/
#ifndef DEFINE_TASK
#define DEFINE_TASK(TaskName)
#endif
DEFINE_TASK(CamLerpPosTask)
DEFINE_TASK(CamLerpScaleTask)
DEFINE_TASK(CamLerpPosScaleTask)
DEFINE_TASK(CamLerpRotationTask)
DEFINE_TASK(CamShakeTask)
DEFINE_TASK(CamWaitToStopTask)
DEFINE_TASK(CamSetInactiveAttributeTask)
DEFINE_TASK(SayTextTask)
DEFINE_TASK(AnimateCharacterTask)
DEFINE_TASK(LerpLodBiasTask)
DEFINE_TASK(ArriveTask)
DEFINE_TASK(DialogMenuTask)
DEFINE_TASK(AnimateTask)
DEFINE_TASK(CenterBottomTextTask)
DEFINE_TASK(FadeTask)
DEFINE_TASK(DoorTask)
DEFINE_TASK(ScriptTimerTask)
DEFINE_TASK(ScriptTask)
DEFINE_TASK(PlaySoundTask)
DEFINE_TASK(WaitForMusicTask)
DEFINE_TASK(DelayTask)
#undef DEFINE_TASK

View File

@@ -0,0 +1,344 @@
/* 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 "alcachofa/alcachofa.h"
#include "alcachofa/script.h"
#include "alcachofa/global-ui.h"
#include "alcachofa/menu.h"
#include "alcachofa/objects.h"
#include "alcachofa/rooms.h"
using namespace Common;
namespace Alcachofa {
const char *MenuButton::typeName() const { return "MenuButton"; }
MenuButton::MenuButton(Room *room, ReadStream &stream)
: PhysicalObject(room, stream)
, _actionId(stream.readSint32LE())
, _graphicNormal(stream)
, _graphicHovered(stream)
, _graphicClicked(stream)
, _graphicDisabled(stream) {}
void MenuButton::draw() {
if (!isEnabled())
return;
Graphic &graphic =
!_isInteractable ? _graphicDisabled
: _isClicked ? _graphicClicked
: wasSelected() ? _graphicHovered
: _graphicNormal;
graphic.update();
g_engine->drawQueue().add<AnimationDrawRequest>(graphic, true, BlendMode::AdditiveAlpha);
}
void MenuButton::update() {
PhysicalObject::update();
if (!_isClicked)
return;
_graphicClicked.update();
if (!_graphicClicked.isPaused())
return;
if (!_triggerNextFrame) {
// another delay probably to show the last frame of animation
_triggerNextFrame = true;
return;
}
_interactionLock.release();
_triggerNextFrame = false;
_isClicked = false;
trigger();
}
void MenuButton::loadResources() {
_graphicNormal.loadResources();
_graphicHovered.loadResources();
_graphicClicked.loadResources();
_graphicDisabled.loadResources();
}
void MenuButton::freeResources() {
_graphicNormal.freeResources();
_graphicHovered.freeResources();
_graphicClicked.freeResources();
_graphicDisabled.freeResources();
}
void MenuButton::onHoverUpdate() {}
void MenuButton::onClick() {
if (_isInteractable && _interactionLock.isReleased()) {
_interactionLock = FakeLock("button", g_engine->menu().interactionSemaphore());
_isClicked = true;
_triggerNextFrame = false;
_graphicClicked.start(false);
}
}
void MenuButton::trigger() {
// all menu buttons should be inherited and override trigger
warning("Unimplemented %s %s action %d", typeName(), name().c_str(), _actionId);
}
const char *InternetMenuButton::typeName() const { return "InternetMenuButton"; }
InternetMenuButton::InternetMenuButton(Room *room, ReadStream &stream)
: MenuButton(room, stream) {}
const char *OptionsMenuButton::typeName() const { return "OptionsMenuButton"; }
OptionsMenuButton::OptionsMenuButton(Room *room, ReadStream &stream)
: MenuButton(room, stream) {}
void OptionsMenuButton::update() {
MenuButton::update();
const auto action = (OptionsMenuAction)actionId();
if (action == OptionsMenuAction::MainMenu && g_engine->input().wasMenuKeyPressed())
onClick();
}
void OptionsMenuButton::trigger() {
g_engine->menu().triggerOptionsAction((OptionsMenuAction)actionId());
}
const char *MainMenuButton::typeName() const { return "MainMenuButton"; }
MainMenuButton::MainMenuButton(Room *room, ReadStream &stream)
: MenuButton(room, stream) {}
void MainMenuButton::update() {
MenuButton::update();
const auto action = (MainMenuAction)actionId();
if (g_engine->input().wasMenuKeyPressed() &&
(action == MainMenuAction::ContinueGame || action == MainMenuAction::NewGame))
onClick();
}
void MainMenuButton::trigger() {
g_engine->menu().triggerMainMenuAction((MainMenuAction)actionId());
}
const char *PushButton::typeName() const { return "PushButton"; }
PushButton::PushButton(Room *room, ReadStream &stream)
: PhysicalObject(room, stream)
, _alwaysVisible(readBool(stream))
, _graphic1(stream)
, _graphic2(stream)
, _actionId(stream.readSint32LE()) {}
const char *EditBox::typeName() const { return "EditBox"; }
EditBox::EditBox(Room *room, ReadStream &stream)
: PhysicalObject(room, stream)
, i1(stream.readSint32LE())
, p1(Shape(stream).firstPoint())
, _labelId(readVarString(stream))
, b1(readBool(stream))
, i3(stream.readSint32LE())
, i4(stream.readSint32LE())
, i5(stream.readSint32LE())
, _fontId(0) {
if (g_engine->version() == EngineVersion::V3_1)
_fontId = stream.readSint32LE();
}
const char *CheckBox::typeName() const { return "CheckBox"; }
CheckBox::CheckBox(Room *room, ReadStream &stream)
: PhysicalObject(room, stream)
, _isChecked(readBool(stream))
, _graphicUnchecked(stream)
, _graphicChecked(stream)
, _graphicHovered(stream)
, _graphicClicked(stream)
, _actionId(stream.readSint32LE()) {}
void CheckBox::draw() {
if (!isEnabled())
return;
Graphic &baseGraphic = _isChecked ? _graphicChecked : _graphicUnchecked;
baseGraphic.update();
g_engine->drawQueue().add<AnimationDrawRequest>(baseGraphic, true, BlendMode::AdditiveAlpha);
if (wasSelected()) {
Graphic &hoverGraphic = _wasClicked ? _graphicClicked : _graphicHovered;
hoverGraphic.update();
g_engine->drawQueue().add<AnimationDrawRequest>(hoverGraphic, true, BlendMode::AdditiveAlpha);
}
}
void CheckBox::update() {
PhysicalObject::update();
if (_wasClicked) {
if (g_engine->getMillis() - _clickTime > 500) {
_wasClicked = false;
trigger();
}
}
// the original engine would stall the application as click delay.
// this would prevent bacterios arm in movie adventure being rendered twice for multiple checkboxes
// we can instead check the hovered state and prevent the arm (clicked/hovered graphic) being drawn
}
void CheckBox::loadResources() {
_wasClicked = false;
_graphicUnchecked.loadResources();
_graphicChecked.loadResources();
_graphicHovered.loadResources();
_graphicClicked.loadResources();
}
void CheckBox::freeResources() {
_graphicUnchecked.freeResources();
_graphicChecked.freeResources();
_graphicHovered.freeResources();
_graphicClicked.freeResources();
}
void CheckBox::onHoverUpdate() {}
void CheckBox::onClick() {
_wasClicked = true;
_clickTime = g_engine->getMillis();
}
void CheckBox::trigger() {
g_engine->menu().triggerOptionsAction((OptionsMenuAction)actionId());
}
const char *CheckBoxAutoAdjustNoise::typeName() const { return "CheckBoxAutoAdjustNoise"; }
CheckBoxAutoAdjustNoise::CheckBoxAutoAdjustNoise(Room *room, ReadStream &stream)
: CheckBox(room, stream) {
stream.readByte(); // unused and ignored byte
}
const char *SlideButton::typeName() const { return "SlideButton"; }
SlideButton::SlideButton(Room *room, ReadStream &stream)
: ObjectBase(room, stream)
, _valueId(stream.readSint32LE())
, _minPos(Shape(stream).firstPoint())
, _maxPos(Shape(stream).firstPoint())
, _graphicIdle(stream)
, _graphicHovered(stream)
, _graphicClicked(stream) {}
void SlideButton::draw() {
auto *optionsMenu = dynamic_cast<OptionsMenu *>(room());
scumm_assert(optionsMenu != nullptr);
Graphic *activeGraphic;
if (optionsMenu->currentSlideButton() == this && g_engine->input().isMouseLeftDown())
activeGraphic = &_graphicClicked;
else
activeGraphic = isMouseOver() ? &_graphicHovered : &_graphicIdle;
activeGraphic->update();
g_engine->drawQueue().add<AnimationDrawRequest>(*activeGraphic, true, BlendMode::AdditiveAlpha);
}
void SlideButton::update() {
const auto mousePos = g_engine->input().mousePos2D();
auto *optionsMenu = dynamic_cast<OptionsMenu *>(room());
scumm_assert(optionsMenu != nullptr);
if (optionsMenu->currentSlideButton() == this) {
if (!g_engine->input().isMouseLeftDown()) {
optionsMenu->currentSlideButton() = nullptr;
g_engine->menu().triggerOptionsValue((OptionsMenuValue)_valueId, _value);
update(); // to update the position
} else {
int clippedMousePosY = CLIP(mousePos.y, _minPos.y, _maxPos.y);
_value = (_maxPos.y - clippedMousePosY) / (float)(_maxPos.y - _minPos.y);
_graphicClicked.topLeft() = Point((_minPos.x + _maxPos.x) / 2, clippedMousePosY);
}
} else {
_graphicIdle.topLeft() = Point(
(_minPos.x + _maxPos.x) / 2,
(int16)(_maxPos.y - _value * (_maxPos.y - _minPos.y)));
if (!isMouseOver())
return;
_graphicHovered.topLeft() = _graphicIdle.topLeft();
if (g_engine->input().wasMouseLeftPressed())
optionsMenu->currentSlideButton() = this;
optionsMenu->clearLastSelectedObject();
g_engine->player().selectedObject() = nullptr;
}
}
void SlideButton::loadResources() {
_graphicIdle.loadResources();
_graphicHovered.loadResources();
_graphicClicked.loadResources();
}
void SlideButton::freeResources() {
_graphicIdle.freeResources();
_graphicHovered.freeResources();
_graphicClicked.freeResources();
}
bool SlideButton::isMouseOver() const {
const auto mousePos = g_engine->input().mousePos2D();
return
mousePos.x >= _minPos.x && mousePos.y >= _minPos.y &&
mousePos.x <= _maxPos.x && mousePos.y <= _maxPos.y;
}
const char *IRCWindow::typeName() const { return "IRCWindow"; }
IRCWindow::IRCWindow(Room *room, ReadStream &stream)
: ObjectBase(room, stream)
, _p1(Shape(stream).firstPoint())
, _p2(Shape(stream).firstPoint()) {}
const char *MessageBox::typeName() const { return "MessageBox"; }
MessageBox::MessageBox(Room *room, ReadStream &stream)
: ObjectBase(room, stream)
, _graph1(stream)
, _graph2(stream)
, _graph3(stream)
, _graph4(stream)
, _graph5(stream) {
_graph1.start(true);
_graph2.start(true);
_graph3.start(true);
_graph4.start(true);
_graph5.start(true);
}
const char *VoiceMeter::typeName() const { return "VoiceMeter"; }
VoiceMeter::VoiceMeter(Room *room, ReadStream &stream)
: GraphicObject(room, stream) {
stream.readByte(); // unused and ignored byte
}
}