Initial commit
This commit is contained in:
3
engines/alcachofa/POTFILES
Normal file
3
engines/alcachofa/POTFILES
Normal file
@@ -0,0 +1,3 @@
|
||||
engines/alcachofa/detection.cpp
|
||||
engines/alcachofa/graphics-opengl.cpp
|
||||
engines/alcachofa/metaengine.cpp
|
||||
493
engines/alcachofa/alcachofa.cpp
Normal file
493
engines/alcachofa/alcachofa.cpp
Normal 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
|
||||
203
engines/alcachofa/alcachofa.h
Normal file
203
engines/alcachofa/alcachofa.h
Normal 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
|
||||
671
engines/alcachofa/camera.cpp
Normal file
671
engines/alcachofa/camera.cpp
Normal 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
122
engines/alcachofa/camera.h
Normal file
@@ -0,0 +1,122 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef 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
|
||||
203
engines/alcachofa/common.cpp
Normal file
203
engines/alcachofa/common.cpp
Normal 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
171
engines/alcachofa/common.h
Normal 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
|
||||
3
engines/alcachofa/configure.engine
Normal file
3
engines/alcachofa/configure.engine
Normal file
@@ -0,0 +1,3 @@
|
||||
# This file is included from the main "configure" script
|
||||
# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
|
||||
add_engine alcachofa "Alcachofa" yes "" "" "highres mpeg2 3d" "tinygl"
|
||||
297
engines/alcachofa/console.cpp
Normal file
297
engines/alcachofa/console.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#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
|
||||
75
engines/alcachofa/console.h
Normal file
75
engines/alcachofa/console.h
Normal 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
|
||||
3
engines/alcachofa/credits.pl
Normal file
3
engines/alcachofa/credits.pl
Normal file
@@ -0,0 +1,3 @@
|
||||
begin_section("Alcachofa");
|
||||
add_person("Hermann Noll", "Helco", "");
|
||||
end_section();
|
||||
230
engines/alcachofa/debug.h
Normal file
230
engines/alcachofa/debug.h
Normal file
@@ -0,0 +1,230 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#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
|
||||
46
engines/alcachofa/detection.cpp
Normal file
46
engines/alcachofa/detection.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#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);
|
||||
90
engines/alcachofa/detection.h
Normal file
90
engines/alcachofa/detection.h
Normal 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
|
||||
120
engines/alcachofa/detection_tables.h
Normal file
120
engines/alcachofa/detection_tables.h
Normal file
@@ -0,0 +1,120 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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
|
||||
468
engines/alcachofa/game-movie-adventure.cpp
Normal file
468
engines/alcachofa/game-movie-adventure.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
1186
engines/alcachofa/game-objects.cpp
Normal file
1186
engines/alcachofa/game-objects.cpp
Normal file
File diff suppressed because it is too large
Load Diff
202
engines/alcachofa/game.cpp
Normal file
202
engines/alcachofa/game.cpp
Normal 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
110
engines/alcachofa/game.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef 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
|
||||
310
engines/alcachofa/general-objects.cpp
Normal file
310
engines/alcachofa/general-objects.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
265
engines/alcachofa/global-ui.cpp
Normal file
265
engines/alcachofa/global-ui.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
73
engines/alcachofa/global-ui.h
Normal file
73
engines/alcachofa/global-ui.h
Normal 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
|
||||
141
engines/alcachofa/graphics-opengl-base.cpp
Normal file
141
engines/alcachofa/graphics-opengl-base.cpp
Normal 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
|
||||
|
||||
}
|
||||
56
engines/alcachofa/graphics-opengl-base.h
Normal file
56
engines/alcachofa/graphics-opengl-base.h
Normal 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
|
||||
225
engines/alcachofa/graphics-opengl-classic.cpp
Normal file
225
engines/alcachofa/graphics-opengl-classic.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
263
engines/alcachofa/graphics-opengl-shaders.cpp
Normal file
263
engines/alcachofa/graphics-opengl-shaders.cpp
Normal file
@@ -0,0 +1,263 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
}
|
||||
201
engines/alcachofa/graphics-opengl.cpp
Normal file
201
engines/alcachofa/graphics-opengl.cpp
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
68
engines/alcachofa/graphics-opengl.h
Normal file
68
engines/alcachofa/graphics-opengl.h
Normal 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
|
||||
324
engines/alcachofa/graphics-tinygl.cpp
Normal file
324
engines/alcachofa/graphics-tinygl.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
999
engines/alcachofa/graphics.cpp
Normal file
999
engines/alcachofa/graphics.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
485
engines/alcachofa/graphics.h
Normal file
485
engines/alcachofa/graphics.h
Normal 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
105
engines/alcachofa/input.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "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
71
engines/alcachofa/input.h
Normal 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
375
engines/alcachofa/menu.cpp
Normal file
@@ -0,0 +1,375 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#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
108
engines/alcachofa/menu.h
Normal 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
|
||||
129
engines/alcachofa/metaengine.cpp
Normal file
129
engines/alcachofa/metaengine.cpp
Normal 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
|
||||
59
engines/alcachofa/metaengine.h
Normal file
59
engines/alcachofa/metaengine.h
Normal 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
|
||||
58
engines/alcachofa/module.mk
Normal file
58
engines/alcachofa/module.mk
Normal 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
605
engines/alcachofa/objects.h
Normal 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 *¤tlyUsing() { 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
|
||||
395
engines/alcachofa/player.cpp
Normal file
395
engines/alcachofa/player.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
85
engines/alcachofa/player.h
Normal file
85
engines/alcachofa/player.h
Normal 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
802
engines/alcachofa/rooms.cpp
Normal 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
214
engines/alcachofa/rooms.h
Normal 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 *¤tSlideButton() { 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
|
||||
351
engines/alcachofa/scheduler.cpp
Normal file
351
engines/alcachofa/scheduler.cpp
Normal file
@@ -0,0 +1,351 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
227
engines/alcachofa/scheduler.h
Normal file
227
engines/alcachofa/scheduler.h
Normal 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
|
||||
118
engines/alcachofa/script-debug.h
Normal file
118
engines/alcachofa/script-debug.h
Normal file
@@ -0,0 +1,118 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef 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
1007
engines/alcachofa/script.cpp
Normal file
File diff suppressed because it is too large
Load Diff
191
engines/alcachofa/script.h
Normal file
191
engines/alcachofa/script.h
Normal 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
679
engines/alcachofa/shape.cpp
Normal 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
258
engines/alcachofa/shape.h
Normal 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
|
||||
397
engines/alcachofa/sounds.cpp
Normal file
397
engines/alcachofa/sounds.cpp
Normal 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
108
engines/alcachofa/sounds.h
Normal 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
56
engines/alcachofa/tasks.h
Normal 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
|
||||
344
engines/alcachofa/ui-objects.cpp
Normal file
344
engines/alcachofa/ui-objects.cpp
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user