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) {