/* 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 .
*
*/
#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 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(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(
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 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 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(Rect(0, 0, width, borderWidth), kBlack);
g_engine->drawQueue().add(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();
}
}