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 b96519f8c0..124cd03fc6 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -718,4 +718,9 @@ extern "C" return nullptr; } } + + bool platform_place_string_on_clipboard(utf8* target) + { + return GetContext()->GetUiContext()->SetClipboardText(target); + } } diff --git a/src/openrct2/ParkImporter.cpp b/src/openrct2/ParkImporter.cpp index 6b20575930..3fe174a906 100644 --- a/src/openrct2/ParkImporter.cpp +++ b/src/openrct2/ParkImporter.cpp @@ -21,6 +21,65 @@ #include "object/ObjectRepository.h" #include "ParkImporter.h" +ParkLoadResult::ParkLoadResult(PARK_LOAD_ERROR error) + : Error(error) +{ +} + +ParkLoadResult::ParkLoadResult(PARK_LOAD_ERROR error, const std::vector &missingObjects) + : Error(error), + MissingObjects(missingObjects) +{ +} + +ParkLoadResult ParkLoadResult::CreateOK() +{ + return ParkLoadResult(PARK_LOAD_ERROR::PARK_LOAD_ERROR_OK); +} + +ParkLoadResult ParkLoadResult::CreateInvalidExtension() +{ + return ParkLoadResult(PARK_LOAD_ERROR::PARK_LOAD_ERROR_INVALID_EXTENSION); +} + +ParkLoadResult ParkLoadResult::CreateMissingObjects(const std::vector &missingObjects) +{ + return ParkLoadResult(PARK_LOAD_ERROR::PARK_LOAD_ERROR_MISSING_OBJECTS, missingObjects); +} + +ParkLoadResult ParkLoadResult::CreateUnknown() +{ + return ParkLoadResult(PARK_LOAD_ERROR::PARK_LOAD_ERROR_UNKNOWN); +} + +extern "C" +{ + PARK_LOAD_ERROR ParkLoadResult_GetError(const ParkLoadResult * t) + { + return t->Error; + } + + size_t ParkLoadResult_GetMissingObjectsCount(const ParkLoadResult * t) + { + return t->MissingObjects.size(); + } + + const rct_object_entry * ParkLoadResult_GetMissingObjects(const ParkLoadResult * t) + { + return t->MissingObjects.data(); + } + + void ParkLoadResult_Delete(ParkLoadResult * t) + { + delete t; + } + + ParkLoadResult * ParkLoadResult_CreateInvalidExtension() + { + return new ParkLoadResult(ParkLoadResult::CreateInvalidExtension()); + } +} + namespace ParkImporter { IParkImporter * Create(const std::string &hintPath) diff --git a/src/openrct2/ParkImporter.h b/src/openrct2/ParkImporter.h index bbce6a92b9..814cd9f45a 100644 --- a/src/openrct2/ParkImporter.h +++ b/src/openrct2/ParkImporter.h @@ -18,15 +18,49 @@ #include "common.h" +#ifdef __cplusplus +extern "C" +{ +#endif + #include "object.h" +#ifdef __cplusplus +} +#endif + +typedef enum PARK_LOAD_ERROR +{ + PARK_LOAD_ERROR_OK, + PARK_LOAD_ERROR_MISSING_OBJECTS, + PARK_LOAD_ERROR_INVALID_EXTENSION, + PARK_LOAD_ERROR_UNKNOWN = 255 +} PARK_LOAD_ERROR; + #ifdef __cplusplus #include +#include #include "scenario/ScenarioRepository.h" interface IObjectManager; interface IObjectRepository; interface IStream; +struct ParkLoadResult final +{ +public: + const PARK_LOAD_ERROR Error; + const std::vector MissingObjects; + + static ParkLoadResult CreateOK(); + static ParkLoadResult CreateInvalidExtension(); + static ParkLoadResult CreateMissingObjects(const std::vector &missingObjects); + static ParkLoadResult CreateUnknown(); + +private: + ParkLoadResult(PARK_LOAD_ERROR error); + ParkLoadResult(PARK_LOAD_ERROR error, const std::vector &missingObjects); +}; + /** * Interface to import scenarios and saved games. */ @@ -34,10 +68,12 @@ 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 ParkLoadResult Load(const utf8 * path) abstract; + virtual ParkLoadResult LoadSavedGame(const utf8 * path, bool skipObjectCheck = false) abstract; + virtual ParkLoadResult LoadScenario(const utf8 * path, bool skipObjectCheck = false) abstract; + virtual ParkLoadResult LoadFromStream(IStream * stream, bool isScenario, bool skipObjectCheck = false) abstract; + virtual void Import() abstract; virtual bool GetDetails(scenario_index_entry * dst) abstract; }; @@ -52,6 +88,10 @@ namespace ParkImporter bool ExtensionIsScenario(const std::string &extension); } +#else + +typedef struct ParkLoadResult ParkLoadResult; + #endif #ifdef __cplusplus @@ -61,6 +101,11 @@ extern "C" void park_importer_load_from_stream(void * stream, const utf8 * hintPath); bool park_importer_extension_is_scenario(const utf8 * extension); + PARK_LOAD_ERROR ParkLoadResult_GetError(const ParkLoadResult * t); + size_t ParkLoadResult_GetMissingObjectsCount(const ParkLoadResult * t); + const rct_object_entry * ParkLoadResult_GetMissingObjects(const ParkLoadResult * t); + void ParkLoadResult_Delete(ParkLoadResult * t); + ParkLoadResult * ParkLoadResult_CreateInvalidExtension(); #ifdef __cplusplus } #endif diff --git a/src/openrct2/editor.c b/src/openrct2/editor.c index 140f30aa4f..07c8c95d20 100644 --- a/src/openrct2/editor.c +++ b/src/openrct2/editor.c @@ -31,6 +31,7 @@ #include "platform/platform.h" #include "rct1.h" #include "ride/ride.h" +#include "ParkImporter.h" #include "scenario/scenario.h" #include "util/sawyercoding.h" #include "util/util.h" @@ -261,16 +262,18 @@ static sint32 editor_load_landscape_from_sc4(const char *path) */ static sint32 editor_read_s6(const char *path) { - bool loadResult = false; + ParkLoadResult * loadResult = NULL; 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 (ParkLoadResult_GetError(loadResult) != PARK_LOAD_ERROR_OK) { + ParkLoadResult_Delete(loadResult); return 0; } + ParkLoadResult_Delete(loadResult); editor_clear_map_for_editing(true); diff --git a/src/openrct2/game.c b/src/openrct2/game.c index 5e1ff8993a..9f7ecab429 100644 --- a/src/openrct2/game.c +++ b/src/openrct2/game.c @@ -33,6 +33,7 @@ #include "network/network.h" #include "object.h" #include "OpenRCT2.h" +#include "ParkImporter.h" #include "peep/peep.h" #include "peep/staff.h" #include "platform/platform.h" @@ -1094,18 +1095,21 @@ bool game_load_save(const utf8 *path) safe_strcpy(gScenarioSavePath, path, MAX_PATH); uint32 extension_type = get_file_extension_type(path); - bool result = false; + ParkLoadResult * result = NULL; + bool load_success = false; if (extension_type == FILE_EXTENSION_SV6) { result = game_load_sv6_path(path); - if (result) + load_success = (ParkLoadResult_GetError(result) == PARK_LOAD_ERROR_OK); + if (load_success) gFirstTimeSaving = false; } else if (extension_type == FILE_EXTENSION_SV4) { result = rct1_load_saved_game(path); - if (result) + load_success = (ParkLoadResult_GetError(result) == PARK_LOAD_ERROR_OK); + if (load_success) gFirstTimeSaving = true; } - if (result) { + if (load_success) { if (network_get_mode() == NETWORK_MODE_CLIENT) { network_close(); } @@ -1117,16 +1121,29 @@ bool game_load_save(const utf8 *path) // This ensures that the newly loaded save reflects the user's // 'show real names of guests' option, now that it's a global setting peep_update_names(gConfigGeneral.show_real_names_of_guests); - 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(); + handle_park_load_failure(result, path); return false; } } +void handle_park_load_failure(const ParkLoadResult * result, const utf8 * path) +{ + if (ParkLoadResult_GetError(result) == PARK_LOAD_ERROR_MISSING_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)), + ParkLoadResult_GetMissingObjectsCount(result), + ParkLoadResult_GetMissingObjects(result)); + } else if (ParkLoadResult_GetError(result) != PARK_LOAD_ERROR_OK) { + // 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(); + } +} + void game_load_init() { rct_window *mainWindow; @@ -1370,7 +1387,12 @@ bool game_load_save_or_scenario(const utf8 * path) return game_load_save(path); case FILE_EXTENSION_SC4: case FILE_EXTENSION_SC6: - return scenario_load_and_play_from_path(path); + { + ParkLoadResult * result = scenario_load_and_play_from_path(path); + bool success = (ParkLoadResult_GetError(result) == PARK_LOAD_ERROR_OK); + ParkLoadResult_Delete(result); + return success; + } } return false; } diff --git a/src/openrct2/game.h b/src/openrct2/game.h index 1f205694b7..60614715f3 100644 --- a/src/openrct2/game.h +++ b/src/openrct2/game.h @@ -170,7 +170,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); +ParkLoadResult * 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); @@ -179,6 +179,7 @@ bool game_is_paused(); bool game_is_not_paused(); void save_game(); void save_game_as(); +void handle_park_load_failure(const ParkLoadResult * result, const utf8 * path); void rct2_exit(); void rct2_exit_reason(rct_string_id title, rct_string_id body); void game_autosave(); diff --git a/src/openrct2/interface/window.h b/src/openrct2/interface/window.h index f72def0a68..f38e7bcfe3 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, size_t numMissingObjects, const rct_object_entry * missingObjects); 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..b1e0921cba 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,49 @@ 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; + } + + std::vector GetInvalidObjects(const rct_object_entry * entries) override + { + std::vector invalidEntries; + invalidEntries.reserve(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.push_back(*entry); + } + else + { + Object * loadedObject = nullptr; + loadedObject = ori->LoadedObject; + if (loadedObject == nullptr) + { + loadedObject = _objectRepository->LoadObject(ori); + if (loadedObject == nullptr) + { + invalidEntries.push_back(*entry); + } + delete loadedObject; + } + } + } + } + return invalidEntries; + } + bool GetRequiredObjects(const rct_object_entry * entries, const ObjectRepositoryItem * * requiredObjects, size_t * outNumRequiredObjects) @@ -519,14 +562,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..7aa837cea6 100644 --- a/src/openrct2/object/ObjectManager.h +++ b/src/openrct2/object/ObjectManager.h @@ -41,9 +41,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 std::vector 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 +66,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/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.h b/src/openrct2/rct1.h index 5d2505f283..3135da565b 100644 --- a/src/openrct2/rct1.h +++ b/src/openrct2/rct1.h @@ -34,6 +34,8 @@ #define RCT1_MAX_RIDES_IN_PARK 128 #define RCT1_RESEARCH_FLAGS_SEPARATOR 0xFF +typedef struct ParkLoadResult ParkLoadResult; + #pragma pack(push, 1) typedef struct rct1_entrance { uint16 x; @@ -1209,8 +1211,8 @@ extern const uint8 gRideCategories[RIDE_TYPE_COUNT]; sint32 vehicle_preference_compare(uint8 rideType, const char * a, const char * b); bool rideTypeShouldLoseSeparateFlag(const rct_ride_entry *rideEntry); -bool rct1_load_saved_game(const char *path); -bool rct1_load_scenario(const char *path); +ParkLoadResult * rct1_load_saved_game(const char *path); +ParkLoadResult * rct1_load_scenario(const char *path); colour_t rct1_get_colour(colour_t colour); diff --git a/src/openrct2/rct1/S4Importer.cpp b/src/openrct2/rct1/S4Importer.cpp index 1d06277299..b7a99d4c01 100644 --- a/src/openrct2/rct1/S4Importer.cpp +++ b/src/openrct2/rct1/S4Importer.cpp @@ -26,7 +26,9 @@ #include "../core/Path.hpp" #include "../core/String.hpp" #include "../core/Util.hpp" +#include "../object/Object.h" #include "../object/ObjectManager.h" +#include "../object/ObjectRepository.h" #include "../ParkImporter.h" #include "../scenario/ScenarioSources.h" #include "Tables.h" @@ -125,16 +127,16 @@ private: uint8 _researchRideTypeUsed[128]; public: - void Load(const utf8 * path) override + ParkLoadResult 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,21 +144,23 @@ public: } } - void LoadSavedGame(const utf8 * path) override + ParkLoadResult LoadSavedGame(const utf8 * path, bool skipObjectCheck = false) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, false); + auto result = LoadFromStream(&fs, false, skipObjectCheck); _s4Path = path; + return result; } - void LoadScenario(const utf8 * path) override + ParkLoadResult LoadScenario(const utf8 * path, bool skipObjectCheck = false) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, true); + auto result = LoadFromStream(&fs, true, skipObjectCheck); _s4Path = path; + return result; } - void LoadFromStream(IStream * stream, bool isScenario) override + ParkLoadResult LoadFromStream(IStream * stream, bool isScenario, bool skipObjectCheck = false) override { size_t dataSize = stream->GetLength() - stream->GetPosition(); std::unique_ptr data = std::unique_ptr(stream->ReadArray(dataSize)); @@ -177,11 +181,24 @@ public: { Memory::Copy(&_s4, decodedData.get(), sizeof(rct1_s4)); _s4Path = ""; + + if (!skipObjectCheck) + { + InitialiseEntryMaps(); + CreateAvailableObjectMappings(); + + auto missingObjects = GetInvalidObjects(); + if (missingObjects.size() > 0) + { + return ParkLoadResult::CreateMissingObjects(missingObjects); + } + } } else { throw Exception("Unable to decode park."); } + return ParkLoadResult::CreateOK(); } void Import() override @@ -302,6 +319,7 @@ private: Memory::Set(_pathAdditionTypeToEntryMap, 255, sizeof(_pathAdditionTypeToEntryMap)); Memory::Set(_sceneryThemeTypeToEntryMap, 255, sizeof(_sceneryThemeTypeToEntryMap)); + InitialiseEntryMaps(); uint16 mapSize = _s4.map_size == 0 ? 128 : _s4.map_size; // Do map initialisation, same kind of stuff done when loading scenario editor @@ -312,6 +330,18 @@ private: gS6Info.category = SCENARIO_CATEGORY_OTHER; } + void InitialiseEntryMaps() + { + Memory::Set(_rideTypeToRideEntryMap, 255, sizeof(_rideTypeToRideEntryMap)); + Memory::Set(_vehicleTypeToRideEntryMap, 255, sizeof(_vehicleTypeToRideEntryMap)); + Memory::Set(_smallSceneryTypeToEntryMap, 255, sizeof(_smallSceneryTypeToEntryMap)); + Memory::Set(_largeSceneryTypeToEntryMap, 255, sizeof(_largeSceneryTypeToEntryMap)); + Memory::Set(_wallTypeToEntryMap, 255, sizeof(_wallTypeToEntryMap)); + Memory::Set(_pathTypeToEntryMap, 255, sizeof(_pathTypeToEntryMap)); + Memory::Set(_pathAdditionTypeToEntryMap, 255, sizeof(_pathAdditionTypeToEntryMap)); + Memory::Set(_sceneryThemeTypeToEntryMap, 255, sizeof(_sceneryThemeTypeToEntryMap)); + } + /** * Scans the map and research list for all the object types used and builds lists and * lookup tables for converting from hard coded RCT1 object types to dynamic object entries. @@ -1718,6 +1748,59 @@ private: } } + std::vector GetInvalidObjects() + { + std::vector missingObjects; + GetInvalidObjects(OBJECT_TYPE_RIDE, _rideEntries.GetEntries(), missingObjects); + GetInvalidObjects(OBJECT_TYPE_SMALL_SCENERY, _smallSceneryEntries.GetEntries(), missingObjects); + GetInvalidObjects(OBJECT_TYPE_LARGE_SCENERY, _largeSceneryEntries.GetEntries(), missingObjects); + GetInvalidObjects(OBJECT_TYPE_WALLS, _wallEntries.GetEntries(), missingObjects); + GetInvalidObjects(OBJECT_TYPE_PATHS, _pathEntries.GetEntries(), missingObjects); + GetInvalidObjects(OBJECT_TYPE_PATH_BITS, _pathAdditionEntries.GetEntries(), missingObjects); + GetInvalidObjects(OBJECT_TYPE_SCENERY_SETS, _sceneryGroupEntries.GetEntries(), missingObjects); + GetInvalidObjects(OBJECT_TYPE_BANNERS, std::vector({ + "BN1 ", + "BN2 ", + "BN3 ", + "BN4 ", + "BN5 ", + "BN6 ", + "BN7 ", + "BN8 ", + "BN9 " + }), missingObjects); + GetInvalidObjects(OBJECT_TYPE_PARK_ENTRANCE, std::vector({ "PKENT1 " }), missingObjects); + GetInvalidObjects(OBJECT_TYPE_WATER, _waterEntry.GetEntries(), missingObjects); + return missingObjects; + } + + void GetInvalidObjects(uint8 objectType, const std::vector &entries, std::vector &missingObjects) + { + IObjectRepository * objectRepository = GetObjectRepository(); + for (const char * objectName : entries) + { + rct_object_entry entry; + entry.flags = 0x00008000 + objectType; + Memory::Copy(entry.name, objectName, 8); + entry.checksum = 0; + + const ObjectRepositoryItem * ori = objectRepository->FindObject(&entry); + if (ori == nullptr) + { + missingObjects.push_back(entry); + } + else + { + Object * object = objectRepository->LoadObject(ori); + if (object == nullptr && objectType != OBJECT_TYPE_SCENERY_SETS) + { + missingObjects.push_back(entry); + } + delete object; + } + } + } + void ImportMapElements() { Memory::Copy(gMapElements, _s4.map_elements, RCT1_MAX_MAP_ELEMENTS * sizeof(rct_map_element)); @@ -2561,39 +2644,43 @@ IParkImporter * ParkImporter::CreateS4() ///////////////////////////////////////// extern "C" { - bool rct1_load_saved_game(const utf8 * path) + ParkLoadResult * rct1_load_saved_game(const utf8 * path) { - bool result; - - auto s4Importer = new S4Importer(); + ParkLoadResult * result = nullptr; + auto s4Importer = std::make_unique(); try { - s4Importer->LoadSavedGame(path); - s4Importer->Import(); - result = true; - } catch (const Exception &) - { - result = false; + result = new ParkLoadResult(s4Importer->LoadSavedGame(path)); + if (result->Error == PARK_LOAD_ERROR_OK) + { + s4Importer->Import(); + } + } + catch (const Exception &) + { + delete result; + result = new ParkLoadResult(ParkLoadResult::CreateUnknown()); } - delete s4Importer; return result; } - bool rct1_load_scenario(const utf8 * path) + ParkLoadResult * rct1_load_scenario(const utf8 * path) { - bool result; - - auto s4Importer = new S4Importer(); + ParkLoadResult * result = nullptr; + auto s4Importer = std::make_unique(); try { - s4Importer->LoadScenario(path); - s4Importer->Import(); - result = true; - } catch (const Exception &) - { - result = false; + result = new ParkLoadResult(s4Importer->LoadSavedGame(path)); + if (result->Error == PARK_LOAD_ERROR_OK) + { + s4Importer->Import(); + } + } + catch (const Exception &) + { + delete result; + result = new ParkLoadResult(ParkLoadResult::CreateUnknown()); } - delete s4Importer; return result; } diff --git a/src/openrct2/rct2.c b/src/openrct2/rct2.c index 5c48cb7962..7179ef7dfb 100644 --- a/src/openrct2/rct2.c +++ b/src/openrct2/rct2.c @@ -38,6 +38,7 @@ #include "object.h" #include "object/ObjectManager.h" #include "OpenRCT2.h" +#include "ParkImporter.h" #include "peep/staff.h" #include "platform/platform.h" #include "rct1.h" @@ -339,8 +340,14 @@ bool rct2_open_file(const char *path) } } else if (_stricmp(extension, "sc6") == 0) { // TODO scenario install - if (scenario_load_and_play_from_path(path)) { + ParkLoadResult * result = scenario_load_and_play_from_path(path); + if (ParkLoadResult_GetError(result) == PARK_LOAD_ERROR_OK) { + ParkLoadResult_Delete(result); return true; + } else { + handle_park_load_failure(result, path); + ParkLoadResult_Delete(result); + return false; } } else if (_stricmp(extension, "td6") == 0 || _stricmp(extension, "td4") == 0) { // TODO track design install diff --git a/src/openrct2/rct2/S6Importer.cpp b/src/openrct2/rct2/S6Importer.cpp index e98599ad32..53ac9a2971 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 + ParkLoadResult 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,21 +98,23 @@ public: } } - void LoadSavedGame(const utf8 * path) override + ParkLoadResult LoadSavedGame(const utf8 * path, bool skipObjectCheck = false) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, false); + auto result = LoadFromStream(&fs, false, skipObjectCheck); _s6Path = path; + return result; } - void LoadScenario(const utf8 * path) override + ParkLoadResult LoadScenario(const utf8 * path, bool skipObjectCheck = false) override { auto fs = FileStream(path, FILE_MODE_OPEN); - LoadFromStream(&fs, true); + auto result = LoadFromStream(&fs, true, skipObjectCheck); _s6Path = path; + return result; } - void LoadFromStream(IStream * stream, bool isScenario) override + ParkLoadResult LoadFromStream(IStream * stream, bool isScenario, bool skipObjectCheck = false) override { if (isScenario && !gConfigGeneral.allow_loading_with_incorrect_checksum && !SawyerEncoding::ValidateChecksum(stream)) { @@ -166,6 +169,13 @@ public: chunkReader.ReadChunk(&_s6.map_elements, sizeof(_s6.map_elements)); chunkReader.ReadChunk(&_s6.next_free_map_element_pointer_index, 3048816); } + + auto missingObjects = _objectManager->GetInvalidObjects(_s6.objects); + if (missingObjects.size() > 0) + { + return ParkLoadResult::CreateMissingObjects(missingObjects); + } + return ParkLoadResult::CreateOK(); } bool GetDetails(scenario_index_entry * dst) override @@ -418,23 +428,28 @@ IParkImporter * ParkImporter::CreateS6(IObjectRepository * objectRepository, IOb extern "C" { - bool game_load_sv6_path(const char * path) + ParkLoadResult * game_load_sv6_path(const char * path) { - bool result = false; + ParkLoadResult * result = nullptr; auto s6Importer = new S6Importer(GetObjectRepository(), GetObjectManager()); try { - s6Importer->LoadSavedGame(path); - s6Importer->Import(); + result = new ParkLoadResult(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_OK) + { + 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 +463,15 @@ extern "C" } delete s6Importer; - gScreenAge = 0; - gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + if (result == nullptr) + { + result = new ParkLoadResult(ParkLoadResult::CreateUnknown()); + } + if (result->Error == PARK_LOAD_ERROR_OK) + { + gScreenAge = 0; + gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + } return result; } @@ -458,18 +480,20 @@ extern "C" * rct2: 0x00676053 * scenario (ebx) */ - sint32 scenario_load(const char * path) + ParkLoadResult * scenario_load(const char * path) { - bool result = false; + ParkLoadResult * result = nullptr; auto s6Importer = new S6Importer(GetObjectRepository(), GetObjectManager()); try { - s6Importer->LoadScenario(path); - s6Importer->Import(); + result = new ParkLoadResult(s6Importer->LoadScenario(path)); + if (result->Error == PARK_LOAD_ERROR_OK) + { + s6Importer->Import(); - game_fix_save_vars(); - sprite_position_tween_reset(); - result = true; + game_fix_save_vars(); + sprite_position_tween_reset(); + } } catch (const ObjectLoadException &) { @@ -488,8 +512,15 @@ extern "C" } delete s6Importer; - gScreenAge = 0; - gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + if (result == nullptr) + { + result = new ParkLoadResult(ParkLoadResult::CreateUnknown()); + } + if (result->Error != PARK_LOAD_ERROR_OK) + { + gScreenAge = 0; + gLastAutoSaveUpdate = AUTOSAVE_PAUSE; + } return result; } } diff --git a/src/openrct2/scenario/ScenarioRepository.cpp b/src/openrct2/scenario/ScenarioRepository.cpp index b0d89666e6..d732fbb413 100644 --- a/src/openrct2/scenario/ScenarioRepository.cpp +++ b/src/openrct2/scenario/ScenarioRepository.cpp @@ -322,7 +322,7 @@ private: try { auto s4Importer = std::unique_ptr(ParkImporter::CreateS4()); - s4Importer->LoadScenario(path.c_str()); + s4Importer->LoadScenario(path.c_str(), true); if (s4Importer->GetDetails(entry)) { String::Set(entry->path, sizeof(entry->path), path.c_str()); diff --git a/src/openrct2/scenario/scenario.c b/src/openrct2/scenario/scenario.c index 939fec7261..2e1a7d6277 100644 --- a/src/openrct2/scenario/scenario.c +++ b/src/openrct2/scenario/scenario.c @@ -30,6 +30,7 @@ #include "../object.h" #include "../object_list.h" #include "../OpenRCT2.h" +#include "../ParkImporter.h" #include "../peep/staff.h" #include "../platform/platform.h" #include "../rct1.h" @@ -88,21 +89,25 @@ money32 gScenarioCompanyValueRecord; static sint32 scenario_create_ducks(); static void scenario_objective_check(); -sint32 scenario_load_and_play_from_path(const char *path) +ParkLoadResult * scenario_load_and_play_from_path(const char * path) { window_close_construction_windows(); uint32 extension = get_file_extension_type(path); + ParkLoadResult * result = NULL; if (extension == FILE_EXTENSION_SC6) { - if (!scenario_load(path)) - return 0; - } - else if (extension == FILE_EXTENSION_SC4) { - if (!rct1_load_scenario(path)) - return 0; - } - else { - return 0; + result = scenario_load(path); + if (ParkLoadResult_GetError(result) != PARK_LOAD_ERROR_OK) { + return result; + } + } else if (extension == FILE_EXTENSION_SC4) { + result = rct1_load_scenario(path); + if (ParkLoadResult_GetError(result) != PARK_LOAD_ERROR_OK) { + return result; + } + } else { + result = ParkLoadResult_CreateInvalidExtension(); + return result; } reset_sprite_spatial_index(); @@ -131,8 +136,7 @@ sint32 scenario_load_and_play_from_path(const char *path) // This ensures that the newly loaded scenario reflects the user's // 'show real names of guests' option, now that it's a global setting peep_update_names(gConfigGeneral.show_real_names_of_guests); - - return 1; + return result; } void scenario_begin() diff --git a/src/openrct2/scenario/scenario.h b/src/openrct2/scenario/scenario.h index fa76197933..f592b001e6 100644 --- a/src/openrct2/scenario/scenario.h +++ b/src/openrct2/scenario/scenario.h @@ -31,6 +31,8 @@ #include "../world/map_animation.h" #include "../world/sprite.h" +typedef struct ParkLoadResult ParkLoadResult; + #pragma pack(push, 1) /** * SV6/SC6 header chunk @@ -389,8 +391,8 @@ extern uint32 gLastAutoSaveUpdate; extern const char *_scenarioFileName; -sint32 scenario_load(const char *path); -sint32 scenario_load_and_play_from_path(const char *path); +ParkLoadResult * scenario_load(const char *path); +ParkLoadResult * 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/util/util.c b/src/openrct2/util/util.c index 9955cc9181..90a7088c8a 100644 --- a/src/openrct2/util/util.c +++ b/src/openrct2/util/util.c @@ -18,8 +18,10 @@ #include #include "../common.h" #include "../core/Guard.hpp" +#include "../interface/window.h" #include "../localisation/localisation.h" #include "../platform/platform.h" +#include "../title/TitleScreen.h" #include "util.h" #include "zlib.h" diff --git a/src/openrct2/windows/object_load_error.c b/src/openrct2/windows/object_load_error.c new file mode 100644 index 0000000000..64de98ac6e --- /dev/null +++ b/src/openrct2/windows/object_load_error.c @@ -0,0 +1,324 @@ +#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" + +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, size_t numMissingObjects, const rct_object_entry * missingObjects) +{ + size_t missingObjectsSize = numMissingObjects * sizeof(rct_object_entry); + invalid_entries = malloc(missingObjectsSize); + memcpy(invalid_entries, missingObjects, missingObjectsSize); + + // Check if window is already open + rct_window * 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 = (uint16)numMissingObjects; + 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); + } +} diff --git a/src/openrct2/windows/server_start.c b/src/openrct2/windows/server_start.c index 5f17dcc2eb..d2f4bcc4c8 100644 --- a/src/openrct2/windows/server_start.c +++ b/src/openrct2/windows/server_start.c @@ -21,6 +21,8 @@ #include "../interface/window.h" #include "../localisation/localisation.h" #include "../network/network.h" +#include "../ParkImporter.h" +#include "../platform/platform.h" #include "../sprites.h" #include "../title/TitleScreen.h" #include "../util/util.h" @@ -165,11 +167,13 @@ static void window_server_start_close(rct_window *w) static void window_server_start_scenarioselect_callback(const utf8 *path) { network_set_password(_password); - if (scenario_load_and_play_from_path(path)) { + ParkLoadResult * result = scenario_load_and_play_from_path(path); + if (ParkLoadResult_GetError(result) == PARK_LOAD_ERROR_OK) { network_begin_server(gConfigNetwork.default_port, gConfigNetwork.listen_address); } else { - title_load(); + handle_park_load_failure(result, path); } + ParkLoadResult_Delete(result); } static void window_server_start_loadsave_callback(sint32 result, const utf8 * path) diff --git a/src/openrct2/windows/title_menu.c b/src/openrct2/windows/title_menu.c index 2b74ec9dac..b5b0c52308 100644 --- a/src/openrct2/windows/title_menu.c +++ b/src/openrct2/windows/title_menu.c @@ -22,8 +22,10 @@ #include "../interface/widget.h" #include "../interface/window.h" #include "../localisation/localisation.h" +#include "../ParkImporter.h" #include "../sprites.h" #include "../title/TitleScreen.h" +#include "../util/util.h" #include "dropdown.h" enum { @@ -128,9 +130,9 @@ void window_title_menu_open() static void window_title_menu_scenarioselect_callback(const utf8 *path) { - if (!scenario_load_and_play_from_path(path)) { - title_load(); - } + ParkLoadResult * result = scenario_load_and_play_from_path(path); + handle_park_load_failure(result, path); + ParkLoadResult_Delete(result); } static void window_title_menu_mouseup(rct_window *w, rct_widgetindex widgetIndex) diff --git a/src/openrct2/windows/top_toolbar.c b/src/openrct2/windows/top_toolbar.c index bc4ee90583..abea8d1be7 100644 --- a/src/openrct2/windows/top_toolbar.c +++ b/src/openrct2/windows/top_toolbar.c @@ -30,6 +30,7 @@ #include "../localisation/localisation.h" #include "../network/network.h" #include "../network/twitch.h" +#include "../ParkImporter.h" #include "../peep/staff.h" #include "../scenario/scenario.h" #include "../sprites.h" @@ -519,9 +520,9 @@ static void window_top_toolbar_mousedown(rct_widgetindex widgetIndex, rct_window static void window_top_toolbar_scenarioselect_callback(const utf8 *path) { - if (!scenario_load_and_play_from_path(path)) { - title_load(); - } + ParkLoadResult * result = scenario_load_and_play_from_path(path); + handle_park_load_failure(result, path); + ParkLoadResult_Delete(result); } /**