diff --git a/OpenRCT2.xcodeproj/project.pbxproj b/OpenRCT2.xcodeproj/project.pbxproj index 2b5311724e..dfc7179f70 100644 --- a/OpenRCT2.xcodeproj/project.pbxproj +++ b/OpenRCT2.xcodeproj/project.pbxproj @@ -471,6 +471,8 @@ F7D774AC1EC6741D00BE6EBC /* language in CopyFiles */ = {isa = PBXBuildFile; fileRef = D4EC48E41C2637710024B507 /* language */; }; F7D774AD1EC6741D00BE6EBC /* shaders in CopyFiles */ = {isa = PBXBuildFile; fileRef = D43407E11D0E14CE00C2B3D4 /* shaders */; }; F7D774AE1EC6741D00BE6EBC /* title in CopyFiles */ = {isa = PBXBuildFile; fileRef = D4EC48E51C2637710024B507 /* title */; }; + F7FFCDAA1F02FD8E0078BFFB /* object_load_error.c in Sources */ = {isa = PBXBuildFile; fileRef = F7FFCDA91F02FD8E0078BFFB /* object_load_error.c */; }; + F7FFCDAC1F02FF570078BFFB /* object_load_error.c in Sources */ = {isa = PBXBuildFile; fileRef = F7FFCDA91F02FD8E0078BFFB /* object_load_error.c */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1354,6 +1356,8 @@ F7CB864C1EEDA1A80030C877 /* WindowManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WindowManager.h; sourceTree = ""; }; F7D7747E1EC61E5100BE6EBC /* UiContext.macOS.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = UiContext.macOS.mm; sourceTree = ""; usesTabs = 0; }; F7D774841EC66CD700BE6EBC /* OpenRCT2-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "OpenRCT2-cli"; sourceTree = BUILT_PRODUCTS_DIR; }; + F7FFCDA91F02FD8E0078BFFB /* object_load_error.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = object_load_error.c; sourceTree = ""; }; + F7FFCDAB1F02FEC00078BFFB /* park_load_result_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = park_load_result_types.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1776,6 +1780,7 @@ F76C84371EC4E7CC00FA49E2 /* object_list.h */, F76C84381EC4E7CC00FA49E2 /* OpenRCT2.cpp */, F76C84391EC4E7CC00FA49E2 /* OpenRCT2.h */, + F7FFCDAB1F02FEC00078BFFB /* park_load_result_types.h */, F76C84511EC4E7CC00FA49E2 /* ParkImporter.cpp */, F76C84521EC4E7CC00FA49E2 /* ParkImporter.h */, F76C84641EC4E7CC00FA49E2 /* PlatformEnvironment.cpp */, @@ -2425,6 +2430,7 @@ F76C85321EC4E7CD00FA49E2 /* new_ride.c */, F76C85331EC4E7CD00FA49E2 /* news.c */, F76C85341EC4E7CD00FA49E2 /* news_options.c */, + F7FFCDA91F02FD8E0078BFFB /* object_load_error.c */, F76C85351EC4E7CD00FA49E2 /* options.c */, F76C85361EC4E7CD00FA49E2 /* park.c */, F76C85371EC4E7CD00FA49E2 /* player.c */, @@ -3014,6 +3020,7 @@ F76C888C1EC5324E00FA49E2 /* UiContext.cpp in Sources */, F76C888D1EC5324E00FA49E2 /* UiContext.Linux.cpp in Sources */, F76C888E1EC5324E00FA49E2 /* UiContext.Win32.cpp in Sources */, + F7FFCDAA1F02FD8E0078BFFB /* object_load_error.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3021,6 +3028,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F7FFCDAC1F02FF570078BFFB /* object_load_error.c in Sources */, F7CB864E1EEDA2050030C877 /* DummyWindowManager.cpp in Sources */, F775F5381EE3725C001F00E7 /* DummyAudioContext.cpp in Sources */, F775F5351EE35A89001F00E7 /* DummyUiContext.cpp in Sources */, diff --git a/contributors.md b/contributors.md index 5c906fa9d1..04dcad5afc 100644 --- a/contributors.md +++ b/contributors.md @@ -9,6 +9,7 @@ Includes all git commit authors. Aliases are GitHub user names. * Lewis Fox (LRFLEW) - Developer, macOS management * Marijn van der Werf (marijnvdwerf) - Developer * (zsilencer) - Developer +* Richard Jenkins (rwjuk) - Developer, issue management ## Implementation (RCT2) * Ted John (IntelOrca) diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index 5fefe5deaa..2126236283 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -4431,6 +4431,14 @@ STR_6119 :A cheap and easy to build roller coaster, but with a limited height STR_6120 :{BABYBLUE}New vehicle now available for {STRINGID}:{NEWLINE}{STRINGID} STR_6121 :{SMALLFONT}{BLACK}Extends the park's land rights all the way to the edges of the map STR_6122 :There are not enough roller coasters in this scenario! +STR_6123 :Error loading objects for park +STR_6124 :Object name +STR_6125 :Object type +STR_6126 :Unknown type +STR_6127 :File: {STRING} +STR_6128 :The file could not be loaded as some of the objects referenced in it are missing or corrupt. A list of these objects is given below. +STR_6129 :Copy selected item to clipboard +STR_6130 :Copy entire list to clipboard ############# # Scenarios # diff --git a/distribution/changelog.txt b/distribution/changelog.txt index edc815b9a4..1f9e5ae6ea 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -1,5 +1,6 @@ 0.0.8 (in development) ------------------------------------------------------------------------ +- Feature: [#1399 (partial), #5177] Add window that displays any missing/corrupt objects when loading a park - Feature: [#5056] Add cheat to own all land. - Feature: [#5133] Add option to display guest expenditure (as seen in RCTC). - Feature: [#5196] Add cheat to disable ride ageing. diff --git a/src/openrct2-ui/UiContext.cpp b/src/openrct2-ui/UiContext.cpp index 988feb218d..b12aea7dac 100644 --- a/src/openrct2-ui/UiContext.cpp +++ b/src/openrct2-ui/UiContext.cpp @@ -639,6 +639,11 @@ public: } } + bool SetClipboardText(const utf8* target) override + { + return (SDL_SetClipboardText(target) == 0); + } + private: void OnResize(sint32 width, sint32 height) diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index ea7da2c897..d067bd9791 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -721,4 +721,9 @@ extern "C" return nullptr; } } + + bool platform_place_string_on_clipboard(utf8* target) + { + return GetContext()->GetUiContext()->SetClipboardText(target); + } } diff --git a/src/openrct2/ParkImporter.h b/src/openrct2/ParkImporter.h index bbce6a92b9..f66a062c1a 100644 --- a/src/openrct2/ParkImporter.h +++ b/src/openrct2/ParkImporter.h @@ -18,6 +18,15 @@ #include "common.h" +#ifdef __cplusplus +extern "C" +{ +#endif +#include "park_load_result_types.h" +#ifdef __cplusplus +} +#endif + #ifdef __cplusplus #include @@ -34,10 +43,10 @@ interface IParkImporter { public: virtual ~IParkImporter() = default; - virtual void Load(const utf8 * path) abstract; - virtual void LoadSavedGame(const utf8 * path) abstract; - virtual void LoadScenario(const utf8 * path) abstract; - virtual void LoadFromStream(IStream * stream, bool isScenario) abstract; + virtual park_load_result * Load(const utf8 * path) abstract; + virtual park_load_result * LoadSavedGame(const utf8 * path) abstract; + virtual park_load_result * LoadScenario(const utf8 * path) abstract; + virtual park_load_result * LoadFromStream(IStream * stream, bool isScenario) abstract; virtual void Import() abstract; virtual bool GetDetails(scenario_index_entry * dst) abstract; }; diff --git a/src/openrct2/editor.c b/src/openrct2/editor.c index 140f30aa4f..b9ab7e7f02 100644 --- a/src/openrct2/editor.c +++ b/src/openrct2/editor.c @@ -261,14 +261,14 @@ static sint32 editor_load_landscape_from_sc4(const char *path) */ static sint32 editor_read_s6(const char *path) { - bool loadResult = false; + park_load_result* loadResult = { 0 }; const char *extension = path_get_extension(path); if (_stricmp(extension, ".sc6") == 0) { loadResult = scenario_load(path); } else if (_stricmp(extension, ".sv6") == 0) { loadResult = game_load_sv6_path(path); } - if (!loadResult) { + if (loadResult->error != PARK_LOAD_ERROR_NONE) { return 0; } diff --git a/src/openrct2/game.c b/src/openrct2/game.c index 5e1ff8993a..4c712f40c2 100644 --- a/src/openrct2/game.c +++ b/src/openrct2/game.c @@ -1094,18 +1094,20 @@ bool game_load_save(const utf8 *path) safe_strcpy(gScenarioSavePath, path, MAX_PATH); uint32 extension_type = get_file_extension_type(path); - bool result = false; + park_load_result* result = {0}; + bool load_success = false; if (extension_type == FILE_EXTENSION_SV6) { result = game_load_sv6_path(path); - if (result) + load_success = (result->error == PARK_LOAD_ERROR_NONE); + if (load_success) gFirstTimeSaving = false; } else if (extension_type == FILE_EXTENSION_SV4) { - result = rct1_load_saved_game(path); - if (result) + load_success = rct1_load_saved_game(path); + if (load_success) gFirstTimeSaving = true; } - if (result) { + if (load_success) { if (network_get_mode() == NETWORK_MODE_CLIENT) { network_close(); } @@ -1120,9 +1122,16 @@ bool game_load_save(const utf8 *path) return true; } else { - // If loading the SV6 or SV4 failed, the current park state will be corrupted - // so just go back to the title screen. - title_load(); + if (result->error == PARK_LOAD_ERROR_BAD_OBJECTS) + { + // The path needs to be duplicated as it's a const here + // which the window function doesn't like + window_object_load_error_open(strndup(path, strnlen(path, MAX_PATH)), result->object_validity); + } else { + // If loading the SV6 or SV4 failed for a reason other than invalid objects + // the current park state will be corrupted so just go back to the title screen. + title_load(); + } return false; } } diff --git a/src/openrct2/game.h b/src/openrct2/game.h index 1f205694b7..74986880e3 100644 --- a/src/openrct2/game.h +++ b/src/openrct2/game.h @@ -20,6 +20,7 @@ #include "rct2/addresses.h" #include "common.h" #include "scenario/scenario.h" +#include "park_load_result_types.h" enum GAME_COMMAND { GAME_COMMAND_SET_RIDE_APPEARANCE, @@ -120,6 +121,12 @@ enum { ERROR_TYPE_FILE_LOAD = 255 }; +enum PARK_LOAD_ERROR { + PARK_LOAD_ERROR_NONE, + PARK_LOAD_ERROR_BAD_OBJECTS, + PARK_LOAD_ERROR_UNKNOWN = 255 +}; + typedef void (GAME_COMMAND_POINTER)(sint32* eax, sint32* ebx, sint32* ecx, sint32* edx, sint32* esi, sint32* edi, sint32* ebp); typedef void (GAME_COMMAND_CALLBACK_POINTER)(sint32 eax, sint32 ebx, sint32 ecx, sint32 edx, sint32 esi, sint32 edi, sint32 ebp); @@ -170,7 +177,7 @@ sint32 game_do_command_p(sint32 command, sint32 *eax, sint32 *ebx, sint32 *ecx, void game_log_multiplayer_command(int command, int *eax, int* ebx, int* ecx, int* edx, int* edi, int* ebp); void game_load_or_quit_no_save_prompt(); -bool game_load_sv6_path(const char * path); +park_load_result * game_load_sv6_path(const char * path); bool game_load_save(const utf8 *path); void game_load_init(); void game_pause_toggle(sint32 *eax, sint32 *ebx, sint32 *ecx, sint32 *edx, sint32 *esi, sint32 *edi, sint32 *ebp); diff --git a/src/openrct2/interface/window.h b/src/openrct2/interface/window.h index f72def0a68..72525e131b 100644 --- a/src/openrct2/interface/window.h +++ b/src/openrct2/interface/window.h @@ -477,6 +477,7 @@ enum { WC_CUSTOM_CURRENCY_CONFIG = 129, WC_DEBUG_PAINT = 130, WC_VIEW_CLIPPING = 131, + WC_OBJECT_LOAD_ERROR = 132, // Only used for colour schemes WC_STAFF = 220, @@ -766,6 +767,7 @@ rct_window *window_mapgen_open(); rct_window *window_loadsave_open(sint32 type, char *defaultName); rct_window *window_changelog_open(); void window_debug_paint_open(); +rct_window *window_object_load_error_open(utf8* path, object_validity_result* result); rct_window * window_editor_main_open(); void window_editor_bottom_toolbar_open(); diff --git a/src/openrct2/localisation/string_ids.h b/src/openrct2/localisation/string_ids.h index 54984fa6da..21048584fe 100644 --- a/src/openrct2/localisation/string_ids.h +++ b/src/openrct2/localisation/string_ids.h @@ -3791,6 +3791,15 @@ enum { STR_NOT_ENOUGH_ROLLER_COASTERS = 6122, + STR_OBJECT_LOAD_ERROR_TITLE = 6123, + STR_OBJECT_NAME = 6124, + STR_OBJECT_TYPE = 6125, + STR_UNKNOWN_OBJECT_TYPE = 6126, + STR_OBJECT_ERROR_WINDOW_FILE = 6127, + STR_OBJECT_ERROR_WINDOW_EXPLANATION = 6128, + STR_COPY_SELECTED = 6129, + STR_COPY_ALL = 6130, + // Have to include resource strings (from scenarios and objects) for the time being now that language is partially working STR_COUNT = 32768 }; diff --git a/src/openrct2/object/ObjectManager.cpp b/src/openrct2/object/ObjectManager.cpp index 9672c7b6fb..b4e77135e4 100644 --- a/src/openrct2/object/ObjectManager.cpp +++ b/src/openrct2/object/ObjectManager.cpp @@ -36,8 +36,8 @@ extern "C" class ObjectManager final : public IObjectManager { private: - IObjectRepository * _objectRepository; - Object * * _loadedObjects = nullptr; + IObjectRepository * _objectRepository; + Object * * _loadedObjects = nullptr; public: explicit ObjectManager(IObjectRepository * objectRepository) @@ -438,6 +438,51 @@ private: return entryIndex; } + rct_object_entry* DuplicateObjectEntry(const rct_object_entry* original) + { + rct_object_entry * duplicate = Memory::Allocate(sizeof(rct_object_entry)); + duplicate->checksum = original->checksum; + strncpy(duplicate->name, original->name, 8); + duplicate->flags = original->flags; + return duplicate; + } + + object_validity_result* GetInvalidObjects(const rct_object_entry * entries) override + { + uint16 invalidObjectCount = 0; + rct_object_entry * * invalidEntries = Memory::AllocateArray(OBJECT_ENTRY_COUNT); + for (sint32 i = 0; i < OBJECT_ENTRY_COUNT; i++) + { + const rct_object_entry * entry = &entries[i]; + const ObjectRepositoryItem * ori = nullptr; + if (!object_entry_is_empty(entry)) + { + ori = _objectRepository->FindObject(entry); + if (ori == nullptr) + { + invalidEntries[invalidObjectCount++] = DuplicateObjectEntry(entry); + } + else + { + Object * loadedObject = nullptr; + loadedObject = ori->LoadedObject; + if (loadedObject == nullptr) + { + loadedObject = _objectRepository->LoadObject(ori); + if (loadedObject == nullptr) + { + invalidEntries[invalidObjectCount++] = DuplicateObjectEntry(entry); + } + } + } + } + } + object_validity_result* result = Memory::Allocate(sizeof(object_validity_result)); + result->invalid_object_count = invalidObjectCount; + result->invalid_objects = invalidEntries; + return result; + } + bool GetRequiredObjects(const rct_object_entry * entries, const ObjectRepositoryItem * * requiredObjects, size_t * outNumRequiredObjects) @@ -519,14 +564,14 @@ private: return loadedObject; } - static void ReportMissingObject(const rct_object_entry * entry) + void ReportMissingObject(const rct_object_entry * entry) { utf8 objName[9] = { 0 }; Memory::Copy(objName, entry->name, 8); Console::Error::WriteLine("[%s] Object not found.", objName); } - static void ReportObjectLoadProblem(const rct_object_entry * entry) + void ReportObjectLoadProblem(const rct_object_entry * entry) { utf8 objName[9] = { 0 }; Memory::Copy(objName, entry->name, 8); diff --git a/src/openrct2/object/ObjectManager.h b/src/openrct2/object/ObjectManager.h index 2afb049203..d93f5bcda3 100644 --- a/src/openrct2/object/ObjectManager.h +++ b/src/openrct2/object/ObjectManager.h @@ -27,6 +27,7 @@ extern "C" { #endif #include "../object.h" + #include "../park_load_result_types.h" #ifdef __cplusplus } #endif @@ -41,9 +42,10 @@ interface IObjectManager { virtual ~IObjectManager() { } - virtual Object * GetLoadedObject(size_t index) abstract; - virtual Object * GetLoadedObject(const rct_object_entry * entry) abstract; - virtual uint8 GetLoadedObjectEntryIndex(const Object * object) abstract; + virtual Object * GetLoadedObject(size_t index) abstract; + virtual Object * GetLoadedObject(const rct_object_entry * entry) abstract; + virtual uint8 GetLoadedObjectEntryIndex(const Object * object) abstract; + virtual object_validity_result* GetInvalidObjects(const rct_object_entry * entries) abstract; virtual Object * LoadObject(const rct_object_entry * entry) abstract; virtual bool LoadObjects(const rct_object_entry * entries, size_t count) abstract; @@ -65,12 +67,12 @@ extern "C" { #endif -void * object_manager_get_loaded_object_by_index(size_t index); -void * object_manager_get_loaded_object(const rct_object_entry * entry); -uint8 object_manager_get_loaded_object_entry_index(const void * loadedObject); -void * object_manager_load_object(const rct_object_entry * entry); -void object_manager_unload_objects(const rct_object_entry * entries, size_t count); -void object_manager_unload_all_objects(); +void * object_manager_get_loaded_object_by_index(size_t index); +void * object_manager_get_loaded_object(const rct_object_entry * entry); +uint8 object_manager_get_loaded_object_entry_index(const void * loadedObject); +void * object_manager_load_object(const rct_object_entry * entry); +void object_manager_unload_objects(const rct_object_entry * entries, size_t count); +void object_manager_unload_all_objects(); #ifdef __cplusplus } diff --git a/src/openrct2/park_load_result_types.h b/src/openrct2/park_load_result_types.h new file mode 100644 index 0000000000..29e3f7c67f --- /dev/null +++ b/src/openrct2/park_load_result_types.h @@ -0,0 +1,33 @@ +#pragma region Copyright (c) 2014-2017 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 + +#ifndef _PARK_LOAD_RESULT_H_ +#define _PARK_LOAD_RESULT_H_ + +#include "object.h" + +typedef struct object_validity_result +{ + uint16 invalid_object_count; + rct_object_entry * * invalid_objects; + +} object_validity_result; + +typedef struct park_load_result { + uint8 error; + object_validity_result* object_validity; +} park_load_result; +#endif diff --git a/src/openrct2/platform/platform.h b/src/openrct2/platform/platform.h index dd174d9d6c..e0c82ff805 100644 --- a/src/openrct2/platform/platform.h +++ b/src/openrct2/platform/platform.h @@ -109,6 +109,7 @@ sint32 platform_enumerate_directories_begin(const utf8 *directory); bool platform_enumerate_directories_next(sint32 handle, utf8 *path); void platform_enumerate_directories_end(sint32 handle); void platform_init_window_icon(); +bool platform_place_string_on_clipboard(utf8* target); // Returns the bitmask of the GetLogicalDrives function for windows, 0 for other systems sint32 platform_get_drives(); diff --git a/src/openrct2/platform/windows.c b/src/openrct2/platform/windows.c index c3a7c88470..da62d39a70 100644 --- a/src/openrct2/platform/windows.c +++ b/src/openrct2/platform/windows.c @@ -804,7 +804,8 @@ datetime64 platform_get_datetime_now_utc() return utcNow; } -utf8* platform_get_username() { +utf8* platform_get_username() +{ static char username[UNLEN + 1]; DWORD usernameLength = UNLEN + 1; diff --git a/src/openrct2/rct1/S4Importer.cpp b/src/openrct2/rct1/S4Importer.cpp index 1d06277299..d4c77d03b5 100644 --- a/src/openrct2/rct1/S4Importer.cpp +++ b/src/openrct2/rct1/S4Importer.cpp @@ -125,16 +125,16 @@ private: uint8 _researchRideTypeUsed[128]; public: - void Load(const utf8 * path) override + park_load_result* Load(const utf8 * path) override { const utf8 * extension = Path::GetExtension(path); if (String::Equals(extension, ".sc4", true)) { - LoadScenario(path); + return LoadScenario(path); } else if (String::Equals(extension, ".sv4", true)) { - LoadSavedGame(path); + return LoadSavedGame(path); } else { @@ -142,22 +142,27 @@ public: } } - void LoadSavedGame(const utf8 * path) override + park_load_result* LoadSavedGame(const utf8 * path) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, false); + park_load_result* result = LoadFromStream(&fs, false); _s4Path = path; + return result; } - void LoadScenario(const utf8 * path) override + park_load_result* LoadScenario(const utf8 * path) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, true); + park_load_result* result = LoadFromStream(&fs, true); _s4Path = path; + return result; } - void LoadFromStream(IStream * stream, bool isScenario) override + park_load_result* LoadFromStream(IStream * stream, bool isScenario) override { + park_load_result* result = Memory::Allocate(sizeof(park_load_result)); + result->error = PARK_LOAD_ERROR_UNKNOWN; + size_t dataSize = stream->GetLength() - stream->GetPosition(); std::unique_ptr data = std::unique_ptr(stream->ReadArray(dataSize)); std::unique_ptr decodedData = std::unique_ptr(Memory::Allocate(sizeof(rct1_s4))); @@ -182,6 +187,8 @@ public: { throw Exception("Unable to decode park."); } + result->error = PARK_LOAD_ERROR_NONE; + return result; } void Import() override diff --git a/src/openrct2/rct2/S6Importer.cpp b/src/openrct2/rct2/S6Importer.cpp index e98599ad32..9ce3c36827 100644 --- a/src/openrct2/rct2/S6Importer.cpp +++ b/src/openrct2/rct2/S6Importer.cpp @@ -24,6 +24,7 @@ #include "../network/network.h" #include "../object/ObjectManager.h" #include "../object/ObjectRepository.h" +#include "../object/ObjectManager.h" #include "../ParkImporter.h" #include "../rct12/SawyerChunkReader.h" #include "../rct12/SawyerEncoding.h" @@ -80,16 +81,16 @@ public: Memory::Set(&_s6, 0, sizeof(_s6)); } - void Load(const utf8 * path) override + park_load_result* Load(const utf8 * path) override { const utf8 * extension = Path::GetExtension(path); if (String::Equals(extension, ".sc6", true)) { - LoadScenario(path); + return LoadScenario(path); } else if (String::Equals(extension, ".sv6", true)) { - LoadSavedGame(path); + return LoadSavedGame(path); } else { @@ -97,22 +98,29 @@ public: } } - void LoadSavedGame(const utf8 * path) override + park_load_result* LoadSavedGame(const utf8 * path) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, false); + park_load_result* result = LoadFromStream(&fs, false); _s6Path = path; + + return result; } - void LoadScenario(const utf8 * path) override + park_load_result* LoadScenario(const utf8 * path) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, true); + park_load_result* result = LoadFromStream(&fs, true); _s6Path = path; + + return result; } - void LoadFromStream(IStream * stream, bool isScenario) override + park_load_result* LoadFromStream(IStream * stream, bool isScenario) override { + park_load_result* result = Memory::Allocate(sizeof(park_load_result)); + result->error = PARK_LOAD_ERROR_UNKNOWN; + if (isScenario && !gConfigGeneral.allow_loading_with_incorrect_checksum && !SawyerEncoding::ValidateChecksum(stream)) { throw IOException("Invalid checksum."); @@ -166,6 +174,19 @@ public: chunkReader.ReadChunk(&_s6.map_elements, sizeof(_s6.map_elements)); chunkReader.ReadChunk(&_s6.next_free_map_element_pointer_index, 3048816); } + + object_validity_result* object_result = _objectManager->GetInvalidObjects(_s6.objects); + + result->object_validity = object_result; + if (object_result->invalid_object_count > 0) + { + result->error = PARK_LOAD_ERROR_BAD_OBJECTS; + } + else + { + result->error = PARK_LOAD_ERROR_NONE; + } + return result; } bool GetDetails(scenario_index_entry * dst) override @@ -418,23 +439,28 @@ IParkImporter * ParkImporter::CreateS6(IObjectRepository * objectRepository, IOb extern "C" { - bool game_load_sv6_path(const char * path) + park_load_result* game_load_sv6_path(const char * path) { - bool result = false; + park_load_result* result = {}; auto s6Importer = new S6Importer(GetObjectRepository(), GetObjectManager()); try { - s6Importer->LoadSavedGame(path); - s6Importer->Import(); + result = s6Importer->LoadSavedGame(path); - game_fix_save_vars(); - sprite_position_tween_reset(); - result = true; + // We mustn't import if there's something + // wrong with the park data + if (result->error == PARK_LOAD_ERROR_NONE) + { + s6Importer->Import(); + + game_fix_save_vars(); + sprite_position_tween_reset(); + } } catch (const ObjectLoadException &) { gErrorType = ERROR_TYPE_FILE_LOAD; - gErrorStringId = STR_GAME_SAVE_FAILED; + gErrorStringId = STR_FILE_CONTAINS_INVALID_DATA; } catch (const IOException &) { @@ -448,8 +474,11 @@ extern "C" } delete s6Importer; - gScreenAge = 0; - gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + if (result->error == PARK_LOAD_ERROR_NONE) + { + gScreenAge = 0; + gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + } return result; } @@ -458,18 +487,20 @@ extern "C" * rct2: 0x00676053 * scenario (ebx) */ - sint32 scenario_load(const char * path) + park_load_result *scenario_load(const char * path) { - bool result = false; + park_load_result* result = {}; auto s6Importer = new S6Importer(GetObjectRepository(), GetObjectManager()); try { - s6Importer->LoadScenario(path); - s6Importer->Import(); + result = s6Importer->LoadScenario(path); + if (result->error != PARK_LOAD_ERROR_NONE) + { + s6Importer->Import(); - game_fix_save_vars(); - sprite_position_tween_reset(); - result = true; + game_fix_save_vars(); + sprite_position_tween_reset(); + } } catch (const ObjectLoadException &) { @@ -487,9 +518,11 @@ extern "C" gErrorStringId = STR_FILE_CONTAINS_INVALID_DATA; } delete s6Importer; - - gScreenAge = 0; - gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + if (result->error != PARK_LOAD_ERROR_NONE) + { + gScreenAge = 0; + gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + } return result; } } diff --git a/src/openrct2/scenario/scenario.c b/src/openrct2/scenario/scenario.c index 939fec7261..20a4b8abc3 100644 --- a/src/openrct2/scenario/scenario.c +++ b/src/openrct2/scenario/scenario.c @@ -93,9 +93,19 @@ sint32 scenario_load_and_play_from_path(const char *path) window_close_construction_windows(); uint32 extension = get_file_extension_type(path); + park_load_result* result; if (extension == FILE_EXTENSION_SC6) { - if (!scenario_load(path)) + result = scenario_load(path); + if (result->error != PARK_LOAD_ERROR_NONE) + { + if (result->error == PARK_LOAD_ERROR_BAD_OBJECTS) + { + // The path needs to be duplicated as it's a const here + // which the window function doesn't like + window_object_load_error_open(strndup(path, strnlen(path, MAX_PATH)), result->object_validity); + } return 0; + } } else if (extension == FILE_EXTENSION_SC4) { if (!rct1_load_scenario(path)) diff --git a/src/openrct2/scenario/scenario.h b/src/openrct2/scenario/scenario.h index fa76197933..a2000c9bf9 100644 --- a/src/openrct2/scenario/scenario.h +++ b/src/openrct2/scenario/scenario.h @@ -21,6 +21,7 @@ #include "../management/finance.h" #include "../management/research.h" #include "../object.h" +#include "../park_load_result_types.h" #include "../rct12.h" #include "../rct2.h" #include "../rct2/addresses.h" @@ -389,7 +390,7 @@ extern uint32 gLastAutoSaveUpdate; extern const char *_scenarioFileName; -sint32 scenario_load(const char *path); +park_load_result *scenario_load(const char *path); sint32 scenario_load_and_play_from_path(const char *path); void scenario_begin(); void scenario_update(); diff --git a/src/openrct2/ui/DummyUiContext.cpp b/src/openrct2/ui/DummyUiContext.cpp index 906bf0f6aa..3b3dab1d67 100644 --- a/src/openrct2/ui/DummyUiContext.cpp +++ b/src/openrct2/ui/DummyUiContext.cpp @@ -79,6 +79,9 @@ namespace OpenRCT2 { namespace Ui // Misc bool ReadBMP(void * * outPixels, uint32 * outWidth, uint32 * outHeight, const std::string &path) override { return false; } + + // Clipboard + bool SetClipboardText(const utf8* target) override { return false; } }; IUiContext * CreateDummyUiContext() diff --git a/src/openrct2/ui/UiContext.h b/src/openrct2/ui/UiContext.h index 7ebf92ec35..49fe1ec225 100644 --- a/src/openrct2/ui/UiContext.h +++ b/src/openrct2/ui/UiContext.h @@ -136,6 +136,9 @@ namespace OpenRCT2 // HACK: This should either be implemented ourselves in libopenrct2 // or the mapgen height map code is moved to libopenrct2ui. virtual bool ReadBMP(void * * outPixels, uint32 * outWidth, uint32 * outHeight, const std::string &path) abstract; + + // Clipboard + virtual bool SetClipboardText(const utf8* target) abstract; }; IUiContext * CreateDummyUiContext(); diff --git a/src/openrct2/windows/object_load_error.c b/src/openrct2/windows/object_load_error.c new file mode 100644 index 0000000000..659c368cc7 --- /dev/null +++ b/src/openrct2/windows/object_load_error.c @@ -0,0 +1,328 @@ +#pragma region Copyright (c) 2014-2017 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 "../interface/widget.h" +#include "../interface/window.h" +#include "../localisation/localisation.h" +#include "../object/ObjectManager.h" +#include "../object.h" +#include "../platform/platform.h" +#include "../sprites.h" +#include "../util/util.h" + +enum WINDOW_OBJECT_LOAD_ERROR_WIDGET_IDX { + WIDX_BACKGROUND, + WIDX_TITLE, + WIDX_CLOSE, + WIDX_COLUMN_OBJECT_NAME, + WIDX_COLUMN_OBJECT_TYPE, + WIDX_SCROLL, + WIDX_COPY_CURRENT, + WIDX_COPY_ALL +}; + +#define WW 400 +#define WH 400 +#define LIST_ITEM_HEIGHT 10 + +rct_widget window_object_load_error_widgets[] = { + { WWT_FRAME, 0, 0, WW - 1, 0, WH - 1, STR_NONE, STR_NONE }, // Background + { WWT_CAPTION, 0, 1, WW - 2, 1, 14, STR_OBJECT_LOAD_ERROR_TITLE, STR_WINDOW_TITLE_TIP }, // Title bar + { WWT_CLOSEBOX, 0, WW - 13, WW - 3, 2, 13, STR_CLOSE_X, STR_CLOSE_WINDOW_TIP }, // Close button + { WWT_13, 0, 4, (WW - 5) / 3, 57, 68, STR_OBJECT_NAME, STR_NONE }, // 'Object name' header + { WWT_13, 0, (WW - 5) / 3 + 1, WW - 5 - 1, 57, 68, STR_OBJECT_TYPE, STR_NONE }, // 'Object type' header + { WWT_SCROLL, 0, 4, WW - 5, 68, WH - 40, SCROLL_VERTICAL, STR_NONE }, // Scrollable list area + { WWT_CLOSEBOX, 0, 20, 200, WW - 32, WW - 12, STR_COPY_SELECTED, STR_NONE }, // Copy selected btn + { WWT_CLOSEBOX, 0, 200, 370, WW - 32, WW - 12, STR_COPY_ALL, STR_NONE }, // Copy all btn + { WIDGETS_END }, +}; + +static rct_string_id get_object_type_string(const rct_object_entry *entry); +static void window_object_load_error_close(rct_window *w); +static void window_object_load_error_update(rct_window *w); +static void window_object_load_error_mouseup(rct_window *w, rct_widgetindex widgetIndex); +static void window_object_load_error_scrollgetsize(rct_window *w, sint32 scrollIndex, sint32 *width, sint32 *height); +static void window_object_load_error_scrollmouseover(rct_window *w, sint32 scrollIndex, sint32 x, sint32 y); +static void window_object_load_error_scrollmousedown(rct_window *w, sint32 scrollIndex, sint32 x, sint32 y); +static void window_object_load_error_paint(rct_window *w, rct_drawpixelinfo *dpi); +static void window_object_load_error_scrollpaint(rct_window *w, rct_drawpixelinfo *dpi, sint32 scrollIndex); + +static rct_window_event_list window_object_load_error_events = { + window_object_load_error_close, + window_object_load_error_mouseup, + NULL, + NULL, + NULL, + NULL, + window_object_load_error_update, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + window_object_load_error_scrollgetsize, + window_object_load_error_scrollmousedown, + NULL, + window_object_load_error_scrollmouseover, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + window_object_load_error_paint, + window_object_load_error_scrollpaint +}; + +rct_object_entry * * invalid_entries = NULL; +sint32 highlighted_index = -1; +utf8* file_path = NULL; + +/** +* Returns an rct_string_id that represents an rct_object_entry's type. +* +* Could possibly be moved out of the window file if other +* uses exist and a suitable location is found. +*/ +static rct_string_id get_object_type_string(const rct_object_entry *entry) +{ + rct_string_id result; + uint8 objectType = entry->flags & 0x0F; + switch (objectType) { + case OBJECT_TYPE_RIDE: + result = STR_OBJECT_SELECTION_RIDE_VEHICLES_ATTRACTIONS; + break; + case OBJECT_TYPE_SMALL_SCENERY: + result = STR_OBJECT_SELECTION_SMALL_SCENERY; + break; + case OBJECT_TYPE_LARGE_SCENERY: + result = STR_OBJECT_SELECTION_LARGE_SCENERY; + break; + case OBJECT_TYPE_WALLS: + result = STR_OBJECT_SELECTION_WALLS_FENCES; + break; + case OBJECT_TYPE_BANNERS: + result = STR_OBJECT_SELECTION_PATH_SIGNS; + break; + case OBJECT_TYPE_PATHS: + result = STR_OBJECT_SELECTION_FOOTPATHS; + break; + case OBJECT_TYPE_PATH_BITS: + result = STR_OBJECT_SELECTION_PATH_EXTRAS; + break; + case OBJECT_TYPE_SCENERY_SETS: + result = STR_OBJECT_SELECTION_SCENERY_GROUPS; + break; + case OBJECT_TYPE_PARK_ENTRANCE: + result = STR_OBJECT_SELECTION_PARK_ENTRANCE; + break; + case OBJECT_TYPE_WATER: + result = STR_OBJECT_SELECTION_WATER; + break; + case OBJECT_TYPE_SCENARIO_TEXT: + result = STR_OBJECT_SELECTION_SCENARIO_DESCRIPTION; + break; + default: + result = STR_UNKNOWN_OBJECT_TYPE; + } + return result; +} + +/** +* Returns a newline-separated string listing all object names. +* Used for placing all names on the clipboard. +*/ +static utf8* combine_object_names(rct_window *w) +{ + if (w->no_list_items > OBJECT_ENTRY_COUNT || w->no_list_items == 0) { + // Something's gone wrong, this shouldn't happen + // We don't want to allocate stupidly large amounts of memory + // for no reason, so bail + return NULL; + } + utf8* buffer; + + // No system has a newline over 2 characters + size_t line_sep_len = strnlen(PLATFORM_NEWLINE, 2); + size_t buffer_len = (w->no_list_items * (8 + line_sep_len)) + 1; + buffer = (utf8*)malloc(buffer_len); + buffer[0] = '\0'; + size_t cur_len = 0; + for (uint16 i = 0; i < w->no_list_items; i++) { + cur_len += (8 + line_sep_len); + assert(cur_len < buffer_len); + strncat(buffer, invalid_entries[i]->name, 8); + strncat(buffer, PLATFORM_NEWLINE, line_sep_len); + } + return buffer; +} + +rct_window *window_object_load_error_open(utf8* path, object_validity_result* result) +{ + rct_window* window; + + invalid_entries = result->invalid_objects; + + // Check if window is already open + window = window_bring_to_front_by_class(WC_OBJECT_LOAD_ERROR); + if (window == NULL) { + window = window_create_centred( + WW, + WH, + &window_object_load_error_events, + WC_OBJECT_LOAD_ERROR, + WF_STICK_TO_FRONT + ); + + window->widgets = window_object_load_error_widgets; + window->enabled_widgets = (1 << WIDX_CLOSE) | (1 << WIDX_COPY_CURRENT) | (1 << WIDX_COPY_ALL); + + window_init_scroll_widgets(window); + window->colours[0] = COLOUR_LIGHT_BLUE; + window->colours[1] = COLOUR_LIGHT_BLUE; + window->colours[2] = COLOUR_LIGHT_BLUE; + } + + // Refresh list items and path + window->no_list_items = result->invalid_object_count; + file_path = path; + + window_invalidate(window); + return window; +} + +static void window_object_load_error_close(rct_window *w) +{ + SafeFree(invalid_entries); +} + +static void window_object_load_error_update(rct_window *w) +{ + // Check if the mouse is hovering over the list + if (!widget_is_highlighted(w, WIDX_SCROLL)) { + highlighted_index = -1; + widget_invalidate(w, WIDX_SCROLL); + } +} + +static void window_object_load_error_mouseup(rct_window *w, rct_widgetindex widgetIndex) +{ + utf8* selected_name; + utf8* combined_list; + switch (widgetIndex) { + case WIDX_CLOSE: + window_close(w); + break; + case WIDX_COPY_CURRENT: + if (w->selected_list_item > -1) { + selected_name = strndup(invalid_entries[w->selected_list_item]->name, 8); + platform_place_string_on_clipboard(selected_name); + SafeFree(selected_name); + } + break; + case WIDX_COPY_ALL: + combined_list = combine_object_names(w); + platform_place_string_on_clipboard(combined_list); + SafeFree(combined_list); + break; + } +} + +static void window_object_load_error_scrollmouseover(rct_window *w, sint32 scrollIndex, sint32 x, sint32 y) +{ + // Highlight item that the cursor is over, or remove highlighting if none + sint32 selected_item; + selected_item = y / 10; + if (selected_item < 0 || selected_item >= w->no_list_items) + highlighted_index = -1; + else + highlighted_index = selected_item; + + widget_invalidate(w, WIDX_SCROLL); +} + +static void window_object_load_error_select_element_from_list(rct_window *w, sint32 index) +{ + if (index < 0 || index > w->no_list_items) { + w->selected_list_item = -1; + } + else { + w->selected_list_item = index; + } +} + +static void window_object_load_error_scrollmousedown(rct_window *w, sint32 scrollIndex, sint32 x, sint32 y) +{ + sint32 selected_item; + selected_item = y / 10; + window_object_load_error_select_element_from_list(w, selected_item); +} + +static void window_object_load_error_scrollgetsize(rct_window *w, sint32 scrollIndex, sint32 *width, sint32 *height) +{ + *height = w->no_list_items * 10; +} + +static void window_object_load_error_paint(rct_window *w, rct_drawpixelinfo *dpi) +{ + window_draw_widgets(w, dpi); + + // Draw explanatory message + set_format_arg(0, rct_string_id, STR_OBJECT_ERROR_WINDOW_EXPLANATION); + gfx_draw_string_left_wrapped(dpi, gCommonFormatArgs, w->x + 5, w->y + 18, WW-10, STR_BLACK_STRING, COLOUR_BLACK); + + // Draw file name + set_format_arg(0, rct_string_id, STR_OBJECT_ERROR_WINDOW_FILE); + set_format_arg(2, utf8*, file_path); + gfx_draw_string_left_clipped(dpi, STR_BLACK_STRING, gCommonFormatArgs, COLOUR_BLACK, w->x + 5, w->y + 43, WW-5); +} + +static void window_object_load_error_scrollpaint(rct_window *w, rct_drawpixelinfo *dpi, sint32 scrollIndex) +{ + + gfx_fill_rect(dpi, dpi->x, dpi->y, dpi->x + dpi->width - 1, dpi->y + dpi->height - 1, ColourMapA[w->colours[1]].mid_light); + const sint32 list_width = w->widgets[WIDX_SCROLL].right - w->widgets[WIDX_SCROLL].left; + + for (sint32 i = 0; i < w->no_list_items; i++) { + sint32 y = i * 10; + if (y > dpi->y + dpi->height) + break; + + if (y + 10 < dpi->y) + continue; + + // If hovering over item, change the color and fill the backdrop. + if (i == w->selected_list_item) // Currently selected element + gfx_fill_rect(dpi, 0, y, list_width, y + LIST_ITEM_HEIGHT - 1, ColourMapA[w->colours[1]].darker | 0x1000000); + else if (i == highlighted_index) // Hovering + gfx_fill_rect(dpi, 0, y, list_width, y + LIST_ITEM_HEIGHT - 1, ColourMapA[w->colours[1]].mid_dark | 0x1000000); + else if ((i & 1) != 0) // odd / even check + gfx_fill_rect(dpi, 0, y, list_width, y + LIST_ITEM_HEIGHT - 1, ColourMapA[w->colours[1]].lighter | 0x1000000); + + // Draw the actual object entry's name... + gfx_draw_string(dpi, strndup(invalid_entries[i]->name, 8), COLOUR_DARK_GREEN, 5, y); + + // ... and type + rct_string_id type = get_object_type_string(invalid_entries[i]); + gfx_draw_string_left(dpi, type, NULL, COLOUR_DARK_GREEN, (WW - 5) / 3 + 1, y); + } + + +}