diff --git a/openrct2.vcxproj b/openrct2.vcxproj index 1524d32b15..9fd478c23b 100644 --- a/openrct2.vcxproj +++ b/openrct2.vcxproj @@ -263,7 +263,7 @@ - + @@ -500,6 +500,8 @@ + + diff --git a/src/ScenarioRepository.cpp b/src/ScenarioRepository.cpp new file mode 100644 index 0000000000..fecb73c42c --- /dev/null +++ b/src/ScenarioRepository.cpp @@ -0,0 +1,602 @@ +#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers +/***************************************************************************** + * OpenRCT2, an open source clone of Roller Coaster Tycoon 2. + * + * OpenRCT2 is the work of many authors, a full list can be found in contributors.md + * For more information, visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 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. + * + * A full copy of the GNU General Public License can be found in licence.txt + *****************************************************************************/ +#pragma endregion + +#include +#include +#include +#include "core/Console.hpp" +#include "core/FileEnumerator.h" +#include "core/FileStream.hpp" +#include "core/Math.hpp" +#include "core/Path.hpp" +#include "core/String.hpp" +#include "ScenarioRepository.h" +#include "ScenarioSources.h" + +extern "C" +{ + #include "config.h" + #include "localisation/localisation.h" + #include "scenario.h" +} + +static int ScenarioCategoryCompare(int categoryA, int categoryB) +{ + if (categoryA == categoryB) return 0; + if (categoryA == SCENARIO_CATEGORY_DLC) return -1; + if (categoryB == SCENARIO_CATEGORY_DLC) return 1; + if (categoryA == SCENARIO_CATEGORY_BUILD_YOUR_OWN) return -1; + if (categoryB == SCENARIO_CATEGORY_BUILD_YOUR_OWN) return 1; + return Math::Sign(categoryA - categoryB); +} + +static int scenario_index_entry_CompareByCategory(const scenario_index_entry &entryA, + const scenario_index_entry &entryB) +{ + // Order by category + if (entryA.category != entryB.category) + { + return ScenarioCategoryCompare(entryA.category, entryB.category); + } + + // Then by source game / name + switch (entryA.category) { + default: + if (entryA.source_game != entryB.source_game) + { + return entryA.source_game - entryB.source_game; + } + return strcmp(entryA.name, entryB.name); + case SCENARIO_CATEGORY_REAL: + case SCENARIO_CATEGORY_OTHER: + return strcmp(entryA.name, entryB.name); + } +} + +static int scenario_index_entry_CompareByIndex(const scenario_index_entry &entryA, + const scenario_index_entry &entryB) +{ + // Order by source game + if (entryA.source_game != entryB.source_game) + { + return entryA.source_game - entryB.source_game; + } + + // Then by index / category / name + uint8 sourceGame = entryA.source_game; + switch (sourceGame) { + default: + if (entryA.source_index == -1 && entryB.source_index == -1) + { + if (entryA.category == entryB.category) + { + return scenario_index_entry_CompareByCategory(entryA, entryB); + } + else + { + return ScenarioCategoryCompare(entryA.category, entryB.category); + } + } + else if (entryA.source_index == -1) + { + return 1; + } + else if (entryB.source_index == -1) + { + return -1; + } + else + { + return entryA.source_index - entryB.source_index; + } + case SCENARIO_SOURCE_REAL: + return scenario_index_entry_CompareByCategory(entryA, entryB); + } +} + +static void scenario_highscore_free(scenario_highscore_entry * highscore) +{ + SafeFree(highscore->fileName); + SafeFree(highscore->name); + SafeDelete(highscore); +} + +class ScenarioRepository final : public IScenarioRepository +{ +private: + static constexpr uint32 HighscoreFileVersion = 1; + + std::vector _scenarios; + std::vector _highscores; + +public: + virtual ~ScenarioRepository() + { + ClearHighscores(); + } + + void Scan() + { + utf8 directory[MAX_PATH]; + + _scenarios.clear(); + + // Scan RCT2 directory + GetRCT2Directory(directory, sizeof(directory)); + Scan(directory); + + // Scan user directory + GetUserDirectory(directory, sizeof(directory)); + Scan(directory); + + Sort(); + LoadScores(); + LoadLegacyScores(); + AttachHighscores(); + } + + size_t GetCount() const override + { + return _scenarios.size(); + } + + const scenario_index_entry * GetByIndex(size_t index) const override + { + const scenario_index_entry * result = nullptr; + if (index < _scenarios.size()) + { + result = &_scenarios[index]; + } + return result; + } + + const scenario_index_entry * GetByFilename(const utf8 * filename) const override + { + for (size_t i = 0; i < _scenarios.size(); i++) + { + const scenario_index_entry * scenario = &_scenarios[i]; + const utf8 * scenarioFilename = Path::GetFileName(scenario->path); + + // Note: this is always case insensitive search for cross platform consistency + if (String::Equals(filename, scenarioFilename, true)) + { + return &_scenarios[i]; + } + } + return nullptr; + } + + const scenario_index_entry * GetByPath(const utf8 * path) const override + { + for (size_t i = 0; i < _scenarios.size(); i++) + { + const scenario_index_entry * scenario = &_scenarios[i]; + if (Path::Equals(path, scenario->path)) + { + return scenario; + } + } + return nullptr; + } + + bool TryRecordHighscore(const utf8 * scenarioFileName, money32 companyValue, const utf8 * name) override + { + const scenario_index_entry * scenario = GetByFilename(scenarioFileName); + if (scenario != nullptr) + { + // Check if record company value has been broken or the highscore is the same but no name is registered + scenario_highscore_entry * highscore = scenario->highscore; + if (highscore == nullptr || companyValue > highscore->company_value || + (highscore->name == nullptr && companyValue == highscore->company_value)) + { + if (highscore == nullptr) + { + highscore = InsertHighscore(); + scenario->highscore->timestamp = platform_get_datetime_now_utc(); + } + else + { + if (highscore->name != nullptr) + { + scenario->highscore->timestamp = platform_get_datetime_now_utc(); + } + SafeFree(highscore->fileName); + SafeFree(highscore->name); + } + scenario->highscore->fileName = String::Duplicate(Path::GetFileName(scenario->path)); + scenario->highscore->name = String::Duplicate(name); + scenario->highscore->company_value = companyValue; + SaveHighscores(); + return true; + } + } + return false; + } + +private: + scenario_index_entry * GetByFilename(const utf8 * filename) + { + const ScenarioRepository * repo = this; + return (scenario_index_entry *)repo->GetByFilename(filename); + } + + scenario_index_entry * GetByPath(const utf8 * path) + { + const ScenarioRepository * repo = this; + return (scenario_index_entry *)repo->GetByPath(path); + } + + void Scan(const utf8 * directory) + { + utf8 pattern[MAX_PATH]; + String::Set(pattern, sizeof(pattern), directory); + Path::Append(pattern, sizeof(pattern), "*.sc6"); + + auto fileEnumerator = FileEnumerator(pattern, true); + while (fileEnumerator.Next()) + { + auto path = fileEnumerator.GetPath(); + auto fileInfo = fileEnumerator.GetFileInfo(); + AddScenario(path, fileInfo->last_modified); + } + } + + void AddScenario(const utf8 * path, uint64 timestamp) + { + rct_s6_header s6Header; + rct_s6_info s6Info; + if (!scenario_load_basic(path, &s6Header, &s6Info)) + { + Console::Error::WriteLine("Unable to read scenario: '%s'", path); + return; + } + + const utf8 * filename = Path::GetFileName(path); + scenario_index_entry * existingEntry = GetByFilename(filename); + if (existingEntry != nullptr) + { + const utf8 * conflictPath; + if (existingEntry->timestamp > timestamp) + { + // Existing entry is more recent + conflictPath = existingEntry->path; + + // Overwrite existing entry with this one + *existingEntry = CreateNewScenarioEntry(path, timestamp, &s6Info); + } + else + { + // This entry is more recent + conflictPath = path; + } + Console::WriteLine("Scenario conflict: '%s' ignored because it is newer.", conflictPath); + } + else + { + scenario_index_entry entry = CreateNewScenarioEntry(path, timestamp, &s6Info); + _scenarios.push_back(entry); + } + } + + scenario_index_entry CreateNewScenarioEntry(const utf8 * path, uint64 timestamp, rct_s6_info * s6Info) + { + scenario_index_entry entry = { 0 }; + + // Set new entry + String::Set(entry.path, sizeof(entry.path), path); + entry.timestamp = timestamp; + entry.category = s6Info->category; + entry.objective_type = s6Info->objective_type; + entry.objective_arg_1 = s6Info->objective_arg_1; + entry.objective_arg_2 = s6Info->objective_arg_2; + entry.objective_arg_3 = s6Info->objective_arg_3; + entry.highscore = nullptr; + String::Set(entry.name, sizeof(entry.name), s6Info->name); + String::Set(entry.details, sizeof(entry.details), s6Info->details); + + // Normalise the name to make the scenario as recognisable as possible. + ScenarioSources::NormaliseName(entry.name, sizeof(entry.name), entry.name); + + // Look up and store information regarding the origins of this scenario. + source_desc desc; + if (ScenarioSources::TryGetByName(entry.name, &desc)) + { + entry.sc_id = desc.id; + entry.source_index = desc.index; + entry.source_game = desc.source; + entry.category = desc.category; + } + else + { + entry.sc_id = SC_UNIDENTIFIED; + entry.source_index = -1; + if (entry.category == SCENARIO_CATEGORY_REAL) + { + entry.source_game = SCENARIO_SOURCE_REAL; + } + else + { + entry.source_game = SCENARIO_SOURCE_OTHER; + } + } + + scenario_translate(&entry, &s6Info->entry); + return entry; + } + + void Sort() + { + if (gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_ORIGIN) + { + std::sort(_scenarios.begin(), _scenarios.end(), [](const scenario_index_entry &a, + const scenario_index_entry &b) -> bool + { + return scenario_index_entry_CompareByIndex(a, b) < 0; + }); + } + else + { + std::sort(_scenarios.begin(), _scenarios.end(), [](const scenario_index_entry &a, + const scenario_index_entry &b) -> bool + { + return scenario_index_entry_CompareByCategory(a, b) < 0; + }); + } + } + + void LoadScores() + { + utf8 scoresPath[MAX_PATH]; + GetScoresPath(scoresPath, sizeof(scoresPath)); + if (!platform_file_exists(scoresPath)) + { + return; + } + + try + { + auto fs = FileStream(scoresPath, FILE_MODE_OPEN); + uint32 fileVersion = fs.ReadValue(); + if (fileVersion != 1) + { + Console::Error::WriteLine("Invalid or incompatible highscores file."); + return; + } + + ClearHighscores(); + + uint32 numHighscores = fs.ReadValue(); + for (uint32 i; i < numHighscores; i++) + { + scenario_highscore_entry * highscore = InsertHighscore(); + highscore->fileName = fs.ReadString(); + highscore->name = fs.ReadString(); + highscore->company_value = fs.ReadValue(); + highscore->timestamp = fs.ReadValue(); + } + } + catch (Exception ex) + { + Console::Error::WriteLine("Error reading highscores."); + } + } + + /** + * Loads the original scores.dat file and replaces any highscores that + * are better for matching scenarios. + */ + void LoadLegacyScores() + { + utf8 scoresPath[MAX_PATH]; + + GetLegacyScoresPath(scoresPath, sizeof(scoresPath)); + LoadLegacyScores(scoresPath); + + GetRCT2ScoresPath(scoresPath, sizeof(scoresPath)); + LoadLegacyScores(scoresPath); + } + + void LoadLegacyScores(const utf8 * path) + { + if (!platform_file_exists(path)) + { + return; + } + + bool highscoresDirty = false; + try + { + auto fs = FileStream(path, FILE_MODE_OPEN); + if (fs.GetLength() <= 4) + { + // Initial value of scores for RCT2, just ignore + return; + } + + // Load header + auto header = fs.ReadValue(); + for (uint32 i = 0; i < header.scenario_count; i++) + { + // Read legacy entry + auto scBasic = fs.ReadValue(); + + // Ignore non-completed scenarios + if (scBasic.flags & SCENARIO_FLAGS_COMPLETED) + { + bool notFound = true; + for (size_t i = 0; i < _highscores.size(); i++) + { + scenario_highscore_entry * highscore = _highscores[i]; + if (String::Equals(scBasic.path, highscore->fileName, true)) + { + notFound = false; + + // Check if legacy highscore is better + if (scBasic.company_value > highscore->company_value) + { + SafeFree(highscore->name); + highscore->name = win1252_to_utf8_alloc(scBasic.completed_by); + highscore->company_value = highscore->company_value; + highscore->timestamp = DATETIME64_MIN; + break; + } + } + } + if (notFound) + { + scenario_highscore_entry * highscore = InsertHighscore(); + highscore->fileName = String::Duplicate(scBasic.path); + highscore->name = win1252_to_utf8_alloc(scBasic.completed_by); + highscore->company_value = highscore->company_value; + highscore->timestamp = DATETIME64_MIN; + } + } + } + } + catch (Exception ex) + { + Console::Error::WriteLine("Error reading legacy scenario scores file: '%s'", path); + } + + if (highscoresDirty) + { + SaveHighscores(); + } + } + + void ClearHighscores() + { + for (auto highscore : _highscores) + { + scenario_highscore_free(highscore); + } + _highscores.clear(); + } + + scenario_highscore_entry * InsertHighscore() + { + auto highscore = new scenario_highscore_entry(); + memset(highscore, 0, sizeof(scenario_highscore_entry)); + _highscores.push_back(highscore); + return highscore; + } + + void AttachHighscores() + { + for (size_t i = 0; i < _highscores.size(); i++) + { + scenario_highscore_entry * highscore = _highscores[i]; + scenario_index_entry * scenerio = GetByPath(highscore->fileName); + if (scenerio != nullptr) + { + scenerio->highscore = highscore; + } + } + } + + void SaveHighscores() + { + utf8 scoresPath[MAX_PATH]; + GetScoresPath(scoresPath, sizeof(scoresPath)); + + try + { + auto fs = FileStream(scoresPath, FILE_MODE_WRITE); + fs.WriteValue(HighscoreFileVersion); + fs.WriteValue((uint32)_highscores.size()); + for (size_t i = 0; i < _highscores.size(); i++) + { + const scenario_highscore_entry * highscore = _highscores[i]; + fs.WriteString(highscore->fileName); + fs.WriteString(highscore->name); + fs.WriteValue(highscore->company_value); + fs.WriteValue(highscore->timestamp); + } + } + catch (Exception ex) + { + Console::Error::WriteLine("Unable to save highscores to '%s'", scoresPath); + } + } + + static utf8 * GetRCT2Directory(utf8 * buffer, size_t bufferSize) + { + String::Set(buffer, bufferSize, gRCT2AddressAppPath); + Path::Append(buffer, bufferSize, "Scenarios"); + return buffer; + } + + static utf8 * GetUserDirectory(utf8 * buffer, size_t bufferSize) + { + platform_get_user_directory(buffer, "scenario", bufferSize); + return buffer; + } + + static void GetScoresPath(utf8 * buffer, size_t bufferSize) + { + platform_get_user_directory(buffer, nullptr, bufferSize); + Path::Append(buffer, bufferSize, "highscores.dat"); + } + + static void GetLegacyScoresPath(utf8 * buffer, size_t bufferSize) + { + platform_get_user_directory(buffer, nullptr, bufferSize); + Path::Append(buffer, bufferSize, "scores.dat"); + } + + static void GetRCT2ScoresPath(utf8 * buffer, size_t bufferSize) + { + String::Set(buffer, bufferSize, get_file_path(PATH_ID_SCORES)); + } +}; + +static std::unique_ptr _scenarioRepository; + +IScenarioRepository * GetScenarioRepository() +{ + if (_scenarioRepository == nullptr) + { + _scenarioRepository = std::unique_ptr(new ScenarioRepository()); + } + return _scenarioRepository.get(); +} + +extern "C" +{ + void scenario_repository_scan() + { + IScenarioRepository * repo = GetScenarioRepository(); + repo->Scan(); + } + + size_t scenario_repository_get_count() + { + IScenarioRepository * repo = GetScenarioRepository(); + return repo->GetCount(); + } + + const scenario_index_entry *scenario_repository_get_by_index(size_t index) + { + IScenarioRepository * repo = GetScenarioRepository(); + return repo->GetByIndex(index); + } + + bool scenario_repository_try_record_highscore(const utf8 * scenarioFileName, money32 companyValue, const utf8 * name) + { + IScenarioRepository * repo = GetScenarioRepository(); + return repo->TryRecordHighscore(scenarioFileName, companyValue, name); + } +} diff --git a/src/ScenarioRepository.h b/src/ScenarioRepository.h new file mode 100644 index 0000000000..e36b609c92 --- /dev/null +++ b/src/ScenarioRepository.h @@ -0,0 +1,93 @@ +#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers +/***************************************************************************** + * OpenRCT2, an open source clone of Roller Coaster Tycoon 2. + * + * OpenRCT2 is the work of many authors, a full list can be found in contributors.md + * For more information, visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 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. + * + * A full copy of the GNU General Public License can be found in licence.txt + *****************************************************************************/ +#pragma endregion + +#pragma once + +#include "common.h" + +#ifndef MAX_PATH + #define MAX_PATH 260 +#endif + +typedef struct rct_object_entry rct_object_entry; + +typedef struct scenario_highscore_entry +{ + utf8 * fileName; + utf8 * name; + money32 company_value; + datetime64 timestamp; +} scenario_highscore_entry; + +typedef struct scenario_index_entry +{ + utf8 path[MAX_PATH]; + uint64 timestamp; + + // Category / sequence + uint8 category; + uint8 source_game; + sint16 source_index; + uint16 sc_id; + + // Objective + uint8 objective_type; + uint8 objective_arg_1; + sint32 objective_arg_2; + sint16 objective_arg_3; + scenario_highscore_entry * highscore; + + utf8 name[64]; + utf8 details[256]; +} scenario_index_entry; + +#ifdef __cplusplus + +interface IScenarioRepository +{ + virtual ~IScenarioRepository() = default; + + /** + * Scans the scenario directories and grabs the metadata for all the scenarios. + */ + virtual void Scan() abstract; + + virtual size_t GetCount() const abstract; + virtual const scenario_index_entry * GetByIndex(size_t index) const abstract; + virtual const scenario_index_entry * GetByFilename(const utf8 * filename) const abstract; + virtual const scenario_index_entry * GetByPath(const utf8 * path) const abstract; + + virtual bool TryRecordHighscore(const utf8 * scenarioFileName, money32 companyValue, const utf8 * name) abstract; +}; + +IScenarioRepository * GetScenarioRepository(); + +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + + void scenario_repository_scan(); + size_t scenario_repository_get_count(); + const scenario_index_entry *scenario_repository_get_by_index(size_t index); + bool scenario_repository_try_record_highscore(const utf8 * scenarioFileName, money32 companyValue, const utf8 * name); + void scenario_translate(scenario_index_entry * scenarioEntry, const rct_object_entry * stexObjectEntry); + +#ifdef __cplusplus +} +#endif diff --git a/src/ScenarioSources.cpp b/src/ScenarioSources.cpp index 543a040651..78be35a0f0 100644 --- a/src/ScenarioSources.cpp +++ b/src/ScenarioSources.cpp @@ -17,6 +17,7 @@ #include "core/Guard.hpp" #include "core/String.hpp" #include "core/Util.hpp" +#include "ScenarioSources.h" extern "C" { diff --git a/src/ScenarioSources.h b/src/ScenarioSources.h new file mode 100644 index 0000000000..99aaea0ea1 --- /dev/null +++ b/src/ScenarioSources.h @@ -0,0 +1,159 @@ +#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers +/***************************************************************************** + * OpenRCT2, an open source clone of Roller Coaster Tycoon 2. + * + * OpenRCT2 is the work of many authors, a full list can be found in contributors.md + * For more information, visit https://github.com/OpenRCT2/OpenRCT2 + * + * OpenRCT2 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. + * + * A full copy of the GNU General Public License can be found in licence.txt + *****************************************************************************/ +#pragma endregion + +#pragma once + +#include "common.h" + +typedef struct source_desc +{ + const utf8 * title; + uint8 id; + uint8 source; + sint32 index; + uint8 category; +} source_desc; + +#ifdef __cplusplus + +namespace ScenarioSources +{ + bool TryGetByName(const utf8 * name, source_desc * outDesc); + bool TryGetById(uint8 id, source_desc * outDesc); + void NormaliseName(utf8 * buffer, size_t bufferSize, const utf8 * name); +} + +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + + bool scenario_get_source_desc(const utf8 *name, source_desc *outDesc); + bool scenario_get_source_desc_by_id(uint8 id, source_desc *outDesc); + void scenario_normalise_name(utf8 *buffer, size_t bufferSize, utf8 *name); + +#ifdef __cplusplus +} +#endif + +// RCT1 scenario index map +enum +{ + SC_UNIDENTIFIED = 255, + + // RCT + SC_FOREST_FRONTIERS = 0, + SC_DYNAMITE_DUNES, + SC_LEAFY_LAKES, + SC_DIAMOND_HEIGHTS, + SC_EVERGREEN_GARDENS, + SC_BUMBLY_BEACH, + SC_TRINITY_ISLANDS, + SC_KATIES_DREAMLAND, + SC_POKEY_PARK, + SC_WHITE_WATER_PARK, + SC_MILLENNIUM_MINES, + SC_KARTS_COASTERS, + SC_MELS_WORLD, + SC_MYSTIC_MOUNTAIN, + SC_PACIFIC_PYRAMIDS, + SC_CRUMBLY_WOODS, + SC_PARADISE_PIER, + SC_LIGHTNING_PEAKS, + SC_IVORY_TOWERS, + SC_RAINBOW_VALLEY, + SC_THUNDER_ROCK, + SC_MEGA_PARK, + + // Loopy Landscapes + SC_ICEBERG_ISLANDS, + SC_VOLCANIA, + SC_ARID_HEIGHTS, + SC_RAZOR_ROCKS, + SC_CRATER_LAKE, + SC_VERTIGO_VIEWS, + SC_PARADISE_PIER_2, + SC_DRAGONS_COVE, + SC_GOOD_KNIGHT_PARK, + SC_WACKY_WARREN, + + // Special + ALTON_TOWERS, + FORT_ANACHRONISM, + + // Added Attractions + SC_WHISPERING_CLIFFS = 40, + SC_THREE_MONKEYS_PARK, + SC_CANARY_MINES, + SC_BARONY_BRIDGE, + SC_FUNTOPIA, + SC_HAUNTED_HARBOUR, + SC_FUN_FORTRESS, + SC_FUTURE_WORLD, + SC_GENTLE_GLEN, + SC_JOLLY_JUNGLE, + SC_HYDRO_HILLS, + SC_SPRIGHTLY_PARK, + SC_MAGIC_QUARTERS, + SC_FRUIT_FARM, + SC_BUTTERFLY_DAM, + SC_COASTER_CANYON, + SC_THUNDERSTORM_PARK, + SC_HARMONIC_HILLS, + SC_ROMAN_VILLAGE, + SC_SWAMP_COVE, + SC_ADRENALINE_HEIGHTS, + SC_UTOPIA, + SC_ROTTING_HEIGHTS, + SC_FIASCO_FOREST, + SC_PICKLE_PARK, + SC_GIGGLE_DOWNS, + SC_MINERAL_PARK, + SC_COASTER_CRAZY, + SC_URBAN_PARK, + SC_GEOFFREY_GARDENS, + + // Special + SC_HEIDE_PARK, + SC_PCPLAYER, + SC_PCGW, + SC_GAMEPLAY, + SC_BLACKPOOL_PLEASURE_BEACH, + + // Loopy Landscapes + SC_GRAND_GLACIER = 80, + SC_CRAZY_CRATERS, + SC_DUSTY_DESERT, + SC_WOODWORM_PARK, + SC_ICARUS_PARK, + SC_SUNNY_SWAMPS, + SC_FRIGHTMARE_HILLS, + SC_THUNDER_ROCKS, + SC_OCTAGON_PARK, + SC_PLEASURE_ISLAND, + SC_ICICLE_WORLDS, + SC_SOUTHERN_SANDS, + SC_TINY_TOWERS, + SC_NEVERMORE_PARK, + SC_PACIFICA, + SC_URBAN_JUNGLE, + SC_TERROR_TOWN, + SC_MEGAWORLD_PARK, + SC_VENUS_PONDS, + SC_MICRO_PARK, +}; diff --git a/src/core/Math.hpp b/src/core/Math.hpp index 91d401243d..bd9ad321df 100644 --- a/src/core/Math.hpp +++ b/src/core/Math.hpp @@ -40,4 +40,12 @@ namespace Math { return (std::min)((std::max)(low, x), high); } + + template + T Sign(T x) + { + if (x < 0) return -1; + if (x > 0) return 1; + return 0; + } } diff --git a/src/core/String.hpp b/src/core/String.hpp index 475c9a823b..db0eaeb058 100644 --- a/src/core/String.hpp +++ b/src/core/String.hpp @@ -65,7 +65,7 @@ namespace String codepoint_t GetNextCodepoint(const utf8 * ptr, const utf8 * * nextPtr = nullptr); utf8 * WriteCodepoint(utf8 * dst, codepoint_t codepoint); - utf8 * Trim(utf8 * str); - utf8 * TrimStart(utf8 * str); - utf8 * TrimStart(utf8 * buffer, size_t bufferSize, const utf8 * src); + utf8 * Trim(utf8 * str); + const utf8 * TrimStart(const utf8 * str); + utf8 * TrimStart(utf8 * buffer, size_t bufferSize, const utf8 * src); } diff --git a/src/interface/window.h b/src/interface/window.h index 08bbefd74f..85ed8513a6 100644 --- a/src/interface/window.h +++ b/src/interface/window.h @@ -28,6 +28,8 @@ #include "../world/park.h" #include "colour.h" +typedef struct scenario_index_entry scenario_index_entry; + struct rct_window; union rct_window_event; extern uint16 TextInputDescriptionArgs[4]; @@ -282,7 +284,7 @@ typedef struct rct_window { uint16 ride_colour; rct_research_item* research_item; rct_object_entry* object_entry; - scenario_index_entry* highlighted_scenario; + const scenario_index_entry* highlighted_scenario; struct { uint16 var_494; uint16 var_496; diff --git a/src/object/ObjectRepository.cpp b/src/object/ObjectRepository.cpp index 63b7055f46..68ebe6d0c6 100644 --- a/src/object/ObjectRepository.cpp +++ b/src/object/ObjectRepository.cpp @@ -30,6 +30,7 @@ #include "../core/Path.hpp" #include "../core/Stopwatch.hpp" #include "../core/String.hpp" +#include "../ScenarioRepository.h" #include "Object.h" #include "ObjectFactory.h" #include "ObjectManager.h" @@ -44,7 +45,6 @@ extern "C" #include "../object.h" #include "../object_list.h" #include "../platform/platform.h" - #include "../scenario.h" #include "../util/sawyercoding.h" #include "../util/util.h" } diff --git a/src/rct1/S4Importer.cpp b/src/rct1/S4Importer.cpp index 41b4e7c0d8..67cb0e6718 100644 --- a/src/rct1/S4Importer.cpp +++ b/src/rct1/S4Importer.cpp @@ -23,6 +23,7 @@ #include "../core/Path.hpp" #include "../core/String.hpp" #include "../core/Util.hpp" +#include "../ScenarioSources.h" #include "../object/ObjectManager.h" #include "S4Importer.h" #include "Tables.h" diff --git a/src/rct2.c b/src/rct2.c index fa3c1cfbff..9378cb8fd7 100644 --- a/src/rct2.c +++ b/src/rct2.c @@ -42,7 +42,7 @@ #include "ride/ride.h" #include "ride/track.h" #include "ride/track_design.h" -#include "scenario.h" +#include "ScenarioRepository.h" #include "title.h" #include "util/util.h" #include "world/map.h" @@ -165,7 +165,7 @@ bool rct2_init() } object_list_load(); - scenario_load_list(); + scenario_repository_scan(); track_design_index_create(); font_sprite_initialise_characters(); diff --git a/src/scenario.c b/src/scenario.c index b918f0953b..53f84768fe 100644 --- a/src/scenario.c +++ b/src/scenario.c @@ -35,6 +35,8 @@ #include "platform/platform.h" #include "ride/ride.h" #include "scenario.h" +#include "ScenarioRepository.h" +#include "ScenarioSources.h" #include "title.h" #include "util/sawyercoding.h" #include "util/util.h" @@ -336,25 +338,11 @@ void scenario_success() gScenarioCompletedCompanyValue = companyValue; peep_applause(); - scenario_index_entry *scenario = scenario_list_find_by_filename(_scenarioFileName); - if (scenario != NULL) { - // Check if record company value has been broken - if (scenario->highscore == NULL || scenario->highscore->company_value < companyValue) { - if (scenario->highscore == NULL) { - scenario->highscore = scenario_highscore_insert(); - } else { - scenario_highscore_free(scenario->highscore); - } - scenario->highscore->fileName = _strdup(path_get_filename(scenario->path)); - scenario->highscore->name = NULL; - scenario->highscore->company_value = companyValue; - scenario->highscore->timestamp = platform_get_datetime_now_utc(); - - // Allow name entry - gParkFlags |= PARK_FLAGS_SCENARIO_COMPLETE_NAME_INPUT; - gScenarioCompanyValueRecord = companyValue; - scenario_scores_save(); - } + if (scenario_repository_try_record_highscore(_scenarioFileName, companyValue, NULL)) + { + // Allow name entry + gParkFlags |= PARK_FLAGS_SCENARIO_COMPLETE_NAME_INPUT; + gScenarioCompanyValueRecord = companyValue; } scenario_end(); } @@ -365,16 +353,10 @@ void scenario_success() */ void scenario_success_submit_name(const char *name) { - scenario_index_entry *scenario = scenario_list_find_by_filename(_scenarioFileName); - if (scenario != NULL) { - money32 scenarioWinCompanyValue = gScenarioCompanyValueRecord; - if (scenario->highscore->company_value == scenarioWinCompanyValue) { - scenario->highscore->name = _strdup(name); - safe_strcpy(gScenarioCompletedBy, name, 32); - scenario_scores_save(); - } + if (scenario_repository_try_record_highscore(_scenarioFileName, gScenarioCompanyValueRecord, NULL)) + { + safe_strcpy(gScenarioCompletedBy, name, 32); } - gParkFlags &= ~PARK_FLAGS_SCENARIO_COMPLETE_NAME_INPUT; } diff --git a/src/scenario.h b/src/scenario.h index 5c9b8f2f05..15f486c31b 100644 --- a/src/scenario.h +++ b/src/scenario.h @@ -102,7 +102,7 @@ typedef struct rct_scenario_basic { char name[64]; // 0x0128 char details[256]; // 0x0168 sint32 flags; // 0x0268 - uint32 company_value; // 0x026C + money32 company_value; // 0x026C char completed_by[64]; // 0x0270 // uint8 source_game; // new in OpenRCT2 // sint16 source_index; // new in OpenRCT2 @@ -376,42 +376,6 @@ enum { OBJECTIVE_MONTHLY_FOOD_INCOME }; -typedef struct scenario_highscore_entry { - utf8 *fileName; - utf8 *name; - money32 company_value; - datetime64 timestamp; -} scenario_highscore_entry; - -typedef struct scenario_index_entry { - utf8 path[MAX_PATH]; - uint64 timestamp; - - // Category / sequence - uint8 category; - uint8 source_game; - sint16 source_index; - uint16 sc_id; - - // Objective - uint8 objective_type; - uint8 objective_arg_1; - sint32 objective_arg_2; - sint16 objective_arg_3; - scenario_highscore_entry *highscore; - - utf8 name[64]; - utf8 details[256]; -} scenario_index_entry; - -typedef struct source_desc { - const utf8 *title; - uint8 id; - uint8 source; - sint32 index; - uint8 category; -} source_desc; - extern const rct_string_id ScenarioCategoryStringIds[SCENARIO_CATEGORY_COUNT]; #if defined(NO_RCT2) @@ -431,11 +395,6 @@ extern uint16 gScenarioParkRatingWarningDays; extern money32 gScenarioCompletedCompanyValue; extern money32 gScenarioCompanyValueRecord; -// Scenario list -extern int gScenarioListCount; -extern int gScenarioListCapacity; -extern scenario_index_entry *gScenarioList; - extern rct_s6_info gS6Info; extern char gScenarioName[64]; extern char gScenarioDetails[256]; @@ -448,13 +407,6 @@ extern uint32 gLastAutoSaveTick; extern const char *_scenarioFileName; -bool scenario_scores_save(); -void scenario_load_list(); -void scenario_list_dispose(); -scenario_index_entry *scenario_list_find_by_filename(const utf8 *filename); -scenario_index_entry *scenario_list_find_by_path(const utf8 *path); -scenario_highscore_entry *scenario_highscore_insert(); -void scenario_highscore_free(scenario_highscore_entry *highscore); bool scenario_load_basic(const char *path, rct_s6_header *header, rct_s6_info *info); int scenario_load(const char *path); int scenario_load_and_play_from_path(const char *path); @@ -475,116 +427,4 @@ void scenario_success(); void scenario_success_submit_name(const char *name); void scenario_autosave_check(); -bool scenario_get_source_desc(const utf8 *name, source_desc *outDesc); -bool scenario_get_source_desc_by_id(uint8 id, source_desc *outDesc); -void scenario_normalise_name(utf8 *buffer, size_t bufferSize, utf8 *name); - -void scenario_translate(scenario_index_entry *scenarioEntry, const rct_object_entry *stexObjectEntry); - -// RCT1 scenario index map -enum { - SC_UNIDENTIFIED = 255, - - // RCT - SC_FOREST_FRONTIERS = 0, - SC_DYNAMITE_DUNES, - SC_LEAFY_LAKES, - SC_DIAMOND_HEIGHTS, - SC_EVERGREEN_GARDENS, - SC_BUMBLY_BEACH, - SC_TRINITY_ISLANDS, - SC_KATIES_DREAMLAND, - SC_POKEY_PARK, - SC_WHITE_WATER_PARK, - SC_MILLENNIUM_MINES, - SC_KARTS_COASTERS, - SC_MELS_WORLD, - SC_MYSTIC_MOUNTAIN, - SC_PACIFIC_PYRAMIDS, - SC_CRUMBLY_WOODS, - SC_PARADISE_PIER, - SC_LIGHTNING_PEAKS, - SC_IVORY_TOWERS, - SC_RAINBOW_VALLEY, - SC_THUNDER_ROCK, - SC_MEGA_PARK, - - // Loopy Landscapes - SC_ICEBERG_ISLANDS, - SC_VOLCANIA, - SC_ARID_HEIGHTS, - SC_RAZOR_ROCKS, - SC_CRATER_LAKE, - SC_VERTIGO_VIEWS, - SC_PARADISE_PIER_2, - SC_DRAGONS_COVE, - SC_GOOD_KNIGHT_PARK, - SC_WACKY_WARREN, - - // Special - ALTON_TOWERS, - FORT_ANACHRONISM, - - // Added Attractions - SC_WHISPERING_CLIFFS = 40, - SC_THREE_MONKEYS_PARK, - SC_CANARY_MINES, - SC_BARONY_BRIDGE, - SC_FUNTOPIA, - SC_HAUNTED_HARBOUR, - SC_FUN_FORTRESS, - SC_FUTURE_WORLD, - SC_GENTLE_GLEN, - SC_JOLLY_JUNGLE, - SC_HYDRO_HILLS, - SC_SPRIGHTLY_PARK, - SC_MAGIC_QUARTERS, - SC_FRUIT_FARM, - SC_BUTTERFLY_DAM, - SC_COASTER_CANYON, - SC_THUNDERSTORM_PARK, - SC_HARMONIC_HILLS, - SC_ROMAN_VILLAGE, - SC_SWAMP_COVE, - SC_ADRENALINE_HEIGHTS, - SC_UTOPIA, - SC_ROTTING_HEIGHTS, - SC_FIASCO_FOREST, - SC_PICKLE_PARK, - SC_GIGGLE_DOWNS, - SC_MINERAL_PARK, - SC_COASTER_CRAZY, - SC_URBAN_PARK, - SC_GEOFFREY_GARDENS, - - // Special - SC_HEIDE_PARK, - SC_PCPLAYER, - SC_PCGW, - SC_GAMEPLAY, - SC_BLACKPOOL_PLEASURE_BEACH, - - // Loopy Landscapes - SC_GRAND_GLACIER = 80, - SC_CRAZY_CRATERS, - SC_DUSTY_DESERT, - SC_WOODWORM_PARK, - SC_ICARUS_PARK, - SC_SUNNY_SWAMPS, - SC_FRIGHTMARE_HILLS, - SC_THUNDER_ROCKS, - SC_OCTAGON_PARK, - SC_PLEASURE_ISLAND, - SC_ICICLE_WORLDS, - SC_SOUTHERN_SANDS, - SC_TINY_TOWERS, - SC_NEVERMORE_PARK, - SC_PACIFICA, - SC_URBAN_JUNGLE, - SC_TERROR_TOWN, - SC_MEGAWORLD_PARK, - SC_VENUS_PONDS, - SC_MICRO_PARK, -}; - #endif diff --git a/src/scenario_list.c b/src/scenario_list.c deleted file mode 100644 index 3fd82c0ee0..0000000000 --- a/src/scenario_list.c +++ /dev/null @@ -1,532 +0,0 @@ -#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers -/***************************************************************************** - * OpenRCT2, an open source clone of Roller Coaster Tycoon 2. - * - * OpenRCT2 is the work of many authors, a full list can be found in contributors.md - * For more information, visit https://github.com/OpenRCT2/OpenRCT2 - * - * OpenRCT2 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. - * - * A full copy of the GNU General Public License can be found in licence.txt - *****************************************************************************/ -#pragma endregion - -#include "config.h" -#include "localisation/localisation.h" -#include "object_list.h" -#include "platform/platform.h" -#include "rct2.h" -#include "scenario.h" -#include "util/util.h" - -// Scenario list -int gScenarioListCount = 0; -int gScenarioListCapacity = 0; -scenario_index_entry *gScenarioList = NULL; - -int gScenarioHighscoreListCount = 0; -int gScenarioHighscoreListCapacity = 0; -scenario_highscore_entry *gScenarioHighscoreList = NULL; - -static void scenario_list_include(const utf8 *directory); -static void scenario_list_add(const utf8 *path, uint64 timestamp); -static void scenario_list_sort(); -static int scenario_list_sort_by_category(const void *a, const void *b); -static int scenario_list_sort_by_index(const void *a, const void *b); - -static bool scenario_scores_load(); -static void scenario_scores_legacy_get_path(utf8 *outPath, size_t size); -static bool scenario_scores_legacy_load(const utf8 *path); -static void scenario_highscore_remove(scenario_highscore_entry *higscore); -static void scenario_highscore_list_dispose(); -static utf8 *io_read_string(SDL_RWops *file); -static void io_write_string(SDL_RWops *file, utf8 *source); - -/** - * Searches and grabs the metadata for all the scenarios. - */ -void scenario_load_list() -{ - utf8 directory[MAX_PATH]; - - // Clear scenario list - gScenarioListCount = 0; - - // Get scenario directory from RCT2 - safe_strcpy(directory, gRCT2AddressAppPath, sizeof(directory)); - safe_strcat_path(directory, "Scenarios", sizeof(directory)); - scenario_list_include(directory); - - // Get scenario directory from user directory - platform_get_user_directory(directory, "scenario", sizeof(directory)); - scenario_list_include(directory); - - scenario_list_sort(); - scenario_scores_load(); - - utf8 scoresPath[MAX_PATH]; - scenario_scores_legacy_get_path(scoresPath, sizeof(scoresPath)); - scenario_scores_legacy_load(scoresPath); - scenario_scores_legacy_load(get_file_path(PATH_ID_SCORES)); -} - -static void scenario_list_include(const utf8 *directory) -{ - int handle; - file_info fileInfo; - - // Scenarios in this directory - utf8 pattern[MAX_PATH]; - safe_strcpy(pattern, directory, sizeof(pattern)); - safe_strcat_path(pattern, "*.sc6", sizeof(pattern)); - - handle = platform_enumerate_files_begin(pattern); - while (platform_enumerate_files_next(handle, &fileInfo)) { - utf8 path[MAX_PATH]; - safe_strcpy(path, directory, sizeof(pattern)); - safe_strcat_path(path, fileInfo.path, sizeof(pattern)); - scenario_list_add(path, fileInfo.last_modified); - } - platform_enumerate_files_end(handle); - - // Include sub-directories - utf8 subDirectory[MAX_PATH]; - handle = platform_enumerate_directories_begin(directory); - while (platform_enumerate_directories_next(handle, subDirectory)) { - utf8 path[MAX_PATH]; - safe_strcpy(path, directory, sizeof(pattern)); - safe_strcat_path(path, subDirectory, sizeof(pattern)); - scenario_list_include(path); - } - platform_enumerate_directories_end(handle); -} - -static void scenario_list_add(const utf8 *path, uint64 timestamp) -{ - // Load the basic scenario information - rct_s6_header s6Header; - rct_s6_info s6Info; - if (!scenario_load_basic(path, &s6Header, &s6Info)) { - return; - } - - scenario_index_entry *newEntry = NULL; - - const utf8 *filename = path_get_filename(path); - scenario_index_entry *existingEntry = scenario_list_find_by_filename(filename); - if (existingEntry != NULL) { - bool bail = false; - const utf8 *conflictPath; - if (existingEntry->timestamp > timestamp) { - // Existing entry is more recent - conflictPath = existingEntry->path; - - // Overwrite existing entry with this one - newEntry = existingEntry; - } else { - // This entry is more recent - conflictPath = path; - bail = true; - } - printf("Scenario conflict: '%s' ignored because it is newer.\n", conflictPath); - if (bail) { - return; - } - } - - if (newEntry == NULL) { - // Increase list size - if (gScenarioListCount == gScenarioListCapacity) { - gScenarioListCapacity = max(8, gScenarioListCapacity * 2); - gScenarioList = (scenario_index_entry*)realloc(gScenarioList, gScenarioListCapacity * sizeof(scenario_index_entry)); - } - newEntry = &gScenarioList[gScenarioListCount]; - gScenarioListCount++; - } - - // Set new entry - safe_strcpy(newEntry->path, path, sizeof(newEntry->path)); - newEntry->timestamp = timestamp; - newEntry->category = s6Info.category; - newEntry->objective_type = s6Info.objective_type; - newEntry->objective_arg_1 = s6Info.objective_arg_1; - newEntry->objective_arg_2 = s6Info.objective_arg_2; - newEntry->objective_arg_3 = s6Info.objective_arg_3; - newEntry->highscore = NULL; - safe_strcpy(newEntry->name, s6Info.name, sizeof(newEntry->name)); - safe_strcpy(newEntry->details, s6Info.details, sizeof(newEntry->details)); - - // Normalise the name to make the scenario as recognisable as possible. - scenario_normalise_name(newEntry->name, sizeof(newEntry->name), newEntry->name); - - // Look up and store information regarding the origins of this scenario. - source_desc desc; - if (scenario_get_source_desc(newEntry->name, &desc)) { - newEntry->sc_id = desc.id; - newEntry->source_index = desc.index; - newEntry->source_game = desc.source; - newEntry->category = desc.category; - } else { - newEntry->sc_id = SC_UNIDENTIFIED; - newEntry->source_index = -1; - if (newEntry->category == SCENARIO_CATEGORY_REAL) { - newEntry->source_game = SCENARIO_SOURCE_REAL; - } else { - newEntry->source_game = SCENARIO_SOURCE_OTHER; - } - } - - scenario_translate(newEntry, &s6Info.entry); -} - -void scenario_list_dispose() -{ - gScenarioListCapacity = 0; - gScenarioListCount = 0; - SafeFree(gScenarioList); -} - -static void scenario_list_sort() -{ - int(*compareFunc)(void const*, void const*); - - compareFunc = gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_ORIGIN ? - scenario_list_sort_by_index : - scenario_list_sort_by_category; - - qsort(gScenarioList, gScenarioListCount, sizeof(scenario_index_entry), compareFunc); -} - -static int scenario_list_category_compare(int categoryA, int categoryB) -{ - if (categoryA == categoryB) return 0; - if (categoryA == SCENARIO_CATEGORY_DLC) return -1; - if (categoryB == SCENARIO_CATEGORY_DLC) return 1; - if (categoryA == SCENARIO_CATEGORY_BUILD_YOUR_OWN) return -1; - if (categoryB == SCENARIO_CATEGORY_BUILD_YOUR_OWN) return 1; - return sgn(categoryA - categoryB); -} - -static int scenario_list_sort_by_category(const void *a, const void *b) -{ - const scenario_index_entry *entryA = (const scenario_index_entry*)a; - const scenario_index_entry *entryB = (const scenario_index_entry*)b; - - // Order by category - if (entryA->category != entryB->category) { - return scenario_list_category_compare(entryA->category, entryB->category); - } - - // Then by source game / name - switch (entryA->category) { - default: - if (entryA->source_game != entryB->source_game) { - return entryA->source_game - entryB->source_game; - } - return strcmp(entryA->name, entryB->name); - case SCENARIO_CATEGORY_REAL: - case SCENARIO_CATEGORY_OTHER: - return strcmp(entryA->name, entryB->name); - } -} - -static int scenario_list_sort_by_index(const void *a, const void *b) -{ - const scenario_index_entry *entryA = (const scenario_index_entry*)a; - const scenario_index_entry *entryB = (const scenario_index_entry*)b; - - // Order by source game - if (entryA->source_game != entryB->source_game) { - return entryA->source_game - entryB->source_game; - } - - // Then by index / category / name - uint8 sourceGame = entryA->source_game; - switch (sourceGame) { - default: - if (entryA->source_index == -1 && entryB->source_index == -1) { - if (entryA->category == entryB->category) { - return scenario_list_sort_by_category(a, b); - } else { - return scenario_list_category_compare(entryA->category, entryB->category); - } - } else if (entryA->source_index == -1) { - return 1; - } else if (entryB->source_index == -1) { - return -1; - } else { - return entryA->source_index - entryB->source_index; - } - case SCENARIO_SOURCE_REAL: - return scenario_list_sort_by_category(a, b); - } -} - -scenario_index_entry *scenario_list_find_by_filename(const utf8 *filename) -{ - for (int i = 0; i < gScenarioListCount; i++) { - const utf8 *scenarioFilename = path_get_filename(gScenarioList[i].path); - if (_strcmpi(filename, scenarioFilename) == 0) { - return &gScenarioList[i]; - } - } - return NULL; -} - -scenario_index_entry *scenario_list_find_by_path(const utf8 *path) -{ - for (int i = 0; i < gScenarioListCount; i++) { - if (_strcmpi(path, gScenarioList[i].path) == 0) { - return &gScenarioList[i]; - } - } - return NULL; -} - -/** - * Gets the path for the scenario scores path. - */ -static void scenario_scores_get_path(utf8 *outPath, size_t size) -{ - platform_get_user_directory(outPath, NULL, size); - safe_strcat_path(outPath, "highscores.dat", size); -} - -/** - * Gets the path for the scenario scores path. - */ -static void scenario_scores_legacy_get_path(utf8 *outPath, size_t size) -{ - platform_get_user_directory(outPath, NULL, size); - safe_strcat_path(outPath, "scores.dat", size); -} - -/** - * Loads the original scores.dat file and replaces any highscores that - * are better for matching scenarios. - */ -static bool scenario_scores_legacy_load(const utf8 *path) -{ - // First check user folder and then fallback to install directory - SDL_RWops *file = SDL_RWFromFile(path, "rb"); - if (file == NULL) { - return false; - } - - Sint64 fileSize = SDL_RWsize(file); - if (fileSize <= 4) { - // Initial value of scores for RCT2, just ignore - return false; - } - - // Load header - rct_scenario_scores_header header; - if (SDL_RWread(file, &header, 16, 1) != 1) { - SDL_RWclose(file); - log_error("Invalid header in legacy scenario scores file."); - return false; - } - - // Read scenarios - bool highscoresDirty = false; - for (uint32 i = 0; i < header.scenario_count; i++) { - // Read legacy entry - rct_scenario_basic scBasic; - if (SDL_RWread(file, &scBasic, sizeof(rct_scenario_basic), 1) != 1) { - break; - } - - // Ignore non-completed scenarios - if (!(scBasic.flags & SCENARIO_FLAGS_COMPLETED)) { - continue; - } - - // Find matching scenario entry - scenario_index_entry *scenarioIndexEntry = scenario_list_find_by_filename(scBasic.path); - if (scenarioIndexEntry != NULL) { - // Check if legacy highscore is better - scenario_highscore_entry *highscore = scenarioIndexEntry->highscore; - if (highscore == NULL) { - highscore = scenario_highscore_insert(); - scenarioIndexEntry->highscore = highscore; - } else if (highscore->company_value < (money32)scBasic.company_value) { - scenario_highscore_free(highscore); - // Re-use highscore entry - } else { - highscore = NULL; - } - - // Set new highscore - if (highscore != NULL) { - highscore->fileName = _strdup(scBasic.path); - highscore->name = win1252_to_utf8_alloc(scBasic.completed_by); - highscore->company_value = (money32)scBasic.company_value; - highscore->timestamp = DATETIME64_MIN; - highscoresDirty = true; - } - } - } - SDL_RWclose(file); - - if (highscoresDirty) { - scenario_scores_save(); - } - return true; -} - -static bool scenario_scores_load() -{ - utf8 scoresPath[MAX_PATH]; - scenario_scores_get_path(scoresPath, sizeof(scoresPath)); - - // Load scores file - SDL_RWops *file = SDL_RWFromFile(scoresPath, "rb"); - if (file == NULL) { - return false; - } - - // Check file version - uint32 fileVersion; - SDL_RWread(file, &fileVersion, sizeof(fileVersion), 1); - if (fileVersion != 1) { - log_error("Invalid or incompatible highscores file."); - return false; - } - - // Read and allocate the highscore list - scenario_highscore_list_dispose(); - SDL_RWread(file, &gScenarioHighscoreListCount, sizeof(gScenarioHighscoreListCount), 1); - gScenarioHighscoreListCapacity = gScenarioHighscoreListCount; - gScenarioHighscoreList = malloc(gScenarioHighscoreListCapacity * sizeof(scenario_highscore_entry)); - - // Read highscores - for (int i = 0; i < gScenarioHighscoreListCount; i++) { - scenario_highscore_entry *highscore = &gScenarioHighscoreList[i]; - highscore->fileName = io_read_string(file); - highscore->name = io_read_string(file); - SDL_RWread(file, &highscore->company_value, sizeof(highscore->company_value), 1); - SDL_RWread(file, &highscore->timestamp, sizeof(highscore->timestamp), 1); - - // Attach highscore to correct scenario entry - if (highscore->fileName == NULL) { - continue; - } - scenario_index_entry *scenarioIndexEntry = scenario_list_find_by_filename(highscore->fileName); - if (scenarioIndexEntry != NULL) { - scenarioIndexEntry->highscore = highscore; - } - } - - SDL_RWclose(file); - return true; -} - -/** - * - * rct2: 0x00677B50 - */ -bool scenario_scores_save() -{ - utf8 scoresPath[MAX_PATH]; - scenario_scores_get_path(scoresPath, sizeof(scoresPath)); - - SDL_RWops *file = SDL_RWFromFile(scoresPath, "wb"); - if (file == NULL) { - log_error("Unable to save scenario scores."); - return false; - } - - const uint32 fileVersion = 1; - - SDL_RWwrite(file, &fileVersion, sizeof(fileVersion), 1); - SDL_RWwrite(file, &gScenarioHighscoreListCount, sizeof(gScenarioHighscoreListCount), 1); - for (int i = 0; i < gScenarioHighscoreListCount; i++) { - scenario_highscore_entry *highscore = &gScenarioHighscoreList[i]; - io_write_string(file, highscore->fileName); - io_write_string(file, highscore->name); - SDL_RWwrite(file, &highscore->company_value, sizeof(highscore->company_value), 1); - SDL_RWwrite(file, &highscore->timestamp, sizeof(highscore->timestamp), 1); - } - SDL_RWclose(file); - - return true; -} - -scenario_highscore_entry *scenario_highscore_insert() -{ - if (gScenarioHighscoreListCount >= gScenarioHighscoreListCapacity) { - gScenarioHighscoreListCapacity = max(8, gScenarioHighscoreListCapacity * 2); - gScenarioHighscoreList = realloc(gScenarioHighscoreList, gScenarioHighscoreListCapacity * sizeof(scenario_highscore_entry)); - } - return &gScenarioHighscoreList[gScenarioHighscoreListCount++]; -} - -static void scenario_highscore_remove(scenario_highscore_entry *highscore) -{ - for (int i = 0; i < gScenarioHighscoreListCount; i++) { - if (&gScenarioHighscoreList[i] == highscore) { - size_t moveSize = (gScenarioHighscoreListCount - i - 1) * sizeof(scenario_highscore_entry); - if (moveSize > 0) { - memmove(&gScenarioHighscoreList[i], &gScenarioHighscoreList[i + 1], moveSize); - } - return; - } - } -} - -void scenario_highscore_free(scenario_highscore_entry *highscore) -{ - SafeFree(highscore->fileName); - SafeFree(highscore->name); -} - -static void scenario_highscore_list_dispose() -{ - for (int i = 0; i < gScenarioHighscoreListCount; i++) { - scenario_highscore_free(&gScenarioHighscoreList[i]); - } - gScenarioHighscoreListCapacity = 0; - gScenarioHighscoreListCount = 0; - SafeFree(gScenarioHighscoreList); -} - -static utf8 *io_read_string(SDL_RWops *file) -{ - size_t bufferCount = 0; - size_t bufferCapacity = 0; - utf8 *buffer = NULL; - - utf8 ch; - do { - SDL_RWread(file, &ch, sizeof(ch), 1); - if (ch == '\0' && buffer == NULL) { - break; - } - - if (bufferCount >= bufferCapacity) { - bufferCapacity = max(32, bufferCapacity * 2); - buffer = realloc(buffer, bufferCapacity * sizeof(uint8)); - } - - buffer[bufferCount] = ch; - bufferCount++; - } while (ch != '\0'); - - if (bufferCount < bufferCapacity) { - buffer = realloc(buffer, bufferCount); - } - return buffer; -} - -static void io_write_string(SDL_RWops *file, utf8 *source) -{ - if (source == NULL) { - utf8 empty = 0; - SDL_RWwrite(file, &empty, sizeof(utf8), 1); - } else { - SDL_RWwrite(file, source, strlen(source) + 1, 1); - } -} diff --git a/src/title.c b/src/title.c index 8cb0af6b9d..0273d38eb7 100644 --- a/src/title.c +++ b/src/title.c @@ -33,6 +33,8 @@ #include "peep/staff.h" #include "ride/ride.h" #include "scenario.h" +#include "ScenarioRepository.h" +#include "ScenarioSources.h" #include "util/util.h" #include "world/climate.h" #include "world/map.h" @@ -423,9 +425,11 @@ static void title_do_next_script_opcode() } const utf8 *path = NULL; - for (int i = 0; i < gScenarioListCount; i++) { - if (gScenarioList[i].source_index == sourceDesc.index) { - path = gScenarioList[i].path; + size_t numScenarios = scenario_repository_get_count(); + for (size_t i = 0; i < numScenarios; i++) { + const scenario_index_entry * scenario = scenario_repository_get_by_index(i); + if (scenario->source_index == sourceDesc.index) { + path = scenario->path; break; } } diff --git a/src/windows/title_scenarioselect.c b/src/windows/title_scenarioselect.c index b9231372b8..c4e64c3b81 100644 --- a/src/windows/title_scenarioselect.c +++ b/src/windows/title_scenarioselect.c @@ -18,7 +18,8 @@ #include "../audio/audio.h" #include "../localisation/date.h" #include "../localisation/localisation.h" -#include "../scenario.h" +#include "../ScenarioRepository.h" +#include "../ScenarioSources.h" #include "../sprites.h" #include "../interface/widget.h" #include "../interface/window.h" @@ -41,7 +42,7 @@ typedef struct sc_list_item { rct_string_id string_id; } heading; struct { - scenario_index_entry *scenario; + const scenario_index_entry *scenario; bool is_locked; } scenario; }; @@ -138,7 +139,7 @@ static rct_window_event_list window_scenarioselect_events = { static void draw_category_heading(rct_window *w, rct_drawpixelinfo *dpi, int left, int right, int y, rct_string_id stringId); static void initialise_list_items(rct_window *w); -static bool is_scenario_visible(rct_window *w, scenario_index_entry *scenario); +static bool is_scenario_visible(rct_window *w, const scenario_index_entry *scenario); static bool is_locking_enabled(rct_window *w); static scenarioselect_callback _callback; @@ -160,7 +161,7 @@ void window_scenarioselect_open(scenarioselect_callback callback) return; // Load scenario list - scenario_load_list(); + scenario_repository_scan(); // Shrink the window if we're showing scenarios by difficulty level. if (gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_DIFFICULTY) { @@ -196,8 +197,9 @@ void window_scenarioselect_open(scenarioselect_callback callback) static void window_scenarioselect_init_tabs(rct_window *w) { int showPages = 0; - for (int i = 0; i < gScenarioListCount; i++) { - scenario_index_entry *scenario = &gScenarioList[i]; + size_t numScenarios = scenario_repository_get_count(); + for (size_t i = 0; i < numScenarios; i++) { + const scenario_index_entry *scenario = scenario_repository_get_by_index(i); if (gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_ORIGIN) { showPages |= 1 << scenario->source_game; } else { @@ -304,7 +306,7 @@ static void window_scenarioselect_scrollmouseover(rct_window *w, int scrollIndex { bool originalShowLockedInformation = _showLockedInformation; _showLockedInformation = false; - scenario_index_entry *selected = NULL; + const scenario_index_entry *selected = NULL; for (sc_list_item *listItem = _listItems; listItem->type != LIST_ITEM_TYPE_END; listItem++) { switch (listItem->type) { case LIST_ITEM_TYPE_HEADING: @@ -364,7 +366,7 @@ static void window_scenarioselect_paint(rct_window *w, rct_drawpixelinfo *dpi) { int i, x, y, format; rct_widget *widget; - scenario_index_entry *scenario; + const scenario_index_entry *scenario; window_draw_widgets(w, dpi); @@ -474,7 +476,7 @@ static void window_scenarioselect_scrollpaint(rct_window *w, rct_drawpixelinfo * break; case LIST_ITEM_TYPE_SCENARIO:; // Draw hover highlight - scenario_index_entry *scenario = listItem->scenario.scenario; + const scenario_index_entry *scenario = listItem->scenario.scenario; bool isHighlighted = w->highlighted_scenario == scenario; if (isHighlighted) { gfx_fill_rect(dpi, 0, y, w->width, y + 23, 0x02000031); @@ -550,19 +552,20 @@ static void initialise_list_items(rct_window *w) { SafeFree(_listItems); - int capacity = gScenarioListCount + 16; - int length = 0; + size_t numScenarios = scenario_repository_get_count(); + size_t capacity = numScenarios + 16; + size_t length = 0; _listItems = malloc(capacity * sizeof(sc_list_item)); // Mega park unlock const uint32 rct1RequiredCompletedScenarios = (1 << SC_MEGA_PARK) - 1; uint32 rct1CompletedScenarios = 0; - int megaParkListItemIndex = -1; + size_t megaParkListItemIndex = SIZE_MAX; int numUnlocks = INITIAL_NUM_UNLOCKED_SCENARIOS; uint8 currentHeading = UINT8_MAX; - for (int i = 0; i < gScenarioListCount; i++) { - scenario_index_entry *scenario = &gScenarioList[i]; + for (size_t i = 0; i < numScenarios; i++) { + const scenario_index_entry *scenario = scenario_repository_get_by_index(i); if (!is_scenario_visible(w, scenario)) { continue; } @@ -640,12 +643,12 @@ static void initialise_list_items(rct_window *w) _listItems[length - 1].type = LIST_ITEM_TYPE_END; // Mega park handling - if (megaParkListItemIndex != -1) { + if (megaParkListItemIndex != SIZE_MAX) { bool megaParkLocked = (rct1CompletedScenarios & rct1RequiredCompletedScenarios) != rct1RequiredCompletedScenarios; _listItems[megaParkListItemIndex].scenario.is_locked = megaParkLocked; if (megaParkLocked && gConfigGeneral.scenario_hide_mega_park) { // Remove mega park - int remainingItems = length - megaParkListItemIndex - 1; + size_t remainingItems = length - megaParkListItemIndex - 1; memmove(&_listItems[megaParkListItemIndex], &_listItems[megaParkListItemIndex + 1], remainingItems); // Remove empty headings @@ -663,7 +666,7 @@ static void initialise_list_items(rct_window *w) } } -static bool is_scenario_visible(rct_window *w, scenario_index_entry *scenario) +static bool is_scenario_visible(rct_window *w, const scenario_index_entry *scenario) { if (gConfigGeneral.scenario_select_mode == SCENARIO_SELECT_MODE_ORIGIN) { if (scenario->source_game != w->selected_tab) {