1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-16 19:43:06 +01:00
Files
OpenRCT2/src/scenario_list.c
2016-04-22 22:07:42 +02:00

554 lines
16 KiB
C

/*****************************************************************************
* Copyright (c) 2014 Ted John
* OpenRCT2, an open source clone of Roller Coaster Tycoon 2.
*
* This file is part of 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.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*****************************************************************************/
#include "addresses.h"
#include "config.h"
#include "localisation/localisation.h"
#include "platform/platform.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 void scenario_translate(scenario_index_entry *scenarioEntry, const rct_object_entry *stexObjectEntry);
static bool scenario_scores_load();
static void scenario_scores_legacy_get_path(utf8 *outPath);
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, gConfigGeneral.game_path, 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");
scenario_list_include(directory);
scenario_list_sort();
scenario_scores_load();
utf8 scoresPath[MAX_PATH];
scenario_scores_legacy_get_path(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);
// 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);
}
static void scenario_translate(scenario_index_entry *scenarioEntry, const rct_object_entry *stexObjectEntry)
{
rct_string_id localisedStringIds[3];
if (language_get_localised_scenario_strings(scenarioEntry->name, localisedStringIds)) {
if (localisedStringIds[0] != STR_NONE) {
safe_strcpy(scenarioEntry->name, language_get_string(localisedStringIds[0]), 64);
}
if (localisedStringIds[2] != STR_NONE) {
safe_strcpy(scenarioEntry->details, language_get_string(localisedStringIds[2]), 256);
}
} else {
// Checks for a scenario string object (possibly for localisation)
if ((stexObjectEntry->flags & 0xFF) != 255) {
if (object_get_scenario_text((rct_object_entry*)stexObjectEntry)) {
rct_stex_entry* stex_entry = RCT2_GLOBAL(RCT2_ADDRESS_SCENARIO_TEXT_TEMP_CHUNK, rct_stex_entry*);
format_string(scenarioEntry->name, stex_entry->scenario_name, NULL);
format_string(scenarioEntry->details, stex_entry->details, NULL);
object_free_scenario_text();
}
}
}
}
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)
{
platform_get_user_directory(outPath, NULL);
strcat(outPath, "highscores.dat");
}
/**
* Gets the path for the scenario scores path.
*/
static void scenario_scores_legacy_get_path(utf8 *outPath)
{
platform_get_user_directory(outPath, NULL);
strcat(outPath, "scores.dat");
}
/**
* 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;
}
// 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 = _strdup(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);
// 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);
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);
}
}