mirror of
https://github.com/scummvm/scummvm.git
synced 2026-06-20 05:45:29 +00:00
597 lines
16 KiB
C++
597 lines
16 KiB
C++
/* 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/system.h"
|
|
#include "common/hashmap.h"
|
|
|
|
#include "mtropolis/assets.h"
|
|
#include "mtropolis/detection.h"
|
|
#include "mtropolis/hacks.h"
|
|
#include "mtropolis/runtime.h"
|
|
#include "mtropolis/modifiers.h"
|
|
#include "mtropolis/saveload.h"
|
|
|
|
namespace MTropolis {
|
|
|
|
Hacks::Hacks() {
|
|
ignoreMismatchedProjectNameInObjectLookups = false;
|
|
}
|
|
|
|
Hacks::~Hacks() {
|
|
}
|
|
|
|
void Hacks::addStructuralHooks(uint32 guid, const Common::SharedPtr<StructuralHooks> &hooks) {
|
|
structuralHooks[guid] = hooks;
|
|
}
|
|
|
|
void Hacks::addModifierHooks(uint32 guid, const Common::SharedPtr<ModifierHooks> &hooks) {
|
|
modifierHooks[guid] = hooks;
|
|
}
|
|
|
|
void Hacks::addAssetHooks(const Common::SharedPtr<AssetHooks> &hooks) {
|
|
assetHooks.push_back(hooks);
|
|
}
|
|
|
|
void Hacks::addSceneTransitionHooks(const Common::SharedPtr<SceneTransitionHooks> &hooks) {
|
|
sceneTransitionHooks.push_back(hooks);
|
|
}
|
|
|
|
void Hacks::addSaveLoadHooks(const Common::SharedPtr<SaveLoadHooks> &hooks) {
|
|
saveLoadHooks.push_back(hooks);
|
|
}
|
|
|
|
namespace HackSuites {
|
|
|
|
class ObsidianCorruptedAirTowerTransitionFix : public AssetHooks {
|
|
public:
|
|
void onLoaded(Asset *asset, const Common::String &name) override;
|
|
};
|
|
|
|
void ObsidianCorruptedAirTowerTransitionFix::onLoaded(Asset *asset, const Common::String &name) {
|
|
if (asset->getAssetType() == kAssetTypeMovie && name == "A105_132.01Fxx.trn") {
|
|
static_cast<MovieAsset *>(asset)->addDamagedFrame(35);
|
|
}
|
|
}
|
|
|
|
class ObsidianInventoryWindscreenHooks : public StructuralHooks {
|
|
public:
|
|
void onSetPosition(Structural *structural, Common::Point &pt) override;
|
|
};
|
|
|
|
void ObsidianInventoryWindscreenHooks::onSetPosition(Structural *structural, Common::Point &pt) {
|
|
if (pt.y < 480) {
|
|
// Set direct to screen so it draws over cinematics
|
|
static_cast<VisualElement *>(structural)->setDirectToScreen(true);
|
|
|
|
// Move in-bounds
|
|
pt.y -= 60;
|
|
}
|
|
}
|
|
|
|
void addObsidianBugFixes(const MTropolisGameDescription &desc, Hacks &hacks) {
|
|
// Workaround for bug in Obsidian:
|
|
// When opening the journal in the intro, a script checks if cGSt.cfst.binjournal is false and if so,
|
|
// sets cGSt.cfst.binjournal to true and then sets including setting cJournalConst.aksjournpath to the
|
|
// main journal scene path. That scene path is used to resolve the scene to go to after clicking
|
|
// the "Continue" button on the warning that pops up.
|
|
//
|
|
// The problem is that cJournalConst uses a project name that doesn't match the retail data, and
|
|
// cJournalConst is unloaded if the player leaves the journal. This causes a progression blocker if
|
|
// the player leaves the journal without clicking Continue.
|
|
hacks.ignoreMismatchedProjectNameInObjectLookups = true;
|
|
|
|
// Fix for corrupted frame in transition from the outer edge in Spider to the air puzzle tower.
|
|
// The data is corrupted in both Mac and Win retail versions.
|
|
hacks.addAssetHooks(Common::SharedPtr<AssetHooks>(new ObsidianCorruptedAirTowerTransitionFix()));
|
|
}
|
|
|
|
void addObsidianImprovedWidescreen(const MTropolisGameDescription &desc, Hacks &hacks) {
|
|
if ((desc.desc.flags & ADGF_DEMO) == 0 && desc.desc.language == Common::EN_ANY && desc.desc.platform == Common::kPlatformWindows) {
|
|
const uint32 inventoryItemGUIDs[] = {
|
|
// Bureau documents
|
|
// 100 area (booths)
|
|
0x4e2d9e,
|
|
0x4de654,
|
|
|
|
// 199 area (booths hint room)
|
|
0x4e2555,
|
|
0x4de654,
|
|
|
|
// 200 area (library)
|
|
0x4c83d2,
|
|
0x4c5802,
|
|
|
|
// 299 area (Cloud Ring)
|
|
0x178d5c,
|
|
0x177754,
|
|
|
|
// 300 area (light+phone)
|
|
0x4e0f86,
|
|
0x4e5107,
|
|
|
|
// 400 area (maze)
|
|
0x4e5528,
|
|
0x4e55cc,
|
|
|
|
// 500 area (Immediate Action)
|
|
0x4e2e7b,
|
|
0x4e0710,
|
|
|
|
// 800 area (bookshelves)
|
|
0x9914fb,
|
|
0x990f1f,
|
|
|
|
// 600 (sky face), 699 (mountain), and 700 (finale) have no document elements (player loses the documents)
|
|
|
|
// Bureau maze keycards
|
|
0x6035f,
|
|
0x62e24,
|
|
0x58d7f,
|
|
0x58212,
|
|
|
|
// Spider metal puzzle beaker
|
|
0x12fa7,
|
|
|
|
// Inspiration chip
|
|
// 100 area (Junkyard)
|
|
0x5f02e6,
|
|
|
|
// 200 area (Plane)
|
|
0x9bd5fc,
|
|
|
|
// 300 area (Piazza)
|
|
0x5ef979,
|
|
|
|
// 400 area (Church)
|
|
0xed9a8f,
|
|
|
|
// 500 area (Statue)
|
|
0x5ecdee,
|
|
};
|
|
|
|
Common::SharedPtr<StructuralHooks> invItemHooks(new ObsidianInventoryWindscreenHooks());
|
|
|
|
for (uint32 guid : inventoryItemGUIDs)
|
|
hacks.addStructuralHooks(guid, invItemHooks);
|
|
}
|
|
if ((desc.desc.flags & ADGF_DEMO) == 0 && desc.desc.language == Common::EN_ANY && desc.desc.platform == Common::kPlatformMacintosh) {
|
|
const uint32 inventoryItemGUIDs[] = {
|
|
// Bureau documents
|
|
// 100 area (booths)
|
|
0x4dfa22,
|
|
0x4e2d9e,
|
|
|
|
// 199 area (booths hint room)
|
|
0x4e2555,
|
|
0x4de654,
|
|
|
|
// 200 area (library)
|
|
0x4c83d2,
|
|
0x4c5802,
|
|
|
|
// 299 area (Cloud Ring)
|
|
0x178d5c,
|
|
0x177754,
|
|
|
|
// 300 area (light+phone)
|
|
0x4e0f86,
|
|
0x4e5107,
|
|
|
|
// 400 area (maze)
|
|
0x4e5528,
|
|
0x4e55cc,
|
|
|
|
// 500 area (Immediate Action)
|
|
0x4a2e7b,
|
|
0x4e0710,
|
|
|
|
// 800 area (bookshelves)
|
|
0x9914fb,
|
|
0x990f1f,
|
|
|
|
// 600 (sky face), 699 (mountain), and 700 (finale) have no document elements (player loses the documents)
|
|
|
|
// Bureau maze keycards
|
|
0x6035f,
|
|
0x62e24,
|
|
0x58d7f,
|
|
0x58212,
|
|
|
|
// Spider metal puzzle beaker
|
|
0x12fa7,
|
|
|
|
// Inspiration chip
|
|
// 100 area (Junkyard)
|
|
0x5f02e6,
|
|
|
|
// 200 area (Plane)
|
|
0x9bd5fc,
|
|
|
|
// 300 area (Piazza)
|
|
0x5ef979,
|
|
|
|
// 400 area (Church)
|
|
0xed9a8f,
|
|
|
|
// 500 area (Statue)
|
|
0x5ecdee,
|
|
};
|
|
|
|
Common::SharedPtr<StructuralHooks> invItemHooks(new ObsidianInventoryWindscreenHooks());
|
|
|
|
for (uint32 guid : inventoryItemGUIDs)
|
|
hacks.addStructuralHooks(guid, invItemHooks);
|
|
}
|
|
}
|
|
|
|
// Auto-save triggers for Obsidian. Basically, we auto-save on reaching specific scenes when conditions are met.
|
|
// There are two types of condition: One is the player reaches the scene from a one-way scene that can not be
|
|
// revisited, such as a chapter transition.
|
|
//
|
|
// The other is a variable latch, which happens if the variable became true since the last time the reset scene
|
|
// (the opening credits) was reached, or since the last time the game was loaded.
|
|
//
|
|
// Variable latches don't work if the latch becomes true and the player saves+reloads their game before they hit
|
|
// the auto-save checkpoint, but that's okay.
|
|
|
|
struct ObsidianAutoSaveTrigger {
|
|
const char *sceneName;
|
|
const char *priorSceneName; // If set, only save when transitioning from this scene
|
|
const char *varTrueLatch; // If set, only save when this variable was set to
|
|
};
|
|
|
|
static ObsidianAutoSaveTrigger kObsidianAutoSaveTriggers[] = {
|
|
// Arrive at campsite
|
|
{
|
|
"103.2L",
|
|
"102_103_Credits",
|
|
nullptr,
|
|
},
|
|
// Bureau start
|
|
{
|
|
"102.0L",
|
|
nullptr,
|
|
"cgst.clst.cl100st.bbossspoken"
|
|
},
|
|
// Win cube maze from side room
|
|
{
|
|
"445.2L",
|
|
nullptr,
|
|
"cgst.clst.cl400st.bcubiclewon",
|
|
},
|
|
// Win cube maze from back room
|
|
{
|
|
"445.0L",
|
|
nullptr,
|
|
"cgst.clst.cl400st.bcubiclewon",
|
|
},
|
|
// Stamp document and back away
|
|
{
|
|
"445.2L",
|
|
nullptr,
|
|
"cgst.clst.cinv.bstampedsd",
|
|
},
|
|
// Get repair document from cabinet + back away (English version)
|
|
{
|
|
"218.4L",
|
|
nullptr,
|
|
"cgst.clst.cinv.bhavesd",
|
|
},
|
|
// Get repair document from Bridge Repair + back away (non-English versions)
|
|
{
|
|
"109.6L",
|
|
nullptr,
|
|
"cgst.clst.cinv.bhavesd",
|
|
},
|
|
// Fix Immediate Action clock + exit left side
|
|
{
|
|
"504.0L",
|
|
nullptr,
|
|
"cgst.clst.cl500st.bwonl505",
|
|
},
|
|
// Fix Immediate Action clock + exit right side
|
|
{
|
|
"504.4L",
|
|
nullptr,
|
|
"cgst.clst.cl500st.bwonl505",
|
|
},
|
|
// Give document to Immediate Action + return to light
|
|
{
|
|
"306.6L",
|
|
nullptr,
|
|
"cgst.clst.cinv.bgavesd",
|
|
},
|
|
// Get rebel document + leave cabinet (English version)
|
|
{
|
|
"227.4L",
|
|
nullptr,
|
|
"cgst.clst.cinv.bhaveom",
|
|
},
|
|
// Solve dial puzzle
|
|
{
|
|
"699.0L",
|
|
nullptr,
|
|
"cgst.clst.cl308st.bwon",
|
|
},
|
|
|
|
// Spider start
|
|
{
|
|
"101.2L",
|
|
"710.0L",
|
|
nullptr,
|
|
},
|
|
// Leave elevator after completing any Spider puzzle
|
|
{
|
|
"121.0L",
|
|
"118.0L_121.0L-s1",
|
|
nullptr,
|
|
},
|
|
|
|
// Inspiration start
|
|
{
|
|
"101.4L",
|
|
"121.0L_Lunch_Time",
|
|
nullptr,
|
|
},
|
|
// Complete propulsion puzzle and leave engine room
|
|
{
|
|
"201.4L",
|
|
nullptr,
|
|
"cgst.cbst.cb2st.bengineon",
|
|
},
|
|
// Complete Church puzzle and leave robot
|
|
{
|
|
"412.4L",
|
|
nullptr,
|
|
"cgst.cbst.cb4st.bwon",
|
|
},
|
|
// Complete statue canvas puzzle
|
|
{
|
|
"523.2L_from_521.2L-s2",
|
|
nullptr,
|
|
"cgst.cbst.cb5st.bpaintedblank",
|
|
},
|
|
|
|
// Conductor start
|
|
{
|
|
"101.6L",
|
|
"203.4L_WIN",
|
|
nullptr,
|
|
},
|
|
// Freed Max
|
|
{
|
|
"104.0L_Max_Freed",
|
|
nullptr,
|
|
"cgst.ccst.cc1st.bmaxfreed",
|
|
},
|
|
};
|
|
|
|
class ObsidianAutoSaveVarsState {
|
|
public:
|
|
ObsidianAutoSaveVarsState();
|
|
|
|
static const VariableModifier *findVar(Runtime *runtime, const Common::String &str);
|
|
|
|
bool getVarState(const Common::String &varName) const;
|
|
void resyncAllVars(Runtime *runtime);
|
|
|
|
private:
|
|
Common::HashMap<Common::String, bool> _varState;
|
|
};
|
|
|
|
ObsidianAutoSaveVarsState::ObsidianAutoSaveVarsState() {
|
|
for (const ObsidianAutoSaveTrigger &trigger : kObsidianAutoSaveTriggers) {
|
|
if (trigger.varTrueLatch)
|
|
_varState[trigger.varTrueLatch] = false;
|
|
}
|
|
}
|
|
|
|
const VariableModifier *ObsidianAutoSaveVarsState::findVar(Runtime *runtime, const Common::String &str) {
|
|
size_t scanStartPos = 0;
|
|
|
|
const Modifier *modifierScan = nullptr;
|
|
const IModifierContainer *container = runtime->getProject();
|
|
|
|
for (;;) {
|
|
size_t dotPos = str.findFirstOf('.', scanStartPos);
|
|
if (dotPos == Common::String::npos)
|
|
dotPos = str.size();
|
|
|
|
Common::String childName = str.substr(scanStartPos, dotPos - scanStartPos);
|
|
if (!container)
|
|
return nullptr;
|
|
|
|
modifierScan = nullptr;
|
|
for (const Common::SharedPtr<Modifier> &modifier : container->getModifiers()) {
|
|
if (caseInsensitiveEqual(childName, modifier->getName())) {
|
|
modifierScan = modifier.get();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!modifierScan)
|
|
return nullptr;
|
|
|
|
if (modifierScan->isCompoundVariable())
|
|
container = static_cast<const CompoundVariableModifier *>(modifierScan);
|
|
|
|
if (dotPos == str.size())
|
|
break;
|
|
|
|
scanStartPos = dotPos + 1;
|
|
}
|
|
|
|
if (modifierScan && modifierScan->isVariable())
|
|
return static_cast<const VariableModifier *>(modifierScan);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool ObsidianAutoSaveVarsState::getVarState(const Common::String &varName) const {
|
|
Common::HashMap<Common::String, bool>::const_iterator it = _varState.find(varName);
|
|
if (it == _varState.end())
|
|
return false;
|
|
return it->_value;
|
|
}
|
|
|
|
void ObsidianAutoSaveVarsState::resyncAllVars(Runtime *runtime) {
|
|
for (Common::HashMap<Common::String, bool>::iterator it = _varState.begin(), itEnd = _varState.end(); it != itEnd; ++it) {
|
|
const VariableModifier *var = findVar(runtime, it->_key);
|
|
if (var) {
|
|
DynamicValue varValue;
|
|
var->varGetValue(nullptr, varValue);
|
|
assert(varValue.getType() == DynamicValueTypes::kBoolean);
|
|
|
|
it->_value = varValue.getBool();
|
|
}
|
|
}
|
|
}
|
|
|
|
class ObsidianAutoSaveSceneTransitionHooks : public SceneTransitionHooks {
|
|
public:
|
|
explicit ObsidianAutoSaveSceneTransitionHooks(const Common::SharedPtr<ObsidianAutoSaveVarsState> &vars, IAutoSaveProvider *autoSaveProvider);
|
|
|
|
void onSceneTransitionEnded(Runtime *runtime, const Common::WeakPtr<Structural> &newScene) override;
|
|
|
|
private:
|
|
Common::SharedPtr<ObsidianAutoSaveVarsState> _varsState;
|
|
IAutoSaveProvider *_autoSaveProvider;
|
|
|
|
Common::String _currentSceneName;
|
|
Common::String _prevSceneName;
|
|
|
|
Common::String _resetSceneName;
|
|
Common::String _saveVarName;
|
|
};
|
|
|
|
ObsidianAutoSaveSceneTransitionHooks::ObsidianAutoSaveSceneTransitionHooks(const Common::SharedPtr<ObsidianAutoSaveVarsState> &vars, IAutoSaveProvider *autoSaveProvider)
|
|
: _varsState(vars), _autoSaveProvider(autoSaveProvider) {
|
|
|
|
_resetSceneName = Common::String("101_102_Credits");
|
|
_saveVarName = Common::String("cgst");
|
|
}
|
|
|
|
void ObsidianAutoSaveSceneTransitionHooks::onSceneTransitionEnded(Runtime *runtime, const Common::WeakPtr<Structural> &newScene) {
|
|
bool triggerAutoSave = false;
|
|
|
|
if (newScene.expired())
|
|
return;
|
|
|
|
_prevSceneName = _currentSceneName;
|
|
_currentSceneName = newScene.lock()->getName();
|
|
|
|
for (const ObsidianAutoSaveTrigger &trigger : kObsidianAutoSaveTriggers) {
|
|
Common::String triggerSceneName(trigger.sceneName);
|
|
|
|
if (!caseInsensitiveEqual(triggerSceneName, _currentSceneName))
|
|
continue;
|
|
|
|
if (trigger.priorSceneName && !caseInsensitiveEqual(trigger.priorSceneName, _prevSceneName))
|
|
continue;
|
|
|
|
if (trigger.varTrueLatch) {
|
|
Common::String varName(trigger.varTrueLatch);
|
|
|
|
// Variable must must have been false since the last game load or reset
|
|
if (_varsState->getVarState(varName))
|
|
continue;
|
|
|
|
bool passedLatchTest = false;
|
|
|
|
const VariableModifier *var = _varsState->findVar(runtime, varName);
|
|
if (var) {
|
|
DynamicValue varValue;
|
|
var->varGetValue(nullptr, varValue);
|
|
assert(varValue.getType() == DynamicValueTypes::kBoolean);
|
|
|
|
passedLatchTest = varValue.getBool();
|
|
}
|
|
|
|
if (!passedLatchTest)
|
|
continue;
|
|
}
|
|
|
|
triggerAutoSave = true;
|
|
break;
|
|
}
|
|
|
|
if (triggerAutoSave) {
|
|
Common::SharedPtr<Modifier> saveVar;
|
|
|
|
for (const Common::SharedPtr<Modifier> &child : runtime->getProject()->getModifiers()) {
|
|
if (caseInsensitiveEqual(child->getName(), _saveVarName)) {
|
|
saveVar = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (saveVar && saveVar->isModifier()) {
|
|
Modifier *modifier = static_cast<Modifier *>(saveVar.get());
|
|
Common::SharedPtr<ModifierSaveLoad> saveLoad = modifier->getSaveLoad();
|
|
|
|
if (saveLoad) {
|
|
CompoundVarSaver saver(saveVar.get());
|
|
_autoSaveProvider->autoSave(&saver);
|
|
|
|
_varsState->resyncAllVars(runtime);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (caseInsensitiveEqual(_currentSceneName, _resetSceneName))
|
|
_varsState->resyncAllVars(runtime);
|
|
}
|
|
|
|
class ObsidianAutoSaveSaveLoadHooks : public SaveLoadHooks {
|
|
public:
|
|
explicit ObsidianAutoSaveSaveLoadHooks(const Common::SharedPtr<ObsidianAutoSaveVarsState> &vars);
|
|
|
|
void onSave(Runtime *runtime, Modifier *saveLoadModifier, Modifier *varModifier) override;
|
|
void onLoad(Runtime *runtime, Modifier *saveLoadModifier, Modifier *varModifier) override;
|
|
|
|
private:
|
|
Common::SharedPtr<ObsidianAutoSaveVarsState> _varsState;
|
|
};
|
|
|
|
|
|
ObsidianAutoSaveSaveLoadHooks::ObsidianAutoSaveSaveLoadHooks(const Common::SharedPtr<ObsidianAutoSaveVarsState> &vars) : _varsState(vars) {
|
|
}
|
|
|
|
void ObsidianAutoSaveSaveLoadHooks::onSave(Runtime *runtime, Modifier *saveLoadModifier, Modifier *varModifier) {
|
|
// Reset all variable latches on save
|
|
_varsState->resyncAllVars(runtime);
|
|
}
|
|
|
|
void ObsidianAutoSaveSaveLoadHooks::onLoad(Runtime *runtime, Modifier *saveLoadModifier, Modifier *varModifier) {
|
|
// Reset all variable latches on load
|
|
_varsState->resyncAllVars(runtime);
|
|
}
|
|
|
|
void addObsidianAutoSaves(const MTropolisGameDescription &desc, Hacks &hacks, IAutoSaveProvider *autoSaveProvider) {
|
|
Common::SharedPtr<ObsidianAutoSaveVarsState> varsState(new ObsidianAutoSaveVarsState());
|
|
hacks.addSceneTransitionHooks(Common::SharedPtr<SceneTransitionHooks>(new ObsidianAutoSaveSceneTransitionHooks(varsState, autoSaveProvider)));
|
|
hacks.addSaveLoadHooks(Common::SharedPtr<SaveLoadHooks>(new ObsidianAutoSaveSaveLoadHooks(varsState)));
|
|
}
|
|
|
|
} // End of namespace HackSuites
|
|
|
|
} // End of namespace MTropolis
|