mirror of https://github.com/OpenRCT2/OpenRCT2.git
565 lines
18 KiB
C++
565 lines
18 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2018 OpenRCT2 developers
|
|
*
|
|
* For a complete list of all authors, please refer to contributors.md
|
|
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
|
*
|
|
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
|
*****************************************************************************/
|
|
|
|
#include "TitleSequencePlayer.h"
|
|
|
|
#include "../interface/Window.h"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <openrct2/Context.h>
|
|
#include <openrct2/Game.h>
|
|
#include <openrct2/GameState.h>
|
|
#include <openrct2/OpenRCT2.h>
|
|
#include <openrct2/ParkImporter.h>
|
|
#include <openrct2/common.h>
|
|
#include <openrct2/core/Console.hpp>
|
|
#include <openrct2/core/Guard.hpp>
|
|
#include <openrct2/core/Path.hpp>
|
|
#include <openrct2/core/String.hpp>
|
|
#include <openrct2/interface/Viewport.h>
|
|
#include <openrct2/interface/Window.h>
|
|
#include <openrct2/management/NewsItem.h>
|
|
#include <openrct2/object/ObjectManager.h>
|
|
#include <openrct2/scenario/ScenarioRepository.h>
|
|
#include <openrct2/scenario/ScenarioSources.h>
|
|
#include <openrct2/title/TitleScreen.h>
|
|
#include <openrct2/title/TitleSequence.h>
|
|
#include <openrct2/title/TitleSequenceManager.h>
|
|
#include <openrct2/title/TitleSequencePlayer.h>
|
|
#include <openrct2/ui/UiContext.h>
|
|
#include <openrct2/ui/WindowManager.h>
|
|
#include <openrct2/windows/Intent.h>
|
|
#include <openrct2/world/Map.h>
|
|
#include <openrct2/world/Scenery.h>
|
|
#include <openrct2/world/Sprite.h>
|
|
|
|
using namespace OpenRCT2;
|
|
|
|
class TitleSequencePlayer final : public ITitleSequencePlayer
|
|
{
|
|
private:
|
|
static constexpr const char* SFMM_FILENAME = "Six Flags Magic Mountain.SC6";
|
|
|
|
IScenarioRepository& _scenarioRepository;
|
|
GameState& _gameState;
|
|
|
|
size_t _sequenceId = 0;
|
|
TitleSequence* _sequence = nullptr;
|
|
int32_t _position = 0;
|
|
int32_t _waitCounter = 0;
|
|
|
|
int32_t _lastScreenWidth = 0;
|
|
int32_t _lastScreenHeight = 0;
|
|
CoordsXY _viewCentreLocation = {};
|
|
|
|
public:
|
|
explicit TitleSequencePlayer(IScenarioRepository& scenarioRepository, GameState& gameState)
|
|
: _scenarioRepository(scenarioRepository)
|
|
, _gameState(gameState)
|
|
{
|
|
}
|
|
|
|
~TitleSequencePlayer() override
|
|
{
|
|
Eject();
|
|
}
|
|
|
|
int32_t GetCurrentPosition() const override
|
|
{
|
|
return _position;
|
|
}
|
|
|
|
void Eject() override
|
|
{
|
|
FreeTitleSequence(_sequence);
|
|
_sequence = nullptr;
|
|
}
|
|
|
|
bool Begin(size_t titleSequenceId) override
|
|
{
|
|
size_t numSequences = TitleSequenceManager::GetCount();
|
|
if (titleSequenceId >= numSequences)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto seqItem = TitleSequenceManager::GetItem(titleSequenceId);
|
|
auto sequence = LoadTitleSequence(seqItem->Path.c_str());
|
|
if (sequence == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Eject();
|
|
_sequence = sequence;
|
|
_sequenceId = titleSequenceId;
|
|
|
|
Reset();
|
|
return true;
|
|
}
|
|
|
|
bool Update() override
|
|
{
|
|
int32_t entryPosition = _position;
|
|
FixViewLocation();
|
|
|
|
if (_sequence == nullptr)
|
|
{
|
|
SetViewLocation(75 * 32, 75 * 32);
|
|
return false;
|
|
}
|
|
|
|
// Check that position is valid
|
|
if (_position >= (int32_t)_sequence->NumCommands)
|
|
{
|
|
_position = 0;
|
|
return false;
|
|
}
|
|
|
|
// Don't execute next command until we are done with the current wait command
|
|
if (_waitCounter != 0)
|
|
{
|
|
_waitCounter--;
|
|
if (_waitCounter == 0)
|
|
{
|
|
const TitleCommand* command = &_sequence->Commands[_position];
|
|
if (command->Type == TITLE_SCRIPT_WAIT)
|
|
{
|
|
IncrementPosition();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while (true)
|
|
{
|
|
const TitleCommand* command = &_sequence->Commands[_position];
|
|
if (ExecuteCommand(command))
|
|
{
|
|
if (command->Type == TITLE_SCRIPT_WAIT)
|
|
{
|
|
break;
|
|
}
|
|
if (command->Type != TITLE_SCRIPT_RESTART)
|
|
{
|
|
IncrementPosition();
|
|
}
|
|
if (_position == entryPosition)
|
|
{
|
|
Console::Error::WriteLine("Infinite loop detected in title sequence.");
|
|
Console::Error::WriteLine(" A wait command may be missing.");
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!SkipToNextLoadCommand() || _position == entryPosition)
|
|
{
|
|
Console::Error::WriteLine("Unable to load any parks from %s.", _sequence->Name);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Reset() override
|
|
{
|
|
_position = 0;
|
|
_waitCounter = 0;
|
|
}
|
|
|
|
void Seek(int32_t targetPosition) override
|
|
{
|
|
if (targetPosition < 0 || targetPosition >= (int32_t)_sequence->NumCommands)
|
|
{
|
|
throw std::runtime_error("Invalid position.");
|
|
}
|
|
if (_position >= targetPosition)
|
|
{
|
|
Reset();
|
|
}
|
|
|
|
if (_sequence->Commands[targetPosition].Type == TITLE_SCRIPT_RESTART)
|
|
{
|
|
targetPosition = 0;
|
|
}
|
|
// Set position to the last LOAD command before target position
|
|
for (int32_t i = targetPosition; i >= 0; i--)
|
|
{
|
|
const TitleCommand* command = &_sequence->Commands[i];
|
|
if ((_position == i && _position != targetPosition) || TitleSequenceIsLoadCommand(command))
|
|
{
|
|
// Break if we have a new load command or if we're already in the range of the correct load command
|
|
_position = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Keep updating until we reach target position
|
|
gInUpdateCode = true;
|
|
|
|
while (_position < targetPosition)
|
|
{
|
|
if (Update())
|
|
{
|
|
_gameState.UpdateLogic();
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
gInUpdateCode = false;
|
|
|
|
_waitCounter = 0;
|
|
}
|
|
|
|
private:
|
|
void IncrementPosition()
|
|
{
|
|
_position++;
|
|
if (_position >= (int32_t)_sequence->NumCommands)
|
|
{
|
|
_position = 0;
|
|
}
|
|
}
|
|
|
|
bool SkipToNextLoadCommand()
|
|
{
|
|
int32_t entryPosition = _position;
|
|
const TitleCommand* command;
|
|
do
|
|
{
|
|
IncrementPosition();
|
|
command = &_sequence->Commands[_position];
|
|
} while (!TitleSequenceIsLoadCommand(command) && _position != entryPosition);
|
|
return _position != entryPosition;
|
|
}
|
|
|
|
bool ExecuteCommand(const TitleCommand* command)
|
|
{
|
|
switch (command->Type)
|
|
{
|
|
case TITLE_SCRIPT_END:
|
|
_waitCounter = 1;
|
|
break;
|
|
case TITLE_SCRIPT_WAIT:
|
|
// The waitCounter is measured in 25-ms game ticks. Previously it was seconds * 40 ticks/second, now it is ms /
|
|
// 25 ms/tick
|
|
_waitCounter = std::max<int32_t>(1, command->Milliseconds / (uint32_t)GAME_UPDATE_TIME_MS);
|
|
break;
|
|
case TITLE_SCRIPT_LOADMM:
|
|
{
|
|
const scenario_index_entry* entry = _scenarioRepository.GetByFilename(SFMM_FILENAME);
|
|
if (entry == nullptr)
|
|
{
|
|
Console::Error::WriteLine("%s not found.", SFMM_FILENAME);
|
|
return false;
|
|
}
|
|
|
|
const utf8* path = entry->path;
|
|
if (!LoadParkFromFile(path))
|
|
{
|
|
Console::Error::WriteLine("Failed to load: \"%s\" for the title sequence.", path);
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
case TITLE_SCRIPT_LOCATION:
|
|
{
|
|
int32_t x = command->X * 32 + 16;
|
|
int32_t y = command->Y * 32 + 16;
|
|
SetViewLocation(x, y);
|
|
break;
|
|
}
|
|
case TITLE_SCRIPT_ROTATE:
|
|
RotateView(command->Rotations);
|
|
break;
|
|
case TITLE_SCRIPT_ZOOM:
|
|
SetViewZoom(command->Zoom);
|
|
break;
|
|
case TITLE_SCRIPT_SPEED:
|
|
gGameSpeed = std::clamp<uint8_t>(command->Speed, 1, 4);
|
|
break;
|
|
case TITLE_SCRIPT_FOLLOW:
|
|
FollowSprite(command->SpriteIndex);
|
|
break;
|
|
case TITLE_SCRIPT_RESTART:
|
|
Reset();
|
|
break;
|
|
case TITLE_SCRIPT_LOAD:
|
|
{
|
|
bool loadSuccess = false;
|
|
uint8_t saveIndex = command->SaveIndex;
|
|
TitleSequenceParkHandle* parkHandle = TitleSequenceGetParkHandle(_sequence, saveIndex);
|
|
if (parkHandle != nullptr)
|
|
{
|
|
loadSuccess = LoadParkFromStream((IStream*)parkHandle->Stream, parkHandle->HintPath);
|
|
TitleSequenceCloseParkHandle(parkHandle);
|
|
}
|
|
if (!loadSuccess)
|
|
{
|
|
if (_sequence->NumSaves > saveIndex)
|
|
{
|
|
const utf8* path = _sequence->Saves[saveIndex];
|
|
Console::Error::WriteLine("Failed to load: \"%s\" for the title sequence.", path);
|
|
}
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
case TITLE_SCRIPT_LOADRCT1:
|
|
{
|
|
source_desc sourceDesc;
|
|
if (!ScenarioSources::TryGetById(command->SaveIndex, &sourceDesc) || sourceDesc.index == -1)
|
|
{
|
|
Console::Error::WriteLine("Invalid scenario id.");
|
|
return false;
|
|
}
|
|
|
|
const utf8* path = nullptr;
|
|
size_t numScenarios = _scenarioRepository.GetCount();
|
|
for (size_t i = 0; i < numScenarios; i++)
|
|
{
|
|
const scenario_index_entry* scenario = _scenarioRepository.GetByIndex(i);
|
|
if (scenario && scenario->source_index == sourceDesc.index)
|
|
{
|
|
path = scenario->path;
|
|
break;
|
|
}
|
|
}
|
|
if (path == nullptr || !LoadParkFromFile(path))
|
|
{
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
case TITLE_SCRIPT_LOADSC:
|
|
{
|
|
bool loadSuccess = false;
|
|
auto scenario = GetScenarioRepository()->GetByInternalName(command->Scenario);
|
|
if (scenario != nullptr)
|
|
{
|
|
loadSuccess = LoadParkFromFile(scenario->path);
|
|
}
|
|
if (!loadSuccess)
|
|
{
|
|
Console::Error::WriteLine("Failed to load: \"%s\" for the title sequence.", command->Scenario);
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void SetViewZoom(const uint32_t& zoom)
|
|
{
|
|
rct_window* w = window_get_main();
|
|
if (w != nullptr && w->viewport != nullptr)
|
|
{
|
|
window_zoom_set(w, zoom, false);
|
|
}
|
|
}
|
|
|
|
void RotateView(uint32_t count)
|
|
{
|
|
rct_window* w = window_get_main();
|
|
if (w != nullptr)
|
|
{
|
|
for (uint32_t i = 0; i < count; i++)
|
|
{
|
|
window_rotate_camera(w, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FollowSprite(uint16_t spriteIndex)
|
|
{
|
|
rct_window* w = window_get_main();
|
|
if (w != nullptr)
|
|
{
|
|
window_follow_sprite(w, spriteIndex);
|
|
}
|
|
}
|
|
|
|
void UnfollowSprite()
|
|
{
|
|
rct_window* w = window_get_main();
|
|
if (w != nullptr)
|
|
{
|
|
window_unfollow_sprite(w);
|
|
}
|
|
}
|
|
|
|
bool LoadParkFromFile(const utf8* path)
|
|
{
|
|
log_verbose("TitleSequencePlayer::LoadParkFromFile(%s)", path);
|
|
bool success = false;
|
|
try
|
|
{
|
|
if (gPreviewingTitleSequenceInGame)
|
|
{
|
|
gLoadKeepWindowsOpen = true;
|
|
CloseParkSpecificWindows();
|
|
context_load_park_from_file(path);
|
|
}
|
|
else
|
|
{
|
|
auto parkImporter = ParkImporter::Create(path);
|
|
auto result = parkImporter->Load(path);
|
|
|
|
auto& objectManager = GetContext()->GetObjectManager();
|
|
objectManager.LoadObjects(result.RequiredObjects.data(), result.RequiredObjects.size());
|
|
|
|
parkImporter->Import();
|
|
}
|
|
PrepareParkForPlayback();
|
|
success = true;
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
Console::Error::WriteLine("Unable to load park: %s", path);
|
|
}
|
|
gLoadKeepWindowsOpen = false;
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* @param stream The stream to read the park data from.
|
|
* @param hintPath Hint path, the extension is grabbed to determine what importer to use.
|
|
*/
|
|
bool LoadParkFromStream(IStream* stream, const std::string& hintPath)
|
|
{
|
|
log_verbose("TitleSequencePlayer::LoadParkFromStream(%s)", hintPath.c_str());
|
|
bool success = false;
|
|
try
|
|
{
|
|
if (gPreviewingTitleSequenceInGame)
|
|
{
|
|
gLoadKeepWindowsOpen = true;
|
|
CloseParkSpecificWindows();
|
|
context_load_park_from_stream(stream);
|
|
}
|
|
else
|
|
{
|
|
std::string extension = Path::GetExtension(hintPath);
|
|
bool isScenario = ParkImporter::ExtensionIsScenario(hintPath);
|
|
auto parkImporter = ParkImporter::Create(hintPath);
|
|
auto result = parkImporter->LoadFromStream(stream, isScenario);
|
|
|
|
auto& objectManager = GetContext()->GetObjectManager();
|
|
objectManager.LoadObjects(result.RequiredObjects.data(), result.RequiredObjects.size());
|
|
|
|
parkImporter->Import();
|
|
}
|
|
PrepareParkForPlayback();
|
|
success = true;
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
Console::Error::WriteLine("Unable to load park: %s", hintPath.c_str());
|
|
}
|
|
gLoadKeepWindowsOpen = false;
|
|
return success;
|
|
}
|
|
|
|
void CloseParkSpecificWindows()
|
|
{
|
|
window_close_by_class(WC_CONSTRUCT_RIDE);
|
|
window_close_by_class(WC_DEMOLISH_RIDE_PROMPT);
|
|
window_close_by_class(WC_EDITOR_INVENTION_LIST_DRAG);
|
|
window_close_by_class(WC_EDITOR_INVENTION_LIST);
|
|
window_close_by_class(WC_EDITOR_OBJECT_SELECTION);
|
|
window_close_by_class(WC_EDTIOR_OBJECTIVE_OPTIONS);
|
|
window_close_by_class(WC_EDITOR_SCENARIO_OPTIONS);
|
|
window_close_by_class(WC_FINANCES);
|
|
window_close_by_class(WC_FIRE_PROMPT);
|
|
window_close_by_class(WC_GUEST_LIST);
|
|
window_close_by_class(WC_INSTALL_TRACK);
|
|
window_close_by_class(WC_PEEP);
|
|
window_close_by_class(WC_RIDE);
|
|
window_close_by_class(WC_RIDE_CONSTRUCTION);
|
|
window_close_by_class(WC_RIDE_LIST);
|
|
window_close_by_class(WC_SCENERY);
|
|
window_close_by_class(WC_STAFF);
|
|
window_close_by_class(WC_TRACK_DELETE_PROMPT);
|
|
window_close_by_class(WC_TRACK_DESIGN_LIST);
|
|
window_close_by_class(WC_TRACK_DESIGN_PLACE);
|
|
}
|
|
|
|
void PrepareParkForPlayback()
|
|
{
|
|
auto windowManager = GetContext()->GetUiContext()->GetWindowManager();
|
|
windowManager->SetMainView(gSavedViewX, gSavedViewY, gSavedViewZoom, gSavedViewRotation);
|
|
reset_sprite_spatial_index();
|
|
reset_all_sprite_quadrant_placements();
|
|
auto intent = Intent(INTENT_ACTION_REFRESH_NEW_RIDES);
|
|
context_broadcast_intent(&intent);
|
|
scenery_set_default_placement_configuration();
|
|
news_item_init_queue();
|
|
load_palette();
|
|
gScreenAge = 0;
|
|
gGameSpeed = 1;
|
|
}
|
|
|
|
/**
|
|
* Sets the map location to the given tile coordinates. Z is automatic.
|
|
* @param x X position in map tiles.
|
|
* @param y Y position in map tiles.
|
|
*/
|
|
void SetViewLocation(int32_t x, int32_t y)
|
|
{
|
|
// Update viewport
|
|
rct_window* w = window_get_main();
|
|
if (w != nullptr)
|
|
{
|
|
int32_t z = tile_element_height(x, y);
|
|
|
|
// Prevent scroll adjustment due to window placement when in-game
|
|
auto oldScreenFlags = gScreenFlags;
|
|
gScreenFlags = SCREEN_FLAGS_TITLE_DEMO;
|
|
window_set_location(w, x, y, z);
|
|
gScreenFlags = oldScreenFlags;
|
|
|
|
viewport_update_position(w);
|
|
|
|
// Save known tile position in case of window resize
|
|
_lastScreenWidth = w->width;
|
|
_lastScreenHeight = w->height;
|
|
_viewCentreLocation.x = x;
|
|
_viewCentreLocation.y = y;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fixes the view location for when the game window has changed size.
|
|
*/
|
|
void FixViewLocation()
|
|
{
|
|
rct_window* w = window_get_main();
|
|
if (w != nullptr && w->viewport_smart_follow_sprite == SPRITE_INDEX_NULL)
|
|
{
|
|
if (w->width != _lastScreenWidth || w->height != _lastScreenHeight)
|
|
{
|
|
SetViewLocation(_viewCentreLocation.x, _viewCentreLocation.y);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
std::unique_ptr<ITitleSequencePlayer> CreateTitleSequencePlayer(IScenarioRepository& scenarioRepository, GameState& gameState)
|
|
{
|
|
return std::make_unique<TitleSequencePlayer>(scenarioRepository, gameState);
|
|
}
|