OpenLoco/src/OpenLoco/S5/S5.cpp

695 lines
24 KiB
C++

#define DO_TITLE_SEQUENCE_CHECKS
#include "S5.h"
#include "../Audio/Audio.h"
#include "../CompanyManager.h"
#include "../Entities/EntityManager.h"
#include "../Game.h"
#include "../GameException.hpp"
#include "../Gui.h"
#include "../IndustryManager.h"
#include "../Interop/Interop.hpp"
#include "../Localisation/StringIds.h"
#include "../Localisation/StringManager.h"
#include "../Map/TileManager.h"
#include "../Objects/ObjectManager.h"
#include "../StationManager.h"
#include "../TownManager.h"
#include "../Ui/WindowManager.h"
#include "../Utility/Exception.hpp"
#include "../Vehicles/Orders.h"
#include "../ViewportManager.h"
#include "SawyerStream.h"
#include <fstream>
using namespace OpenLoco::Interop;
using namespace OpenLoco::Map;
using namespace OpenLoco::Ui;
namespace OpenLoco::S5
{
constexpr uint32_t currentVersion = 0x62262;
constexpr uint32_t magicNumber = 0x62300;
static loco_global<GameState, 0x00525E18> _gameState;
static loco_global<Options, 0x009C8714> _activeOptions;
static loco_global<Header, 0x009CCA34> _header;
static loco_global<Options, 0x009CCA54> _previewOptions;
static loco_global<char[512], 0x0112CE04> _savePath;
static loco_global<uint8_t, 0x00508F1A> _gameSpeed;
static loco_global<uint8_t, 0x0050C197> _loadErrorCode;
static loco_global<string_id, 0x0050C198> _loadErrorMessage;
static bool save(const fs::path& path, const S5File& file, const std::vector<ObjectHeader>& packedObjects);
Options& getOptions()
{
return _activeOptions;
}
Options& getPreviewOptions()
{
return _previewOptions;
}
static Header prepareHeader(SaveFlags flags, size_t numPackedObjects)
{
Header result;
std::memset(&result, 0, sizeof(result));
result.type = S5Type::savedGame;
if (flags & SaveFlags::landscape)
result.type = S5Type::landscape;
if (flags & SaveFlags::scenario)
result.type = S5Type::scenario;
result.numPackedObjects = static_cast<uint16_t>(numPackedObjects);
result.version = currentVersion;
result.magic = magicNumber;
if (flags & SaveFlags::raw)
{
result.flags |= S5Flags::isRaw;
}
if (flags & SaveFlags::dump)
{
result.flags |= S5Flags::isDump;
}
if (!(flags & SaveFlags::scenario) && !(flags & SaveFlags::raw) && !(flags & SaveFlags::dump))
{
result.flags |= S5Flags::hasSaveDetails;
}
return result;
}
// 0x0045A0B3
static void previewWindowDraw(Window* w, Gfx::Context* context)
{
for (auto viewport : w->viewports)
{
if (viewport != nullptr)
{
viewport->render(context);
}
}
}
static void drawPreviewImage(void* pixels, Ui::Size size)
{
auto mainViewport = WindowManager::getMainViewport();
if (mainViewport != nullptr)
{
auto mapPosXY = mainViewport->getCentreMapPosition();
auto mapPosXYZ = Pos3(mapPosXY.x, mapPosXY.y, TileManager::getHeight(mapPosXY));
static WindowEventList eventList; // 0x4FB3F0
eventList.draw = previewWindowDraw;
auto tempWindow = WindowManager::createWindow(
WindowType::previewImage,
{ 0, 0 },
size,
WindowFlags::stick_to_front,
&eventList);
if (tempWindow != nullptr)
{
auto tempViewport = ViewportManager::create(
tempWindow,
0,
{ tempWindow->x, tempWindow->y },
{ tempWindow->width, tempWindow->height },
ZoomLevel::half,
mapPosXYZ);
if (tempViewport != nullptr)
{
tempViewport->flags = ViewportFlags::town_names_displayed | ViewportFlags::station_names_displayed;
// Swap screen Context with our temporary one to draw the window then revert it back
auto& context = Gfx::screenContext();
auto backupContext = context;
context.bits = reinterpret_cast<uint8_t*>(pixels);
context.x = 0;
context.y = 0;
context.width = size.width;
context.height = size.height;
context.pitch = 0;
context.zoom_level = 0;
Gfx::redrawScreenRect(0, 0, size.width, size.height);
context = backupContext;
}
WindowManager::close(WindowType::previewImage);
}
}
}
// 0x004471A4
static std::unique_ptr<SaveDetails> prepareSaveDetails(GameState& gameState)
{
auto saveDetails = std::make_unique<SaveDetails>();
const auto& playerCompany = gameState.companies[gameState.playerCompanyId];
StringManager::formatString(saveDetails->company, sizeof(saveDetails->company), playerCompany.name);
StringManager::formatString(saveDetails->owner, sizeof(saveDetails->owner), playerCompany.ownerName);
saveDetails->date = gameState.currentDay;
saveDetails->performance_index = playerCompany.performanceIndex;
saveDetails->challenge_progress = playerCompany.challengeProgress;
saveDetails->challenge_flags = playerCompany.challengeFlags;
std::strncpy(saveDetails->scenario, gameState.scenarioName, sizeof(saveDetails->scenario));
drawPreviewImage(saveDetails->image, { 250, 200 });
return saveDetails;
}
static constexpr SawyerEncoding getBestEncodingForObjectType(ObjectType type)
{
switch (type)
{
case ObjectType::competitor:
return SawyerEncoding::uncompressed;
default:
return SawyerEncoding::runLengthSingle;
case ObjectType::currency:
return SawyerEncoding::runLengthMulti;
case ObjectType::townNames:
case ObjectType::scenarioText:
return SawyerEncoding::rotate;
}
}
// 0x00472633
// 0x004722FF
static void writePackedObjects(SawyerStreamWriter& fs, const std::vector<ObjectHeader>& packedObjects)
{
// TODO at some point, change this to just pack the object file directly from
// disc rather than using the in-memory version. This then avoids having
// to unload the object temporarily to save the S5.
for (const auto& header : packedObjects)
{
auto index = ObjectManager::findIndex(header);
if (index)
{
// Unload the object so that the object data is restored to
// its original file state
ObjectManager::unload(*index);
auto encodingType = getBestEncodingForObjectType(header.getType());
auto obj = ObjectManager::get<Object>(*index);
auto objSize = ObjectManager::getByteLength(*index);
fs.write(header);
fs.writeChunk(encodingType, obj, objSize);
}
else
{
throw std::runtime_error("Unable to pack object: object not loaded");
}
}
}
/**
* Removes all tile elements that have the ghost flag set.
* Assumes all elements are organised in tile order.
*/
static void removeGhostElements(std::vector<TileElement>& elements)
{
for (size_t i = 0; i < elements.size(); i++)
{
if (elements[i].isGhost())
{
if (elements[i].isLast())
{
if (i == 0 || elements[i - 1].isLast())
{
// First element of tile, can not remove...
}
else
{
elements[i - 1].setLast(true);
elements.erase(elements.begin() + i);
i--;
}
}
else
{
elements.erase(elements.begin() + i);
i--;
}
}
}
}
static std::unique_ptr<S5File> prepareSaveFile(SaveFlags flags, const std::vector<ObjectHeader>& requiredObjects, const std::vector<ObjectHeader>& packedObjects)
{
auto mainWindow = WindowManager::getMainWindow();
auto savedView = mainWindow != nullptr ? mainWindow->viewports[0]->toSavedView() : SavedViewSimple();
auto file = std::make_unique<S5File>();
file->header = prepareHeader(flags, packedObjects.size());
if (file->header.type == S5Type::scenario || file->header.type == S5Type::landscape)
{
file->landscapeOptions = std::make_unique<Options>(_activeOptions);
}
if (file->header.flags & S5Flags::hasSaveDetails)
{
file->saveDetails = prepareSaveDetails(_gameState);
}
std::memcpy(file->requiredObjects, requiredObjects.data(), sizeof(file->requiredObjects));
file->gameState = _gameState;
file->gameState.savedViewX = savedView.viewX;
file->gameState.savedViewY = savedView.viewY;
file->gameState.savedViewZoom = static_cast<uint8_t>(savedView.zoomLevel);
file->gameState.savedViewRotation = savedView.rotation;
file->gameState.magicNumber = magicNumber; // Match implementation at 0x004437FC
auto tileElements = TileManager::getElements();
file->tileElements.resize(tileElements.size());
std::memcpy(file->tileElements.data(), tileElements.data(), tileElements.size_bytes());
removeGhostElements(file->tileElements);
return file;
}
static constexpr bool shouldPackObjects(SaveFlags flags)
{
return !(flags & SaveFlags::raw) && !(flags & SaveFlags::dump) && (flags & SaveFlags::packCustomObjects) && !isNetworked();
}
// 0x00441C26
bool save(const fs::path& path, SaveFlags flags)
{
if (!(flags & SaveFlags::noWindowClose) && !(flags & SaveFlags::raw) && !(flags & SaveFlags::dump))
{
WindowManager::closeConstructionWindows();
}
if (!(flags & SaveFlags::raw))
{
TileManager::reorganise();
EntityManager::resetSpatialIndex();
EntityManager::zeroUnused();
StationManager::zeroUnused();
Vehicles::zeroOrderTable();
}
bool saveResult;
{
auto requiredObjects = ObjectManager::getHeaders();
std::vector<ObjectHeader> packedObjects;
if (shouldPackObjects(flags))
{
std::copy_if(requiredObjects.begin(), requiredObjects.end(), std::back_inserter(packedObjects), [](ObjectHeader& header) {
return header.isCustom();
});
}
auto file = prepareSaveFile(flags, requiredObjects, packedObjects);
saveResult = save(path, *file, packedObjects);
}
if (!(flags & SaveFlags::raw) && !(flags & SaveFlags::dump))
{
ObjectManager::reloadAll();
}
if (saveResult)
{
Gfx::invalidateScreen();
if (!(flags & SaveFlags::raw))
{
resetScreenAge();
}
return true;
}
return false;
}
static bool save(const fs::path& path, const S5File& file, const std::vector<ObjectHeader>& packedObjects)
{
try
{
SawyerStreamWriter fs(path);
fs.writeChunk(SawyerEncoding::rotate, file.header);
if (file.header.type == S5Type::scenario || file.header.type == S5Type::landscape)
{
fs.writeChunk(SawyerEncoding::rotate, *file.landscapeOptions);
}
if (file.header.flags & S5Flags::hasSaveDetails)
{
fs.writeChunk(SawyerEncoding::rotate, *file.saveDetails);
}
if (file.header.numPackedObjects != 0)
{
writePackedObjects(fs, packedObjects);
}
fs.writeChunk(SawyerEncoding::rotate, file.requiredObjects, sizeof(file.requiredObjects));
if (file.header.type == S5Type::scenario)
{
fs.writeChunk(SawyerEncoding::runLengthSingle, file.gameState.rng, 0xB96C);
fs.writeChunk(SawyerEncoding::runLengthSingle, file.gameState.towns, 0x123480);
fs.writeChunk(SawyerEncoding::runLengthSingle, file.gameState.animations, 0x79D80);
}
else
{
fs.writeChunk(SawyerEncoding::runLengthSingle, file.gameState);
}
if (file.header.flags & SaveFlags::raw)
{
throw NotImplementedException();
}
else
{
fs.writeChunk(SawyerEncoding::runLengthMulti, file.tileElements.data(), file.tileElements.size() * sizeof(TileElement));
}
fs.writeChecksum();
fs.close();
return true;
}
catch (const std::exception& e)
{
std::fprintf(stderr, "Unable to save S5: %s\n", e.what());
return false;
}
}
// 0x00445A4A
static void fixState(GameState& state)
{
if (state.fixFlags & S5FixFlags::fixFlag0)
{
state.fixFlags |= S5FixFlags::fixFlag1;
}
if (!(state.fixFlags & S5FixFlags::fixFlag1))
{
// Shift data after companies to correct location
auto src = reinterpret_cast<uint8_t*>(&state) + 0x49EA24;
auto dst = src + 0x1C20;
for (size_t i = 0; i < 0x40E200; i++)
{
*--dst = *--src;
}
// Convert each company format from old to new
for (size_t i = 0; i < std::size(state.companies); i++)
{
for (size_t j = 0; j < 372; j++)
{
*--dst = *--src;
}
for (size_t j = 0; j < 480; j++)
{
*--dst = 0;
}
for (size_t j = 0; j < 35924; j++)
{
*--dst = *--src;
}
}
}
}
// 0x00441FC9
static std::unique_ptr<S5File> load(const fs::path& path)
{
SawyerStreamReader fs(path);
if (!fs.validateChecksum())
{
throw std::runtime_error("Invalid checksum");
}
auto file = std::make_unique<S5File>();
// Read header
fs.readChunk(&file->header, sizeof(file->header));
// Read saved details 0x00442087
if (file->header.flags & S5Flags::hasSaveDetails)
{
file->saveDetails = std::make_unique<SaveDetails>();
fs.readChunk(file->saveDetails.get(), sizeof(file->saveDetails));
}
// Read packed objects
if (file->header.numPackedObjects > 0)
{
bool objectInstalled = false;
for (auto i = 0; i < file->header.numPackedObjects; ++i)
{
ObjectHeader object;
fs.read(&object, sizeof(ObjectHeader));
if (ObjectManager::tryInstallObject(object, fs.readChunk()))
{
objectInstalled = true;
}
}
if (objectInstalled)
{
ObjectManager::loadIndex();
}
// 0x004420B2
}
if (file->header.type == S5Type::objects)
{
addr<0x00525F62, uint16_t>() = 0;
_loadErrorCode = 254;
_loadErrorMessage = StringIds::new_objects_installed_successfully;
// Throws!
Game::returnToTitle();
}
else
{
// Load required objects
fs.readChunk(file->requiredObjects, sizeof(file->requiredObjects));
// Load game state
fs.readChunk(&file->gameState, sizeof(file->gameState));
fixState(file->gameState);
// Load tile elements
auto tileElements = fs.readChunk();
auto numTileElements = tileElements.size() / sizeof(TileElement);
file->tileElements.resize(numTileElements);
std::memcpy(file->tileElements.data(), tileElements.data(), numTileElements * sizeof(TileElement));
}
return file;
}
// 0x00473BC7
static void object_create_identifier_name(char* dst, const ObjectHeader& header)
{
registers regs;
regs.edi = X86Pointer(dst);
regs.ebp = X86Pointer(&header);
call(0x00473BC7, regs);
}
// 0x00444D76
static void setObjectErrorMessage(const ObjectHeader& header)
{
auto buffer = const_cast<char*>(StringManager::getString(StringIds::buffer_2040));
StringManager::formatString(buffer, sizeof(buffer), StringIds::missing_object_data_id_x);
object_create_identifier_name(strchr(buffer, 0), header);
_loadErrorCode = 255;
_loadErrorMessage = StringIds::buffer_2040;
}
class LoadException : public std::runtime_error
{
private:
string_id _localisedMessage;
public:
LoadException(const char* message, string_id localisedMessage)
: std::runtime_error(message)
, _localisedMessage(localisedMessage)
{
}
string_id getLocalisedMessage() const
{
return _localisedMessage;
}
};
static void sub_4BAEC4()
{
addr<0x001136496, uint8_t>() = 2;
addr<0x00525FB1, uint8_t>() = 255;
addr<0x00525FCA, uint8_t>() = 255;
}
// 0x00441FA7
bool load(const fs::path& path, uint32_t flags)
{
_gameSpeed = 0;
if (!(flags & LoadFlags::titleSequence) && !(flags & LoadFlags::twoPlayer))
{
WindowManager::closeConstructionWindows();
WindowManager::closeAllFloatingWindows();
}
try
{
auto file = load(path);
if (file->header.version != currentVersion)
{
throw LoadException("Unsupported S5 version", StringIds::error_file_contains_invalid_data);
}
#ifdef DO_TITLE_SEQUENCE_CHECKS
if (flags & LoadFlags::titleSequence)
{
if (!(file->header.flags & S5Flags::isTitleSequence))
{
throw LoadException("File was not a title sequence", StringIds::error_file_contains_invalid_data);
}
}
else
{
if (file->header.flags & S5Flags::isTitleSequence)
{
throw LoadException("File is a title sequence", StringIds::error_file_contains_invalid_data);
}
}
#endif
if (file->header.type == S5Type::scenario)
{
throw LoadException("File is a scenario, not a saved game", StringIds::error_file_contains_invalid_data);
}
if ((file->header.flags & S5Flags::isRaw) || (file->header.flags & S5Flags::isDump))
{
throw LoadException("Unsupported S5 format", StringIds::error_file_contains_invalid_data);
}
if (flags & LoadFlags::twoPlayer)
{
if (file->header.type != S5Type::landscape)
{
throw LoadException("Not a two player saved game", StringIds::error_file_is_not_two_player_save);
}
}
else
{
if (file->header.type != S5Type::savedGame)
{
throw LoadException("Not a single player saved game", StringIds::error_file_is_not_single_player_save);
}
}
auto loadObjectResult = ObjectManager::loadAll(file->requiredObjects);
if (!loadObjectResult.success)
{
setObjectErrorMessage(loadObjectResult.problemObject);
if (flags & LoadFlags::twoPlayer)
{
CompanyManager::reset();
addr<0x00525F62, uint16_t>() = 0;
return false;
}
else
{
Game::returnToTitle();
return false;
}
}
ObjectManager::reloadAll();
_gameState = file->gameState;
TileManager::setElements(stdx::span<Map::TileElement>(reinterpret_cast<Map::TileElement*>(file->tileElements.data()), file->tileElements.size()));
EntityManager::resetSpatialIndex();
CompanyManager::updateColours();
call(0x004748FA);
TileManager::resetSurfaceClearance();
IndustryManager::createAllMapAnimations();
if (!(flags & LoadFlags::titleSequence))
{
clearScreenFlag(ScreenFlags::title);
initialiseViewports();
Gui::init();
Audio::resetMusic();
}
auto mainWindow = WindowManager::getMainWindow();
if (mainWindow != nullptr)
{
SavedViewSimple savedView;
savedView.viewX = file->gameState.savedViewX;
savedView.viewY = file->gameState.savedViewY;
savedView.zoomLevel = static_cast<ZoomLevel>(file->gameState.savedViewZoom);
savedView.rotation = file->gameState.savedViewRotation;
mainWindow->viewportFromSavedView(savedView);
mainWindow->invalidate();
}
EntityManager::updateSpatialIndex();
TownManager::updateLabels();
StationManager::updateLabels();
sub_4BAEC4();
addr<0x0052334E, uint16_t>() = 0; // _thousandthTickCounter
Gfx::invalidateScreen();
call(0x004C153B);
call(0x0046E07B); // load currency gfx
addr<0x00525F62, uint16_t>() = 0;
if (flags & LoadFlags::titleSequence)
{
addr<0x00525F5E, uint32_t>()--; // _scenario_ticks
addr<0x00525F64, uint32_t>()--; // _scenario_ticks2
addr<0x0050BF6C, uint8_t>() = 1;
}
if (!(flags & LoadFlags::titleSequence) && !(flags & LoadFlags::twoPlayer))
{
resetScreenAge();
throw GameException::Interrupt;
}
return true;
}
catch (const LoadException& e)
{
std::fprintf(stderr, "Unable to load S5: %s\n", e.what());
_loadErrorCode = 255;
_loadErrorMessage = e.getLocalisedMessage();
return false;
}
catch (const std::exception& e)
{
std::fprintf(stderr, "Unable to load S5: %s\n", e.what());
_loadErrorCode = 255;
_loadErrorMessage = StringIds::null;
return false;
}
}
void registerHooks()
{
registerHook(
0x00441C26,
[](registers& regs) FORCE_ALIGN_ARG_POINTER -> uint8_t {
auto path = fs::u8path(std::string(_savePath));
return save(path, static_cast<SaveFlags>(regs.eax)) ? 0 : X86_FLAG_CARRY;
});
registerHook(
0x00441FA7,
[](registers& regs) FORCE_ALIGN_ARG_POINTER -> uint8_t {
auto path = fs::u8path(std::string(_savePath));
return load(path, regs.eax) ? X86_FLAG_CARRY : 0;
});
}
}