/* 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 "common/file.h"
#include "common/random.h"
#include "mtropolis/plugin/mti.h"
#include "mtropolis/plugins.h"
#include "mtropolis/miniscript.h"
#include "video/mpegps_decoder.h"
#include "graphics/managed_surface.h"
#include "common/file.h"
namespace MTropolis {
namespace MTI {
/*
Board layout:
Layer 0:
0 1 2 3 4 5 6 7 8 9 10 11 12
+-----+-----+-----+-----+-----+-----+
0 + 0 | 1 | 2 | 3 | 4 | 5 |
1 +-----+-----+-----+-----+-----+-----+
2 +-----+ 7 | 8 | 9 | 10 +-----+-----+
3 | 6 +-----+-----+-----+-----+ 11 | 12 |
4 +-----+ 13 | 14 | 15 | 16 +-----+-----+
5 +-----+-----+-----+-----+-----+-----+
6 + 17 | 18 | 19 | 20 | 21 | 22 |
+-----+-----+-----+-----+-----+-----+
Layer 1:
0 1 2 3 4 5 6 7 8 9 10 11 12
0
1 +-----+-----+
2 | 23 | 24 |
3 +-----+-----+
4 | 25 | 26 |
5 +-----+-----+
6
Layer 2:
0 1 2 3 4 5 6 7 8 9 10 11 12
0
1
2 +-----+
3 | 27 |
4 +-----+
5
6
*/
ShanghaiModifier::TileCoordinate ShanghaiModifier::_tileCoordinates[ShanghaiModifier::kNumTiles] = {
{0, 0, 0},
{2, 0, 0},
{4, 0, 0},
{6, 0, 0},
{8, 0, 0},
{10, 0, 0},
{0, 3, 0},
{2, 2, 0},
{4, 2, 0},
{6, 2, 0},
{8, 2, 0},
{10, 3, 0},
{12, 3, 0},
{2, 4, 0},
{4, 4, 0},
{6, 4, 0},
{8, 4, 0},
{0, 6, 0},
{2, 6, 0},
{4, 6, 0},
{6, 6, 0},
{8, 6, 0},
{10, 6, 0},
{4, 2, 0},
{6, 2, 0},
{4, 4, 1},
{6, 4, 1},
{5, 3, 2},
};
ShanghaiModifier::ShanghaiModifier() {
for (uint x = 0; x < kBoardSizeX; x++)
for (uint y = 0; y < kBoardSizeY; y++)
for (uint z = 0; z < kBoardSizeZ; z++)
_tileAtCoordinate[x][y][z] = -1;
for (uint i = 0; i < kNumTiles; i++) {
const TileCoordinate &coord = _tileCoordinates[i];
assert(coord.x < kBoardSizeX);
assert(coord.y < kBoardSizeY);
assert(coord.z < kBoardSizeZ);
_tileAtCoordinate[coord.x][coord.y][coord.z] = i;
}
}
ShanghaiModifier::~ShanghaiModifier() {
}
bool ShanghaiModifier::respondsToEvent(const Event &evt) const {
if (_resetTileSetWhen.respondsTo(evt))
return true;
return false;
}
VThreadState ShanghaiModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr &msg) {
if (_resetTileSetWhen.respondsTo(msg->getEvent())) {
uint tileFaces[kNumTiles];
resetTiles(*runtime->getRandom(), tileFaces);
Modifier *varMod = this->_tileSetRef.resolution.lock().get();
if (varMod == nullptr || !varMod->isVariable()) {
warning("Shanghai reset var ref was unavailable");
return kVThreadError;
}
VariableModifier *var = static_cast(varMod);
Common::SharedPtr list(new DynamicList());
for (uint i = 0; i < kNumTiles; i++) {
DynamicValue tileValue;
tileValue.setInt(tileFaces[i]);
list->setAtIndex(i, tileValue);
}
DynamicValue listValue;
listValue.setList(list);
MiniscriptThread thread(runtime, nullptr, nullptr, nullptr, this);
var->varSetValue(&thread, listValue);
return kVThreadReturn;
}
return kVThreadReturn;
}
void ShanghaiModifier::disable(Runtime *runtime) {
}
bool ShanghaiModifier::load(const PlugInModifierLoaderContext &context, const Data::MTI::ShanghaiModifier &data) {
if (data.resetWhen.type != Data::PlugInTypeTaggedValue::kEvent)
return false;
if (!_resetTileSetWhen.load(data.resetWhen.value.asEvent))
return false;
if (data.tileSetVar.type != Data::PlugInTypeTaggedValue::kVariableReference)
return false;
_tileSetRef = VarReference(data.tileSetVar.value.asVarRefGUID, "");
return true;
}
void ShanghaiModifier::linkInternalReferences(ObjectLinkingScope *scope) {
_tileSetRef.linkInternalReferences(scope);
}
void ShanghaiModifier::visitInternalReferences(IStructuralReferenceVisitor *visitor) {
_tileSetRef.visitInternalReferences(visitor);
}
void ShanghaiModifier::resetTiles(Common::RandomSource &rng, uint (&tileFaces)[kNumTiles]) const {
uint possibleFaces[kNumFaces];
uint numPossibleFaces = kNumFaces;
for (uint i = 0; i < kNumFaces; i++)
possibleFaces[i] = i + 1;
uint facesToInsert[kNumTiles / 2];
uint numFacesToInsert = kNumTiles / 2;
// Pick random faces, each one gets inserted twice
for (uint i = 0; i < kNumTiles / 4u; i++) {
uint faceToInsert = selectAndRemoveOne(rng, possibleFaces, numPossibleFaces);
facesToInsert[i * 2 + 0] = faceToInsert;
facesToInsert[i * 2 + 1] = faceToInsert;
}
// We build the board by adding all tiles and then randomly picking 2 exposed tiles and
// assigning them a matching pair. A pair is only valid if the resulting board state has
// valid moves.
BoardState_t boardState = emptyBoardState();
for (uint i = 0; i < kNumTiles; i++)
boardState = boardState | boardStateBit(i);
for (uint pair = 0; pair < kNumTiles / 2u; pair++) {
uint exposedTiles[kNumTiles];
uint numExposedTiles = 0;
for (uint i = 0; i < kNumTiles; i++) {
if (boardState & boardStateBit(i)) {
if (tileIsExposed(boardState, i))
exposedTiles[numExposedTiles++] = i;
}
}
uint firstExposedTile = selectAndRemoveOne(rng, exposedTiles, numExposedTiles);
BoardState_t withFirstRemoved = boardState ^ boardStateBit(firstExposedTile);
uint secondExposedTile = selectAndRemoveOne(rng, exposedTiles, numExposedTiles);
BoardState_t withBothRemoved = withFirstRemoved ^ boardStateBit(secondExposedTile);
if (numExposedTiles > 0) {
// If this isn't the last move, validate that this won't result in a stuck board state (e.g. only one tile exposed)
// If it would result in such a state, pick a different move.
for (;;) {
if (boardStateHasValidMove(withBothRemoved))
break;
if (numExposedTiles == 0) {
error("Shanghai board creation failed, board state was %x, removed %u to produce board state %x", static_cast(boardState), firstExposedTile, static_cast(withFirstRemoved));
break;
}
secondExposedTile = selectAndRemoveOne(rng, exposedTiles, numExposedTiles);
withBothRemoved = withFirstRemoved ^ boardStateBit(secondExposedTile);
}
}
boardState = withBothRemoved;
uint faceToInsert = selectAndRemoveOne(rng, facesToInsert, numFacesToInsert);
tileFaces[firstExposedTile] = faceToInsert;
tileFaces[secondExposedTile] = faceToInsert;
debug(2, "Shanghai randomizer: Move %u is %u + %u", pair, firstExposedTile, secondExposedTile);
}
}
uint ShanghaiModifier::selectAndRemoveOne(Common::RandomSource &rng, uint *valuesList, uint &listSize) {
if (listSize == 0) {
error("Internal error: selectAndRemoveOne ran out of values");
return 0;
}
if (listSize == 1) {
listSize = 0;
return valuesList[0];
}
uint selectedIndex = rng.getRandomNumber(listSize - 1);
uint selectedValue = valuesList[selectedIndex];
valuesList[selectedIndex] = valuesList[listSize - 1];
listSize--;
return selectedValue;
}
bool ShanghaiModifier::boardStateHasValidMove(BoardState_t boardState) const {
uint numExposedTiles = 0;
for (uint i = 0; i < kNumTiles; i++) {
if (boardState & boardStateBit(i)) {
if (tileIsExposed(boardState, i)) {
numExposedTiles++;
if (numExposedTiles == 2)
return true;
}
}
}
return false;
}
bool ShanghaiModifier::tileIsExposed(BoardState_t boardState, uint tile) const {
uint tileX = _tileCoordinates[tile].x;
uint tileY = _tileCoordinates[tile].y;
uint tileZ = _tileCoordinates[tile].z;
uint blockMinY = tileY;
uint blockMaxY = tileY;
if (blockMinY > 0)
blockMinY--;
if (blockMaxY < kBoardSizeY - 1u)
blockMaxY++;
bool blockedOnLeft = false;
if (tileX >= 2) {
// Check for left-side blocks
for (uint y = blockMinY; y <= blockMaxY; y++) {
if (tileExistsAtCoordinate(boardState, tileX - 2, y, tileZ)) {
blockedOnLeft = true;
break;
}
}
}
if (blockedOnLeft) {
bool blockedOnRight = false;
// Check for right-side blocks
if (tileX < kBoardSizeX - 2u) {
for (uint y = blockMinY; y <= blockMaxY; y++) {
if (tileExistsAtCoordinate(boardState, tileX + 2, y, tileZ)) {
blockedOnRight = true;
break;
}
}
}
// Tile is blocked on left and right
if (blockedOnRight)
return false;
}
// Check upper blocks
uint blockMinX = tileX;
uint blockMaxX = tileX;
if (blockMinX > 0)
blockMinX--;
if (blockMaxX < kBoardSizeX - 1u)
blockMaxX++;
for (uint z = tileZ + 1; z < kBoardSizeZ; z++) {
for (uint x = blockMinX; x <= blockMaxX; x++) {
for (uint y = blockMinY; y <= blockMaxY; y++) {
if (tileExistsAtCoordinate(boardState, x, y, z))
return false;
}
}
}
return true;
}
bool ShanghaiModifier::tileExistsAtCoordinate(BoardState_t boardState, uint x, uint y, uint z) const {
assert(x < kBoardSizeX);
assert(y < kBoardSizeY);
assert(z < kBoardSizeZ);
int8 tile = _tileAtCoordinate[x][y][z];
if (tile < 0)
return false;
if (boardState & boardStateBit(static_cast(tile)))
return true;
return false;
}
ShanghaiModifier::BoardState_t ShanghaiModifier::boardStateBit(uint bit) {
return static_cast(1) << bit;
}
ShanghaiModifier::BoardState_t ShanghaiModifier::emptyBoardState() {
return 0;
}
#ifdef MTROPOLIS_DEBUG_ENABLE
void ShanghaiModifier::debugInspect(IDebugInspectionReport *report) const {
}
#endif
Common::SharedPtr ShanghaiModifier::shallowClone() const {
return Common::SharedPtr(new ShanghaiModifier(*this));
}
const char *ShanghaiModifier::getDefaultName() const {
return "Shanghai Modifier"; // ???
}
class MPEGVideoPlayer : public IPostEffect, public IPlayMediaSignalReceiver {
public:
explicit MPEGVideoPlayer(Runtime *runtime, const Common::SharedPtr &videoDecoder, IMPEGVideoCompletionNotifier *completionNotifier);
~MPEGVideoPlayer();
static Common::SharedPtr createForVideoID(Runtime *runtime, int videoID, IMPEGVideoCompletionNotifier *completionNotifier);
void playMedia(Runtime *runtime, Project *project) override;
void renderPostEffect(Graphics::ManagedSurface &surface) const override;
private:
Runtime *_runtime;
Project *_project;
IMPEGVideoCompletionNotifier *_completionNotifier;
const Graphics::Surface *_displayingSurface;
Common::SharedPtr _decoder;
Common::SharedPtr _playMediaReceiver;
bool _finished;
};
MPEGVideoPlayer::MPEGVideoPlayer(Runtime *runtime, const Common::SharedPtr &videoDecoder, IMPEGVideoCompletionNotifier *completionNotifier)
: _runtime(runtime), _project(nullptr), _decoder(videoDecoder), _finished(false), _displayingSurface(nullptr), _completionNotifier(completionNotifier) {
_project = runtime->getProject();
runtime->addPostEffect(this);
_playMediaReceiver = _project->notifyOnPlayMedia(this);
}
MPEGVideoPlayer::~MPEGVideoPlayer() {
_playMediaReceiver->removeReceiver(this);
_runtime->removePostEffect(this);
}
Common::SharedPtr MPEGVideoPlayer::createForVideoID(Runtime *runtime, int videoID, IMPEGVideoCompletionNotifier *completionNotifier) {
#ifdef USE_MPEG2
Common::String videoPath = Common::String::format("video/%i.vob", videoID);
Common::SharedPtr decoder(new Video::MPEGPSDecoder());
if (!decoder->loadFile(Common::Path(videoPath)))
return nullptr;
decoder->start();
return Common::SharedPtr(new MPEGVideoPlayer(runtime, decoder, completionNotifier));
#else
return nullptr;
#endif
}
void MPEGVideoPlayer::playMedia(Runtime *runtime, Project *project) {
if (_finished)
return;
while (_decoder->getTimeToNextFrame() == 0) {
const Graphics::Surface *newFrame = _decoder->decodeNextFrame();
if (newFrame) {
_displayingSurface = newFrame;
_runtime->setSceneGraphDirty();
} else {
_finished = true;
_displayingSurface = nullptr;
_completionNotifier->onVideoCompleted();
break;
}
}
}
void MPEGVideoPlayer::renderPostEffect(Graphics::ManagedSurface &surface) const {
if (_displayingSurface) {
const Graphics::Surface *surf = _displayingSurface;
Graphics::ManagedSurface *mainWindowSurf = _runtime->getMainWindow().lock()->getSurface().get();
int32 topLeftX = (mainWindowSurf->w - surf->w) / 2;
int32 topLeftY = (mainWindowSurf->h - surf->h) / 2;
Common::Rect fullVideoRect(topLeftX, topLeftY, topLeftX + surf->w, topLeftY + surf->h);
Common::Rect clippedVideoRect = fullVideoRect;
clippedVideoRect.clip(Common::Rect(0, 0, mainWindowSurf->w, mainWindowSurf->h));
Common::Rect videoSrcRect(0, 0, surf->w, surf->h);
videoSrcRect.left += clippedVideoRect.left - fullVideoRect.left;
videoSrcRect.right += clippedVideoRect.right - fullVideoRect.right;
videoSrcRect.top += clippedVideoRect.top - fullVideoRect.top;
videoSrcRect.bottom += clippedVideoRect.bottom - fullVideoRect.bottom;
mainWindowSurf->blitFrom(*surf, videoSrcRect, clippedVideoRect);
}
}
SampleModifier::SampleModifier() : _videoNumber(0), _runtime(nullptr), _isPlaying(false) {
}
SampleModifier::~SampleModifier() {
stopPlaying();
}
bool SampleModifier::respondsToEvent(const Event &evt) const {
return _executeWhen.respondsTo(evt);
}
VThreadState SampleModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr &msg) {
if (_executeWhen.respondsTo(msg->getEvent())) {
_runtime = runtime;
stopPlaying();
_vidPlayer.reset();
_vidPlayer = MPEGVideoPlayer::createForVideoID(runtime, _videoNumber, this);
if (_vidPlayer) {
runtime->addMouseBlocker();
runtime->getMainWindow().lock()->setMouseVisible(false);
runtime->setSceneGraphDirty();
_keySignaller = _runtime->getProject()->notifyOnKeyboardEvent(this);
_isPlaying = true;
} else {
warning("Attempted to play MPEG video %i but player setup failed", static_cast(_videoNumber));
}
return kVThreadReturn;
}
return kVThreadReturn;
}
void SampleModifier::disable(Runtime *runtime) {
stopPlaying();
_vidPlayer.reset();
}
bool SampleModifier::load(const PlugInModifierLoaderContext &context, const Data::MTI::SampleModifier &data) {
if (data.executeWhen.type != Data::PlugInTypeTaggedValue::kEvent)
return false;
if (data.videoNumber.type != Data::PlugInTypeTaggedValue::kInteger)
return false;
_videoNumber = data.videoNumber.value.asInt;
if (!_executeWhen.load(data.executeWhen.value.asEvent))
return false;
return true;
}
void SampleModifier::onVideoCompleted() {
stopPlaying();
}
void SampleModifier::onKeyboardEvent(Runtime *runtime, const KeyboardInputEvent &keyEvt) {
if (keyEvt.getKeyEventType() == Common::EVENT_KEYDOWN && keyEvt.getKeyState().keycode == Common::KEYCODE_SPACE) {
_vidPlayer.reset();
stopPlaying();
}
}
#ifdef MTROPOLIS_DEBUG_ENABLE
void SampleModifier::debugInspect(IDebugInspectionReport *report) const {
}
#endif
Common::SharedPtr SampleModifier::shallowClone() const {
return Common::SharedPtr(new SampleModifier(*this));
}
const char *SampleModifier::getDefaultName() const {
return "Sample Modifier";
}
void SampleModifier::stopPlaying() {
if (_isPlaying) {
_runtime->removeMouseBlocker();
_runtime->getMainWindow().lock()->setMouseVisible(true);
_keySignaller->removeReceiver(this);
_isPlaying = false;
}
}
MTIPlugIn::MTIPlugIn()
: _shanghaiModifierFactory(this), _sampleModifierFactory(this) {
}
void MTIPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
registrar->registerPlugInModifier("Shanghai", &_shanghaiModifierFactory);
registrar->registerPlugInModifier("Sample", &_sampleModifierFactory);
}
} // namespace MTI
namespace PlugIns {
Common::SharedPtr createMTI() {
return Common::SharedPtr(new MTI::MTIPlugIn());
}
} // End of namespace PlugIns
} // End of namespace MTropolis