diff --git a/projects/Core/CMakeLists.txt b/projects/Core/CMakeLists.txt index 46924a8c9ec..45b5738a317 100644 --- a/projects/Core/CMakeLists.txt +++ b/projects/Core/CMakeLists.txt @@ -34,8 +34,9 @@ set( "api/messages/text_style.cpp" "api/moon_animator_events.cpp" "api/scenes/create_objects.cpp" - "api/scenes/scene_load.cpp" "api/scenes/polygon.cpp" + "api/scenes/scene_load.cpp" + "api/system/save_files.cpp" "api/audio.cpp" "api/screen_position.cpp" "api/uber_states/uber_state.cpp" @@ -108,6 +109,7 @@ set( "api/scenes/scene_load.h" "api/scenes/polygon.h" "api/system/message_provider.h" + "api/system/save_files.h" "api/audio.h" "api/screen_position.h" "api/uber_states/uber_state.h" diff --git a/projects/Core/api/system/save_files.cpp b/projects/Core/api/system/save_files.cpp new file mode 100644 index 00000000000..e69b9f747bf --- /dev/null +++ b/projects/Core/api/system/save_files.cpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include +#include + +using namespace app::classes; + +namespace core::api::save_files { + app::Byte__Array* get_byte_array(int slot_index, int backup_index) { + if (Grdk::Wrapper::get_InitializedOk()) { // Handle GRDK (Xbox Account) saves + return Grdk::Wrapper::Load_1(slot_index, backup_index); + } + + auto save_info = SaveGameController::GetSaveFileInfo(game::save_controller(), slot_index, backup_index); + + return System::IO::File::ReadAllBytes( + backup_index >= 0 + ? save_info->fields.m_FullBackupSaveFilePath + : save_info->fields.m_FullSaveFilePath + ); + } + + std::vector get_bytes(int slot_index, int backup_index) { + return utils::ByteStream(get_byte_array(slot_index, backup_index)).peek_to_end(); + } +} + diff --git a/projects/Core/api/system/save_files.h b/projects/Core/api/system/save_files.h new file mode 100644 index 00000000000..29d633bb691 --- /dev/null +++ b/projects/Core/api/system/save_files.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace core::api::save_files { + /** + * Get the on-disk save data for a save file. + * @param slot_index Save slot index + * @param backup_index Backup slot index. Set this to -1 to request the current game save file. + * @return Pointer to an il2cpp array of System.Byte. + */ + CORE_DLLEXPORT app::Byte__Array* get_byte_array(int slot_index, int backup_index = -1); + + /** + * Get the on-disk save data for a save file. + * @param slot_index Save slot index + * @param backup_index Backup slot index. Set this to -1 to request the current game save file. + * @return Byte vector containing the on-disk save data + */ + CORE_DLLEXPORT std::vector get_bytes(int slot_index, int backup_index = -1); +} diff --git a/projects/Core/api/uber_states/uber_state.cpp b/projects/Core/api/uber_states/uber_state.cpp index d6f7014ed46..57ab090cbff 100644 --- a/projects/Core/api/uber_states/uber_state.cpp +++ b/projects/Core/api/uber_states/uber_state.cpp @@ -277,12 +277,12 @@ namespace core::api::uber_states { if (settings::dev_mode()) { if (prev != value) { const auto text = std::format("uber state ({}|{}) set to {} from {}", static_cast(m_group), m_state, value, prev); - info("uber_state", text); + debug("uber_state", text); } else if (has_volatile_value()) { const auto text = std::format( "uber state ({}|{}) set to {} because it had a volatile value set", static_cast(m_group), m_state, value ); - info("uber_state", text); + debug("uber_state", text); } } diff --git a/projects/Core/messages/message_controller.cpp b/projects/Core/messages/message_controller.cpp index 7bc346cdafc..52bd7d4bbc3 100644 --- a/projects/Core/messages/message_controller.cpp +++ b/projects/Core/messages/message_controller.cpp @@ -218,6 +218,10 @@ namespace core::messages { } } + void MessageController::clear_recent_messages() { + m_recent_messages.clear(); + } + void MessageController::clear_central() { m_central_display.clear(); m_recent_display.clear(); diff --git a/projects/Core/messages/message_controller.h b/projects/Core/messages/message_controller.h index dd3725b97ce..97f6a15c038 100644 --- a/projects/Core/messages/message_controller.h +++ b/projects/Core/messages/message_controller.h @@ -26,6 +26,7 @@ namespace core::messages { // Handles showing / hiding the given messagebox based on info provided. message_handle_ptr_t queue(std::shared_ptr message, IndependentMessageInfo info); message_handle_ptr_t queue_central(MessageInfo info, bool add_to_recent = false); + void clear_recent_messages(); void show_recent_messages(); void update(float delta_time); void clear_central(); diff --git a/projects/Core/property/reactivity.h b/projects/Core/property/reactivity.h index 62fb014c84e..821f0488a2d 100644 --- a/projects/Core/property/reactivity.h +++ b/projects/Core/property/reactivity.h @@ -8,6 +8,7 @@ #include +#include #include #include diff --git a/projects/Core/save_meta/save_meta.cpp b/projects/Core/save_meta/save_meta.cpp index 2d76a999360..96d16bfcabc 100644 --- a/projects/Core/save_meta/save_meta.cpp +++ b/projects/Core/save_meta/save_meta.cpp @@ -1,12 +1,13 @@ #include +#include #include #include #include +#include #include #include #include #include -#include #include #include #include @@ -268,18 +269,11 @@ namespace core::save_meta { // ...and then load SaveMeta with the ThroughDeathsAndQTMsAndBackups persistence // level from the backup save file if we're on a new/different save slot if (current_save_guid != previous_save_guid) { - app::Byte__Array* bytes; - - if (Grdk::Wrapper::get_InitializedOk()) { // Handle GRDK (Xbox Account) saves - bytes = Grdk::Wrapper::Load_1(save_slot_index, backup_slot); - } else { - auto save_info = SaveGameController::GetSaveFileInfo(this_ptr, save_slot_index, backup_slot); - auto path = save_info->fields.m_FullBackupSaveFilePath; - auto path_str = il2cpp::convert_csstring(path); - bytes = System::IO::File::ReadAllBytes(path); - } - - read_save_meta_from_byte_array(bytes, true, SaveMetaSlotPersistence::ThroughDeathsAndQTMsAndBackups); + read_save_meta_from_byte_array( + api::save_files::get_byte_array(save_slot_index, backup_slot), + true, + SaveMetaSlotPersistence::ThroughDeathsAndQTMsAndBackups + ); } return return_value; diff --git a/projects/Core/utils/byte_stream.h b/projects/Core/utils/byte_stream.h index f52bd1d8d85..1caec3835fe 100644 --- a/projects/Core/utils/byte_stream.h +++ b/projects/Core/utils/byte_stream.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/projects/Modloader/CMakeLists.txt b/projects/Modloader/CMakeLists.txt index d3d994af585..dd5fe8332cd 100644 --- a/projects/Modloader/CMakeLists.txt +++ b/projects/Modloader/CMakeLists.txt @@ -17,6 +17,7 @@ set( "windows_api/modloader.cpp" "windows_api/sleep.cpp" "app/il2cpp_init.cpp" + "console_logging_handler.cpp" "file_logging_handler.cpp" "modloader.cpp" "il2cpp_helpers.cpp" @@ -35,6 +36,7 @@ set( "windows_api/memory.h" "windows_api/sleep.h" "windows_api/windows.h" + "console_logging_handler.h" "file_logging_handler.h" "modloader.h" "constants.h" diff --git a/projects/Modloader/console_logging_handler.cpp b/projects/Modloader/console_logging_handler.cpp new file mode 100644 index 00000000000..435c8b4ca9d --- /dev/null +++ b/projects/Modloader/console_logging_handler.cpp @@ -0,0 +1,26 @@ +#include + +#include "windows_api/console.h" + +namespace modloader { + std::string get_message_type_prefix(MessageType type) { + switch (type) { + case MessageType::Error: + return "\033[91mERROR\033[0m"; + case MessageType::Warning: + return "\033[93mWARN \033[0m"; + case MessageType::Info: + return "\033[92mINFO \033[0m"; + case MessageType::Debug: + return "\033[97mDEBUG\033[0m"; + } + + return "UNKWN"; + } + + void ConsoleLoggingHandler::write(MessageType type, std::string const& group, std::string const& message) { + modloader::win::console::console_send( + std::format("{} \033[95m{}\033[0m: {}", get_message_type_prefix(type), group, message) + ); + } +} diff --git a/projects/Modloader/console_logging_handler.h b/projects/Modloader/console_logging_handler.h new file mode 100644 index 00000000000..5687699ffad --- /dev/null +++ b/projects/Modloader/console_logging_handler.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include +#include +#include + +namespace modloader { + class ConsoleLoggingHandler final : public ILoggingHandler { + public: + explicit ConsoleLoggingHandler() {}; + + void write(MessageType type, std::string const& group, std::string const& message) override; + }; +} diff --git a/projects/Modloader/il2cpp_helpers.cpp b/projects/Modloader/il2cpp_helpers.cpp index 1b90f2ac4c6..832e9665f56 100644 --- a/projects/Modloader/il2cpp_helpers.cpp +++ b/projects/Modloader/il2cpp_helpers.cpp @@ -16,11 +16,10 @@ #include #include #include +#include #include -#include -#include #include #include @@ -930,6 +929,10 @@ namespace il2cpp { std::string get_class_namespace(Il2CppClass* klass) { return {il2cpp_class_get_namespace(klass)}; } + void attach_thread() { + il2cpp_thread_attach(il2cpp_domain_get()); + } + /** * Converts a C# string to std::string by truncating characters. * May lose special characters. diff --git a/projects/Modloader/il2cpp_helpers.h b/projects/Modloader/il2cpp_helpers.h index 7f99ae836b5..7ff9ade4592 100644 --- a/projects/Modloader/il2cpp_helpers.h +++ b/projects/Modloader/il2cpp_helpers.h @@ -1,8 +1,6 @@ #pragma once -#include #include -#include #include #include #include @@ -13,7 +11,7 @@ #include #include -#include +#include #include using GCHandleId = uint32_t; @@ -190,6 +188,8 @@ namespace il2cpp { IL2CPP_MODLOADER_DLLEXPORT std::string get_class_namespace(Il2CppClass* klass); + IL2CPP_MODLOADER_DLLEXPORT void attach_thread(); + template Return* get_class(std::string_view namezpace, std::string_view name) { return reinterpret_cast(untyped::get_class(namezpace, name)); diff --git a/projects/Modloader/modloader.cpp b/projects/Modloader/modloader.cpp index c1cd6a50082..d4c00e18fde 100644 --- a/projects/Modloader/modloader.cpp +++ b/projects/Modloader/modloader.cpp @@ -11,12 +11,13 @@ #include #include #include +#include +#include #include #include #include -#include //---------------------------------------------------Globals----------------------------------------------------- @@ -89,7 +90,7 @@ namespace modloader { } void debug(std::string const& group, std::string const& message) { - trace(MessageType::Info, group, message); + trace(MessageType::Debug, group, message); } void info(std::string const& group, std::string const& message) { @@ -108,6 +109,7 @@ namespace modloader { std::shared_ptr buffer_logging_handler; std::shared_ptr file_logging_handler; + std::shared_ptr console_logging_handler; std::binary_semaphore wait_for_exit(0); IL2CPP_MODLOADER_C_DLLEXPORT void injection_entry(const std::filesystem::path& path, const std::function& on_initialization_complete, const std::function& on_error) { @@ -115,6 +117,7 @@ namespace modloader { buffer_logging_handler = register_logging_handler(std::make_shared()); file_logging_handler = register_logging_handler(std::make_shared(base_path() / csv_path)); + console_logging_handler = register_logging_handler(std::make_shared()); trace(MessageType::Info, "initialize", "Loading settings."); diff --git a/projects/Modloader/udp_socket.h b/projects/Modloader/udp_socket.h index a7789fe5d2b..d1a90c7be3d 100644 --- a/projects/Modloader/udp_socket.h +++ b/projects/Modloader/udp_socket.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include namespace modloader { @@ -50,4 +50,4 @@ namespace modloader { std::string server; int port; }; -} // namespace modloader \ No newline at end of file +} // namespace modloader diff --git a/projects/Modloader/windows_api/console.cpp b/projects/Modloader/windows_api/console.cpp index 59f3046846a..59e9b485a3f 100644 --- a/projects/Modloader/windows_api/console.cpp +++ b/projects/Modloader/windows_api/console.cpp @@ -35,7 +35,7 @@ namespace modloader::win::console { Command root; - bool initialzed = false; + bool initialized = false; bool failed = false; FILE* console_file; std::future console_input; @@ -218,7 +218,7 @@ namespace modloader::win::console { void console_initialize() { console_file = nullptr; - initialzed = false; + initialized = false; failed = true; if (!AllocConsole()) { return; @@ -247,13 +247,19 @@ namespace modloader::win::console { SetConsoleCP(GetACP()); SetConsoleOutputCP(GetACP()); + HANDLE console_handle = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD console_mode = 0; + GetConsoleMode(console_handle, &console_mode); + console_mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + SetConsoleMode(console_handle, console_mode); + console_input = std::async(read_command); - initialzed = true; + initialized = true; failed = false; } void console_free() { - if (!initialzed) { + if (!initialized) { return; } @@ -302,7 +308,7 @@ namespace modloader::win::console { void console_poll() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (initialzed && console_input.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + if (initialized && console_input.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { const auto command = console_input.get(); if (command.rfind("echo ", 0) != std::string::npos) { std::cout << command.substr(5, command.length()) << std::endl; diff --git a/projects/Randomizer/CMakeLists.txt b/projects/Randomizer/CMakeLists.txt index bb7bac5fe8b..8a379dfa42f 100644 --- a/projects/Randomizer/CMakeLists.txt +++ b/projects/Randomizer/CMakeLists.txt @@ -59,6 +59,7 @@ set( "features/scenes/modifications/marsh_burrow_fight_arena_allow_teleportation.cpp" "features/scenes/modifications/meeting_kwolok_cutscene_trigger_underwater_fix.cpp" "features/scenes/modifications/reactive_bone_bridge_state.cpp" + "features/scenes/modifications/ruins_escape_allow_teleportation.cpp" "features/scenes/modifications/sandless_feeding_grounds_to_elevator.cpp" "features/scenes/modifications/sandless_shriek_escape.cpp" "features/secrets.cpp" @@ -83,6 +84,7 @@ set( "game/behaviour_changes/fix_straight_grenades.cpp" "game/behaviour_changes/flap_without_glide.cpp" "game/behaviour_changes/instant_tp_activation.cpp" + "game/behaviour_changes/keep_shriek_health_bar_visible.cpp" "game/behaviour_changes/kwolok_boss_rubberbanding_fix.cpp" "game/behaviour_changes/luma_contact_switch_door_jank_fix.cpp" "game/behaviour_changes/patch2_kickback.cpp" @@ -93,6 +95,7 @@ set( "game/behaviour_changes/teleporter_glades_identifier.cpp" "game/behaviour_changes/teleporter_map_activation.cpp" "game/behaviour_changes/teleporting_oob_fix.cpp" + "game/behaviour_changes/luma_trial_bubbles.cpp" "game/behaviour_changes/trials_leaderboards.cpp" "game/behaviour_changes/triple_jump_shockwave.cpp" "game/condition_intercepts/day_night_logic.cpp" diff --git a/projects/Randomizer/features/scenes/modifications/ruins_escape_allow_teleportation.cpp b/projects/Randomizer/features/scenes/modifications/ruins_escape_allow_teleportation.cpp new file mode 100644 index 00000000000..36aedebac69 --- /dev/null +++ b/projects/Randomizer/features/scenes/modifications/ruins_escape_allow_teleportation.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + using namespace app::classes; + + std::optional> teleport_restrict_zone; + std::shared_ptr effect; + + core::api::uber_states::UberState fix_enabled_state(UberStateGroup::RandoConfig, 21); + + [[maybe_unused]] + auto on_scene_loaded_handler = core::api::scenes::single_event_bus().register_handler( + "desertRuinsBGChase", + [](const core::api::scenes::SceneLoadEventMetadata* metadata, const std::string&) { + if (metadata->state != app::SceneState__Enum::Loaded) { + return; + } + + const auto scene_root_go = il2cpp::unity::get_game_object(metadata->scene->fields.SceneRoot); + + const auto teleport_restrict_zone_go = il2cpp::unity::find_child( + scene_root_go, + std::vector{ + "wormSetup", + "teleportRestrictZone", + } + ); + + teleport_restrict_zone = il2cpp::WeakGCRef( + il2cpp::unity::get_component(teleport_restrict_zone_go, types::TeleportRestrictZone::get_class()) + ); + + effect = core::reactivity::watch_effect([] { + if (teleport_restrict_zone.has_value() && teleport_restrict_zone->is_valid()) { + auto cage_structure_tool = (**teleport_restrict_zone)->fields.CageStructureTool; + + if (fix_enabled_state.get()) { + cage_structure_tool->fields.Vertices->fields._items->vector[0]->fields.Position = app::Vector3{-94.740479f, -140.710449f, 0.f}; + cage_structure_tool->fields.Vertices->fields._items->vector[1]->fields.Position = app::Vector3{-94.369141f, -56.7434082f, 0.f}; + } else { + // Vanilla values + cage_structure_tool->fields.Vertices->fields._items->vector[0]->fields.Position = app::Vector3{-134.740479f, -140.710449f, 0.f}; + cage_structure_tool->fields.Vertices->fields._items->vector[1]->fields.Position = app::Vector3{-134.369141f, -56.7434082f, 0.f}; + } + + CageStructureTool::MarkDirty(cage_structure_tool); + } else { + effect = nullptr; + } + }); + } + ); +} // namespace diff --git a/projects/Randomizer/game/behaviour_changes/keep_shriek_health_bar_visible.cpp b/projects/Randomizer/game/behaviour_changes/keep_shriek_health_bar_visible.cpp new file mode 100644 index 00000000000..deb405265b0 --- /dev/null +++ b/projects/Randomizer/game/behaviour_changes/keep_shriek_health_bar_visible.cpp @@ -0,0 +1,25 @@ +#include +#include +#include +#include + +#include + +namespace { + using namespace app::classes; + + core::api::uber_states::UberState keep_health_bar_state(UberStateGroup::RandoConfig, 22); + + IL2CPP_INTERCEPT(PetrifiedOwlBossEntity, bool, ShouldShowHealthBar, (app::PetrifiedOwlBossEntity* this_ptr)) { + if (!keep_health_bar_state.get()) { + return next::PetrifiedOwlBossEntity::ShouldShowHealthBar(this_ptr); + } + + if (this_ptr->fields._._.VitalState != app::Entity_EntityVitalState__Enum::Alive) { + return false; + } + + // m_shouldIncrementState is true when you actually cannot deal damage to Shriek + return !this_ptr->fields.m_shouldIncrementState; + } +} // namespace diff --git a/projects/Randomizer/game/behaviour_changes/luma_trial_bubbles.cpp b/projects/Randomizer/game/behaviour_changes/luma_trial_bubbles.cpp new file mode 100644 index 00000000000..12a575b14ad --- /dev/null +++ b/projects/Randomizer/game/behaviour_changes/luma_trial_bubbles.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +namespace { + IL2CPP_INTERCEPT(Bubble, void, OnPoolSpawned, (app::Bubble* this_ptr)) { + next::Bubble::OnPoolSpawned(this_ptr); + + // Reset to default values so recycled bubbles don't have random values + this_ptr->fields.AccelerationY = this_ptr->fields.BaseAccelerationY; + il2cpp::unity::set_local_scale(this_ptr, this_ptr->fields.BaseLocalScale); + } + + IL2CPP_INTERCEPT(Bubblemaker, void, SpawnBubble, (app::Bubblemaker* this_ptr)) { + // Prevent fake bubbles to spawn due to lazily resetting MoonTimelines + if (this_ptr->fields.RaceOverride && this_ptr->fields.m_raceTimeLastUpdate == 0.0) { + return; + } + + next::Bubblemaker::SpawnBubble(this_ptr); + } + + IL2CPP_INTERCEPT(Bubblemaker, void, OnSyncRaceTimer, (app::Bubblemaker* this_ptr, float time)) { + // Reset all timers on race restart, they forgot that... + // This isn't too important since it doesn't actually spawn bubbles, + // but it displays weird animations + if (time == 0.0) { + this_ptr->fields.m_raceTimeLastUpdate = 0.0; + this_ptr->fields.m_spawnOffsetTimer = 0.0; + this_ptr->fields.m_spawnTimer = 0.0; + return; + } + + next::Bubblemaker::OnSyncRaceTimer(this_ptr, time); + } +} // namespace diff --git a/projects/Randomizer/league/league.cpp b/projects/Randomizer/league/league.cpp index 6ec0e0bed7b..0a067fed89d 100644 --- a/projects/Randomizer/league/league.cpp +++ b/projects/Randomizer/league/league.cpp @@ -31,7 +31,10 @@ #include #define CPPHTTPLIB_OPENSSL_SUPPORT +#include #include +#include +#include #include #undef MessageBox @@ -173,7 +176,7 @@ namespace randomizer::league { status_message_mutex.unlock(); } - void start_submission_thread(const std::string& save_file_path) { + void start_submission_thread(const int save_slot_index) { using namespace std::chrono_literals; if (!get_multiverse_id().has_value()) { @@ -195,11 +198,12 @@ namespace randomizer::league { const auto multiverse_id = get_multiverse_id().value(); const auto jwt = randomizer::online::get_jwt(); - submit_thread = std::make_shared([host, insecure, multiverse_id, jwt, save_file_path]() { + submit_thread = std::make_shared([host, insecure, multiverse_id, jwt, save_slot_index]() { + il2cpp::attach_thread(); + upload_attempt.store(0); - std::ifstream input(save_file_path, std::ios::binary); - std::vector save_file_data(std::istreambuf_iterator(input), {}); + auto save_file_data = core::api::save_files::get_bytes(save_slot_index); while (submission_status.load() == SubmissionStatus::Uploading) { const auto attempt = upload_attempt.fetch_add(1) + 1; @@ -215,7 +219,7 @@ namespace randomizer::league { auto result = client.Post(std::format("/api/league/{}/submission", multiverse_id), { {"User-Agent", std::format("OriAndTheWillOfTheWispsRandomizer/{}", randomizer_version().to_string())}, {"Authorization", std::format("Bearer {}", jwt)} - }, save_file_data.data(), save_file_data.size(), "application/octet-stream"); + }, reinterpret_cast(save_file_data.data()), save_file_data.size(), "application/octet-stream"); if (result.error() != httplib::Error::Success) { set_status_message_thread_safe("Request failed"); @@ -319,8 +323,7 @@ namespace randomizer::league { core::api::audio::play_sound(SoundEventID::LeagueSubmitted); const auto save_controller = core::api::game::save_controller(); - const auto current_save_file_path = il2cpp::convert_csstring(SaveGameController::get_CurrentSaveFileInfo(save_controller)->fields.m_FullSaveFilePath); - start_submission_thread(current_save_file_path); + start_submission_thread(SaveGameController::get_CurrentSlotIndex(save_controller)); }); } else { const auto resolved_epilogue_timeline = il2cpp::invoke(shriek_entity->fields.EpilogueTimeline, "Resolve", 0); diff --git a/projects/Randomizer/randomizer.cpp b/projects/Randomizer/randomizer.cpp index d809559e8fe..41b046b3779 100644 --- a/projects/Randomizer/randomizer.cpp +++ b/projects/Randomizer/randomizer.cpp @@ -61,6 +61,7 @@ namespace randomizer { bool reach_check_queued = false; bool reach_check_in_progress = false; + bool pause_timer = false; std::optional multiverse_id_to_connect_to = std::nullopt; @@ -107,7 +108,10 @@ namespace randomizer { }); auto on_after_new_game_initialized = core::api::game::event_bus().register_handler(GameEvent::NewGameInitialized, EventTiming::After, [](auto, auto) { + core::message_controller().clear_recent_messages(); + queue_input_unlocked_callback([]() { + pause_timer = false; randomizer_seed.trigger(seed::SeedClientEvent::Spawn); randomizer_seed.trigger(seed::SeedClientEvent::Reload); core::api::game::save(true); @@ -155,8 +159,10 @@ namespace randomizer { } auto on_before_new_game_initialized = core::api::game::event_bus().register_handler(GameEvent::NewGameInitialized, EventTiming::Before, [](auto, auto) { - seed_meta_save_data->seed_source_string = new_game_seed_source->to_source_string(); - seed_archive_save_data->seed_archive = new_game_seed_archive; + pause_timer = true; + + seed_save_data->seed_source_string = new_game_seed_source->to_source_string(); + seed_save_data->seed_content = new_game_seed_content; load_seed(false); // Allow cheats in offline games and clear GUID restrictions @@ -258,6 +264,10 @@ namespace randomizer { }); } // namespace + bool timer_should_pause() { + return pause_timer; + } + void load_new_game_source() { std::ifstream seed_source_file(modloader::base_path() / ".newgameseedsource", std::ios::binary); diff --git a/projects/Randomizer/randomizer.h b/projects/Randomizer/randomizer.h index a9d9fae2783..b8770f6a8f7 100644 --- a/projects/Randomizer/randomizer.h +++ b/projects/Randomizer/randomizer.h @@ -34,6 +34,7 @@ namespace randomizer { NewGameSeedSourceUpdated, }; + bool timer_should_pause(); void reread_seed_source(); void server_connect(long multiverse_id, bool force_reconnect = false); void server_reconnect_current_multiverse(); diff --git a/projects/Randomizer/seed/legacy_parser/parser.cpp b/projects/Randomizer/seed/legacy_parser/parser.cpp index e69de29bb2d..c118caf6146 100644 --- a/projects/Randomizer/seed/legacy_parser/parser.cpp +++ b/projects/Randomizer/seed/legacy_parser/parser.cpp @@ -0,0 +1,2210 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace randomizer::seed::legacy_parser { + using location_type = core::api::uber_states::UberStateCondition; + // Hacky way to do it, but can't be bothered to pass it all the way down. + location_data::LocationCollection const* current_location_data = nullptr; + + struct ParserData { + std::shared_ptr data; + ItemData& location_data; + int items_added; + int& next_location_id; + int& next_procedure_id; + bool should_add_to_always_granted; + bool should_add_default_messages; + + int line_number; + std::string item_definition; + + void add_item(ItemData::item_entry entry) { + ++items_added; + entry->seed_line_number = line_number; + entry->seed_definition = item_definition; + auto& collection = should_add_to_always_granted ? location_data.always_granted_items : location_data.items; + collection[next_location_id++] = std::move(entry); + } + }; + + enum class ActionType { + SpiritLight = 0, + Resource = 1, + Ability = 2, + Shard = 3, + SystemCommand = 4, + Teleporter = 5, + Message = 6, + UberState = 8, + QuestEvent = 9, + BonusItem = 10, + WeaponUpgrade = 11, + ZoneHint = 12, + CheckableHint = 13, + Relic = 14, + SysMessage = 15, + Wheel = 16, + Shop = 17, + SetMapMessage = 18 + }; + + enum class ResourceType { Health = 0, Energy = 1, Ore = 2, Keystone = 3, ShardSlot = 4 }; + + enum class SystemCommand { + Save = 0, + SetResource = 1, + Checkpoint = 2, + SupressMagic = 3, + StopIfEqual = 4, + StopIfGreater = 5, + StopIfLess = 6, + SetState = 7, + Warp = 8, + RedirectState = 11, + SetHealth = 12, + SetEnergy = 13, + SetSpiritLight = 14, + Bind = 15, + InputAction = 16, + GrantIfEqual = 17, + GrantIfGreater = 18, + GrantIfLess = 19, + DisableSync = 20, + EnableSync = 21, + CreateWarp = 22, + DestroyWarp = 23, + GrantIfBounds = 24, + GrantIfCondEqual = 25, + GrantIfCondGreater = 26, + GrantIfCondLess = 27, + Unbind = 28, + SaveString = 29, + AppendString = 30, + SetIconOverride = 31, + ClearIconOverride = 32, + }; + + void set_location(items::Message* message, location_type const& location) { + auto const& location_data = current_location_data->location(location); + if (location_data.has_value() && location_data.value().position.has_value()) { + const auto [x, y] = location_data.value().position.value(); + message->info.pickup_position = app::Vector3{x, y, 0}; + } + } + + bool parse_action(location_type const& location, std::span parts, ParserData& data); + + std::string concatenate_parts(std::span const& parts) { + return std::accumulate(parts.begin(), parts.end(), std::string(), [](std::string const& ss, std::string const& s) { + return ss.empty() ? s : std::format("{}|{}", ss, s); + }); + } + + void parse_parts(const std::string_view str, std::vector& parts) { + enum class ReadMode { Normal, Ptr }; + + std::string next; + auto mode = ReadMode::Normal; + std::stringstream stream((std::string(str))); + char c = static_cast(stream.get()); + while (!stream.eof()) { + if (mode == ReadMode::Normal && c == '|') { + parts.push_back(next); + next.clear(); + } else if (mode == ReadMode::Normal && c == '$' && stream.peek() == '(') { + mode = ReadMode::Ptr; + next += c; + } else if (mode == ReadMode::Ptr && c == ')') { + mode = ReadMode::Normal; + next += c; + } else { + next += c; + } + + c = static_cast(stream.get()); + } + + parts.push_back(next); + } + + bool parse_spirit_light(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int spirit_light; + if (!string_convert(parts[0], spirit_light)) { + return false; + } + + const auto line = concatenate_parts(parts); + + const auto assigner = std::make_shared>(); + assigner->variable = core::api::game::player::spirit_light(); + assigner->value.set(spirit_light); + data.add_item(assigner); + + const auto collected = std::make_shared>(); + collected->variable = core::Property(UberStateGroup::RandoStats, 3); + collected->value.set(spirit_light); + data.add_item(collected); + + if (!data.should_add_default_messages) { + return true; + } + + std::string currency = "Spirit Light"; + if (!core::settings::use_default_currency_name()) { + const auto slug_hash = std::hash()(data.data->info.meta.slug); + const auto location_hash = std::hash()(location); + currency = core::text::get_random_text_with_hash( + spirit_light == 1 + ? *static_text_entry::CurrencySingular + : *static_text_entry::CurrencyPlural, + slug_hash ^ location_hash + ); + } + + const auto text = std::format("{} {}", spirit_light, currency); + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + message->info.text.set(text); + + data.add_item(message); + data.location_data.names.emplace_back(message->info.text); + data.location_data.icons.emplace_back(MapIcon::Experience); + + return true; + } + + bool parse_resource(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int resource_type_int; + if (!string_convert(parts[0], resource_type_int)) { + return false; + } + + const auto line = concatenate_parts(parts); + + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + MapIcon icon; + switch (static_cast(resource_type_int)) { + case ResourceType::Health: { + const auto adder = std::make_shared>(); + adder->variable = core::api::game::player::max_health(); + adder->value.set(5); + data.add_item(adder); + const auto refill = std::make_shared(); + refill->type = items::Refill::RefillType::Health; + data.add_item(refill); + icon = MapIcon::HealthFragment; + message->info.text.set("Health Fragment"); + break; + } + case ResourceType::Energy: { + const auto adder = std::make_shared>(); + adder->variable = core::api::game::player::max_energy(); + adder->value.set(0.5f); + data.add_item(adder); + const auto refill = std::make_shared(); + refill->type = items::Refill::RefillType::Energy; + data.add_item(refill); + icon = MapIcon::EnergyFragment; + message->info.text.set("Energy Fragment"); + break; + } + case ResourceType::Ore: { + const auto adder = std::make_shared>(); + adder->variable = core::api::game::player::ore(); + adder->value.set(1); + data.add_item(adder); + const auto collected = std::make_shared>(); + collected->variable = core::Property(UberStateGroup::RandoStats, 5); + collected->value.set(1); + data.add_item(collected); + icon = MapIcon::Ore; + message->info.text.set("Gorlek Ore"); + break; + } + case ResourceType::Keystone: { + const auto adder = std::make_shared>(); + adder->variable = core::api::game::player::keystones(); + adder->value.set(1); + data.add_item(adder); + const auto collected = std::make_shared>(); + collected->variable = core::Property(UberStateGroup::RandoStats, 0); + collected->value.set(1); + data.add_item(collected); + icon = MapIcon::Keystone; + message->info.text.set("Keystone"); + break; + } + case ResourceType::ShardSlot: { + const auto adder = std::make_shared>(); + adder->variable = core::api::game::player::shard_slots(); + adder->value.set(1); + data.add_item(adder); + icon = MapIcon::ShardSlotUpgrade; + message->info.text.set("Shard Slot"); + break; + } + default: + return false; + } + + if (!data.should_add_default_messages) { + return true; + } + + data.add_item(message); + data.location_data.names.emplace_back(message->info.text); + data.location_data.icons.emplace_back(icon); + return true; + } + + bool parse_ability(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int ability_type_int; + if (!string_convert(parts[0], ability_type_int)) { + return false; + } + + const auto line = concatenate_parts(parts); + + const auto should_add = !parts[0].starts_with("-"); + ability_type_int = should_add ? ability_type_int : -ability_type_int; + const auto assigner = std::make_shared>(); + assigner->variable.assign(core::api::uber_states::UberState(UberStateGroup::Skills, ability_type_int)); + assigner->value.set(should_add); + data.add_item(assigner); + + if (!data.should_add_default_messages) { + return true; + } + + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + const auto text = should_add ? std::format("[ability({0})]", ability_type_int) : std::format("Removed [ability({0})]", ability_type_int); + message->info.text.set(text); + data.add_item(message); + data.location_data.icons.emplace_back(MapIcon::AbilityPedestal); + data.location_data.names.emplace_back(text); + return true; + } + + bool parse_shard(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int shard_type_int; + if (!string_convert(parts[0], shard_type_int)) { + return false; + } + + const auto line = concatenate_parts(parts); + + const auto should_add = !parts[0].starts_with("-"); + shard_type_int = should_add ? shard_type_int : -shard_type_int; + const auto shard_type = static_cast(shard_type_int); + const auto assigner = std::make_shared>(); + assigner->variable = core::api::game::player::shard(shard_type); + assigner->value.set(should_add); + data.add_item(assigner); + + if (!data.should_add_default_messages) { + return true; + } + + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + const auto text = should_add ? std::format("[shard({0})]", shard_type_int) : std::format("Removed [shard({0})]", shard_type_int); + message->info.text.set(text); + data.add_item(message); + data.location_data.icons.emplace_back(MapIcon::SpiritShard); + data.location_data.names.emplace_back(text); + return true; + } + + bool parse_stop_if(std::span parts, ParserData& data, BooleanOperator op) { + int group; + int state; + double value; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state) || !string_convert(parts[2], value)) { + return false; + } + + const auto stop_condition = core::api::uber_states::UberStateCondition{{group, state}, op, value}; + const auto stop = std::make_shared(); + stop->stop.assign([](auto) {}, [stop_condition]() { return stop_condition.resolve(); }); + + data.add_item(stop); + return true; + } + + bool parse_grant_if_state( // NOLINT + location_type const& location, + core::api::uber_states::UberState const& state, + std::span parts, + ParserData& data, + BooleanOperator op + ) { + if (parts.size() < 2) { + return false; + } + + double value; + if (!string_convert(parts[0], value)) { + return false; + } + + const auto line = concatenate_parts(parts); + + const auto skip = std::make_shared(); + auto condition = core::api::uber_states::UberStateCondition{{state.group(), state.state()}, op, value}; + data.add_item(skip); + auto skip_value = data.items_added; + + const auto sub_parts = std::span(parts.begin() + 1, parts.end()); + data.item_definition = concatenate_parts(sub_parts); + parse_action(location, sub_parts, data); + + skip_value = data.items_added - skip_value; + skip->skip.assign(core::set_get{ + [](int) {}, + [condition, skip_value] { return condition.resolve() ? 0 : skip_value; }, + }); + + return true; + } + + bool parse_grant_if( // NOLINT + location_type const& location, + std::span parts, + ParserData& data, + BooleanOperator op + ) { + if (parts.size() < 2) { + return false; + } + + int group; + int state; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state)) { + return false; + } + + const auto sub_parts = std::span(parts.begin() + 2, parts.end()); + return parse_grant_if_state(location, {group, state}, sub_parts, data, op); + } + + bool parse_grant_if_bounds( // NOLINT + location_type const& location, + std::span parts, + ParserData& data + ) { + if (parts.size() < 4) { + return false; + } + + app::Rect rect{}; + if (!string_convert(parts[0], rect.m_XMin) || !string_convert(parts[1], rect.m_YMin) || !string_convert(parts[2], rect.m_Width) || + !string_convert(parts[3], rect.m_Height)) { + return false; + } + + const auto line = concatenate_parts(parts); + + if (rect.m_XMin > rect.m_Width) { + std::swap(rect.m_XMin, rect.m_Width); + } + + if (rect.m_YMin > rect.m_Height) { + std::swap(rect.m_YMin, rect.m_Height); + } + + rect.m_Width -= rect.m_XMin; + rect.m_Height -= rect.m_YMin; + + const auto skip = std::make_shared(); + data.add_item(skip); + auto skip_value = data.items_added; + + const auto sub_parts = std::span(parts.begin() + 1, parts.end()); + parse_action(location, sub_parts, data); + + skip_value = data.items_added - skip_value; + skip->skip.assign(core::set_get{ + [](int value) {}, + [rect, skip_value]() { return modloader::math::in_rect(core::api::game::player::get_position(), rect) ? 0 : skip_value; }, + }); + + return true; + } + + bool parse_save(std::span parts, ParserData& data, bool checkpoint) { + const auto caller = std::make_shared(); + + std::optional override_position = std::nullopt; + + if (parts.size() >= 2) { + app::Vector2 position; + if (!string_convert(parts[0], position.x) || !string_convert(parts[1], position.y)) { + return false; + } + override_position = position; + } + + core::api::game::SaveOptions save_options( + false, false, !checkpoint, false, override_position + ); + + caller->description = checkpoint ? "do checkpoint" : "do save"; + + if (override_position.has_value()) { + caller->description += std::format(" at {}, {}", override_position->x, override_position->y); + } + + caller->func = [save_options] { + if (core::api::game::can_save()) { + core::api::game::save(false, save_options); + } + }; + + data.add_item(caller); + return true; + } + + bool parse_warp(std::span parts, ParserData& data) { + if (parts.size() < 2) { + return false; + } + + app::Vector3 position{}; + if (!string_convert(parts[0], position.x) || !string_convert(parts[1], position.y)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = "warp player"; + caller->func = [position]() { game::teleportation::teleport_instantly(position); }; + data.add_item(caller); + return true; + } + + bool parse_redirect_state(std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + conditions::applier_key key; + int state; + if (!string_convert(parts[1], key.second) || !string_convert(parts[2], state)) { + return false; + } + + key.first = parts[0]; + const auto caller = std::make_shared(); + caller->description = std::format("register redirect {}:{}", key.first, key.second); + caller->func = [key, state]() { conditions::register_new_setup_redirect(key, state); }; + data.add_item(caller); + return true; + } + + bool parse_bind(std::span parts, ParserData& data) { + if (parts.size() != 2) { + return false; + } + + int binding; + int equip; + if (!string_convert(parts[0], binding) || !string_convert(parts[1], equip)) { + return false; + } + + if (binding < 0 || binding > 2) { + return false; + } + + auto binding_type = static_cast(binding); + auto equip_type = static_cast(equip); + + if (!core::api::game::player::is_valid_equipment(equip_type)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = std::format("equip {} to binding {}", equip, binding); + caller->func = [binding_type, equip_type]() { core::api::game::player::bind(binding_type, equip_type); }; + data.add_item(caller); + return true; + } + + bool parse_unbind(std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int equip; + if (!string_convert(parts[0], equip)) { + return false; + } + + auto equip_type = static_cast(equip); + if (!core::api::game::player::is_valid_equipment(equip_type)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = std::format("unbind {}", static_cast(equip_type)); + caller->func = [equip_type]() { core::api::game::player::unbind(equip_type); }; + data.add_item(caller); + return true; + } + + bool parse_sync(std::span parts, ParserData& data, bool unsyncable) { + if (parts.size() != 2) { + return false; + } + + int group; + int state; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state)) { + return false; + } + + core::api::uber_states::UberState sync_state(group, state); + const auto caller = std::make_shared(); + caller->description = std::format("set unsyncable {}", sync_state.to_string()); + caller->func = [sync_state, unsyncable]() { multiplayer_universe().uber_state_handler().set_unsyncable(sync_state, unsyncable); }; + data.add_item(caller); + return true; + } + + bool parse_create_warp(std::span parts, ParserData& data) { + if (parts.size() != 4) { + return false; + } + + const auto icon = std::make_shared(); + if (!string_convert(parts[0], icon->id) || !string_convert(parts[1], icon->position.x) || !string_convert(parts[2], icon->position.y)) { + return false; + } + + icon->label = parts[3]; + icon->flags = game::map::FilterFlag::All | game::map::FilterFlag::Teleports | game::map::FilterFlag::InLogic | game::map::FilterFlag::Spoilers; + icon->type = MapIcon::SavePedestal; + icon->can_teleport = true; + data.add_item(icon); + return true; + } + + bool parse_destroy_warp(std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + const auto icon = std::make_shared(); + if (!string_convert(parts[0], icon->id)) { + return false; + } + + data.add_item(icon); + return true; + } + + bool parse_string(std::span parts, ParserData& data, bool append) { + if (parts.size() < 2) { + return false; + } + + int id; + if (!string_convert(parts[0], id)) { + return false; + } + + std::string text; + for (auto it = parts.begin() + 1; it != parts.end(); ++it) { + if (!text.empty()) { + text += "|"; + } + + text += *it; + } + + auto caller = std::make_shared(); + caller->description = append ? std::format("append \"{}\" to text entry {}", text, id) : std::format("set text entry {} to \"{}\"", id, text); + caller->func = [append, id, text]() { + auto mutable_text = text; + general_text_processor()->process(*general_text_processor(), mutable_text); + std::string actual_text(append ? core::text::get_text(id) : ""); + actual_text += mutable_text; + core::text::clear_text(id); + core::text::register_text(id, actual_text); + }; + + data.add_item(caller); + return true; + } + + bool parse_input_action(const std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + const auto action = magic_enum::enum_cast(parts[0]); + if (!action.has_value()) { + return false; + } + + const auto input = std::make_shared(); + input->action = action.value(); + data.add_item(input); + return true; + } + bool parse_set_icon_override(const std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + core::api::uber_states::UberStateCondition location; + if (!parse_condition(std::span(parts.begin(), parts.begin() + 2), location)) { + return false; + } + + const auto icon = magic_enum::enum_cast(parts[2]); + if (!icon.has_value()) { + return false; + } + + const auto item = std::make_shared(); + item->data = data.data; + item->location = location; + item->icon = icon.value(); + data.add_item(item); + return true; + } + + bool parse_clear_icon_override(const std::span parts, ParserData& data) { + if (parts.size() != 2) { + return false; + } + + core::api::uber_states::UberStateCondition location; + if (!parse_condition(std::span(parts.begin(), parts.begin() + 2), location)) { + return false; + } + + const auto item = std::make_shared(); + item->data = data.data; + item->location = location; + data.add_item(item); + return true; + } + + bool parse_sys_command(location_type const& location, std::span parts, ParserData& data) { + // NOLINT + int sys_command_int; + if (!string_convert(parts[0], sys_command_int)) { + return false; + } + + const auto sys_command = static_cast(sys_command_int); + const auto next_parts = std::span(parts.begin() + 1, parts.end()); + switch (sys_command) { + case SystemCommand::Save: + return parse_save(next_parts, data, false); + case SystemCommand::Checkpoint: + return parse_save(next_parts, data, true); + case SystemCommand::StopIfEqual: + return parse_stop_if(next_parts, data, BooleanOperator::Equals); + case SystemCommand::StopIfGreater: + return parse_stop_if(next_parts, data, BooleanOperator::Greater); + case SystemCommand::StopIfLess: + return parse_stop_if(next_parts, data, BooleanOperator::Lesser); + case SystemCommand::GrantIfEqual: + return parse_grant_if(location, next_parts, data, BooleanOperator::Equals); + case SystemCommand::GrantIfGreater: + return parse_grant_if(location, next_parts, data, BooleanOperator::Greater); + case SystemCommand::GrantIfLess: + return parse_grant_if(location, next_parts, data, BooleanOperator::Lesser); + case SystemCommand::GrantIfCondEqual: + return parse_grant_if_state(location, location.state, next_parts, data, BooleanOperator::Equals); + case SystemCommand::GrantIfCondGreater: + return parse_grant_if_state(location, location.state, next_parts, data, BooleanOperator::Greater); + case SystemCommand::GrantIfCondLess: + return parse_grant_if_state(location, location.state, next_parts, data, BooleanOperator::Lesser); + case SystemCommand::GrantIfBounds: + return parse_grant_if_bounds(location, next_parts, data); + case SystemCommand::Warp: + return parse_warp(next_parts, data); + case SystemCommand::RedirectState: + return parse_redirect_state(next_parts, data); + case SystemCommand::Bind: + return parse_bind(next_parts, data); + case SystemCommand::Unbind: + return parse_unbind(next_parts, data); + case SystemCommand::EnableSync: + return parse_sync(next_parts, data, false); + case SystemCommand::DisableSync: + return parse_sync(next_parts, data, true); + case SystemCommand::CreateWarp: + return parse_create_warp(next_parts, data); + case SystemCommand::DestroyWarp: + return parse_destroy_warp(next_parts, data); + case SystemCommand::SaveString: + return parse_string(next_parts, data, false); + case SystemCommand::AppendString: + return parse_string(next_parts, data, true); + case SystemCommand::InputAction: + return parse_input_action(next_parts, data); + case SystemCommand::SetIconOverride: + return parse_set_icon_override(next_parts, data); + case SystemCommand::ClearIconOverride: + return parse_clear_icon_override(next_parts, data); + case SystemCommand::SetHealth: + modloader::warn("legacy_parser", "Use of deprecated command SystemCommand.SetHealth use virtual uber states instead."); + return false; + case SystemCommand::SetEnergy: + modloader::warn("legacy_parser", "Use of deprecated command SystemCommand.SetEnergy use virtual uber states instead."); + return false; + case SystemCommand::SetSpiritLight: + modloader::warn("legacy_parser", "Use of deprecated command SystemCommand.SetSpiritLight use virtual uber states instead."); + return false; + case SystemCommand::SetResource: + modloader::warn("legacy_parser", "Use of deprecated command SystemCommand.SetResource use virtual uber states instead."); + return false; + case SystemCommand::SupressMagic: + modloader::warn("legacy_parser", "Use of deprecated command SystemCommand.SuppressMagic use virtual uber states instead."); + return false; + case SystemCommand::SetState: + modloader::warn("legacy_parser", "Use of deprecated command SystemCommand.SetState use uber states instead."); + return false; + default: + return false; + } + } + + bool parse_teleporter(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int teleporter_int; + if (!string_convert(parts[0], teleporter_int)) { + return false; + } + + const auto line = concatenate_parts(parts); + + const auto should_add = !parts[0].starts_with("-"); + teleporter_int = should_add ? teleporter_int : -teleporter_int; + const auto assigner = std::make_shared>(); + assigner->value.set(should_add); + auto teleporter_name = "Unknown"; + switch (teleporter_int) { + case 0: + assigner->variable.assign(core::api::uber_states::UberState(24922, 42531)); + teleporter_name = "Midnight Burrows"; + break; + case 1: + assigner->variable.assign(core::api::uber_states::UberState(11666, 61594)); + teleporter_name = "Howl's Den"; + break; + case 3: + assigner->variable.assign(core::api::uber_states::UberState(53632, 18181)); + teleporter_name = "Wellspring"; + break; + case 4: + assigner->variable.assign(core::api::uber_states::UberState(28895, 54235)); + teleporter_name = "Baur's Reach"; + break; + case 5: + assigner->variable.assign(core::api::uber_states::UberState(937, 26601)); + teleporter_name = "Kwolok's Hollow"; + break; + case 6: + assigner->variable.assign(core::api::uber_states::UberState(18793, 38871)); + teleporter_name = "Mouldwood Depths"; + break; + case 7: + assigner->variable.assign(core::api::uber_states::UberState(58674, 7071)); + teleporter_name = "Woods Entrance"; + break; + case 8: + assigner->variable.assign(core::api::uber_states::UberState(58674, 1965)); + teleporter_name = "Woods Exit"; + break; + case 9: + assigner->variable.assign(core::api::uber_states::UberState(58674, 10029)); + teleporter_name = "Feeding Grounds"; + break; + case 10: + assigner->variable.assign(core::api::uber_states::UberState(20120, 49994)); + teleporter_name = "Central Wastes"; + break; + case 11: + assigner->variable.assign(core::api::uber_states::UberState(20120, 41398)); + teleporter_name = "Outer Ruins"; + break; + case 12: + assigner->variable.assign(core::api::uber_states::UberState(16155, 41465)); + teleporter_name = "Willow's End"; + break; + case 14: + assigner->variable.assign(core::api::uber_states::UberState(10289, 4928)); + teleporter_name = "Inner Ruins"; + break; + case 2: { + assigner->variable.assign(core::api::uber_states::UberState(945, 58183)); + teleporter_name = "Central Luma"; + if (should_add) { + auto lower_water = std::make_shared>(); + lower_water->variable.assign(core::api::uber_states::UberState(5377, 63173)); + lower_water->value.set(should_add); + data.add_item(lower_water); + } + break; + } + case 13: + assigner->variable.assign(core::api::uber_states::UberState(945, 1370)); + teleporter_name = "Luma Boss"; + break; + case 15: + assigner->variable.assign(core::api::uber_states::UberState(16155, 50867)); + teleporter_name = "Shriek"; + break; + case 16: + assigner->variable.assign(core::api::uber_states::UberState(21786, 10185)); + teleporter_name = "Inkwater Marsh"; + break; + case 17: + assigner->variable.assign(core::api::uber_states::UberState(42178, 42096)); + teleporter_name = "Glades"; + break; + default: + return false; + } + + data.add_item(assigner); + if (!data.should_add_default_messages) { + return true; + } + + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + const auto text = should_add ? std::format("#{0} TP#", teleporter_name) : std::format("Removed #{0} TP#", teleporter_name); + message->info.text.set(text); + data.add_item(message); + data.location_data.icons.emplace_back(MapIcon::SavePedestalInactive); + data.location_data.names.emplace_back(text); + return true; + } + + bool parse_message(location_type const& location, std::span parts, ParserData& data) { + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + message->info.duration = 4; + std::string text; + for (const auto& part: parts) { + if (part.starts_with("f=")) { + if (int frames; string_convert(part.substr(2), frames)) { + message->info.duration = static_cast(frames) / 60.f; + } + } else if (part.starts_with("p=")) { + // Not used anymore. + } else if (part == "noclear") { + // Not used anymore. + } else if (part == "quiet") { + message->info.play_sound = false; + } else if (part == "instant" || part == "prioritized") { + message->info.prioritized = true; + message->should_add_to_recent = false; + } else if (part == "nofade") { + message->info.instant_fade = true; + } else if (part == "prepend") { + // Not used anymore. + } else { + if (!text.empty()) { + text += "|"; + } + + text += part; + } + } + + message->info.text.set(text); + data.add_item(message); + data.location_data.names.emplace_back(text); + return true; + } + + std::regex ptr_regex(R"(\$\(([0-9]+)\|([0-9]+)\))"); + std::regex range_regex(R"(\[([^,\]]+),([^,\]]+)\])"); + + bool gen_from_frag(const std::string& frag, core::Property& value) { + std::smatch results; + if (std::regex_match(frag, results, ptr_regex)) { + int group; + int state; + string_convert(results[1].str(), group); + string_convert(results[2].str(), state); + value.assign(core::api::uber_states::UberState(group, state)); + } else { + double constant_value; + if (string_convert(frag, constant_value)) { + value.set(constant_value); + } else { + return false; + } + } + + return true; + } + + bool parse_uber_state(std::span parts, ParserData& data) { + if (parts.size() < 4 || parts.size() > 5) { + return false; + } + + int group; + if (!string_convert(parts[0], group)) { + return false; + } + + int state; + if (!string_convert(parts[1], state)) { + return false; + } + + // auto type = uberTypeFromString(extras[2]); + const bool negative = parts[3].starts_with('-'); + const bool is_modifier = parts[3].starts_with('+') || negative; + if (is_modifier) { + parts[3] = parts[3].substr(1); + } + + auto should_skip = false; + if (parts.size() > 4) { + if (!parts[4].starts_with("skip")) { + return false; + } + + should_skip = true; + } + + const auto line = concatenate_parts(parts); + if (std::smatch results; std::regex_match(parts[3], results, range_regex)) { + core::Property start; + core::Property end; + gen_from_frag(results[0], start); + gen_from_frag(results[1], end); + + auto setget = core::set_get{ + [](auto value) {}, + [start, end, negative]() { + const auto lower = start.get(); + const auto upper = end.get(); + const auto random_value = core::random(); + const auto sign = negative ? -1.f : 1.f; + return sign * random_value * (upper - lower) + lower; + }, + }; + + if (is_modifier) { + if (negative) { + auto setter = std::make_shared>(); + setter->variable.assign(core::api::uber_states::UberState(group, state)); + setter->value.assign(setget); + setter->should_skip_grants = should_skip; + data.add_item(setter); + } else { + auto setter = std::make_shared>(); + setter->variable.assign(core::api::uber_states::UberState(group, state)); + setter->value.assign(setget); + setter->should_skip_grants = should_skip; + data.add_item(setter); + } + } else { + auto setter = std::make_shared>(); + setter->variable.assign(core::api::uber_states::UberState(group, state)); + setter->value.assign(setget); + setter->should_skip_grants = should_skip; + data.add_item(setter); + } + } else if (is_modifier) { + if (negative) { + auto setter = std::make_shared>(); + setter->should_skip_grants = should_skip; + setter->variable.assign(core::api::uber_states::UberState(group, state)); + gen_from_frag(parts[3], setter->value); + data.add_item(setter); + } else { + auto setter = std::make_shared>(); + setter->should_skip_grants = should_skip; + setter->variable.assign(core::api::uber_states::UberState(group, state)); + gen_from_frag(parts[3], setter->value); + data.add_item(setter); + } + } else { + auto setter = std::make_shared>(); + setter->should_skip_grants = should_skip; + setter->variable.assign(core::api::uber_states::UberState(group, state)); + gen_from_frag(parts[3], setter->value); + data.add_item(setter); + } + + return true; + } + + bool parse_quest_event(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + // If we add more than one event, don't early out here. + if (int quest_event_int; !string_convert(parts[0], quest_event_int) || quest_event_int != 0) { + return false; + } + + const auto line = concatenate_parts(parts); + const auto should_add = !parts[0].starts_with("-"); + + const auto assigner = std::make_shared>(); + assigner->value.set(should_add); + assigner->variable.assign(core::api::uber_states::UberState(6, 2000)); + data.add_item(assigner); + + auto applier = std::make_shared(); + applier->state = core::api::uber_states::UberState(937, 34641); + data.add_item(applier); + + applier = std::make_shared(); + applier->state = core::api::uber_states::UberState(37858, 12379); + data.add_item(applier); + + applier = std::make_shared(); + applier->state = core::api::uber_states::UberState(37858, 10720); + data.add_item(applier); + + if (!data.should_add_default_messages) { + return true; + } + + auto quest_event = "Clean Water"; + const auto text = should_add ? std::format("{0}", quest_event) : std::format("Removed {0}", quest_event); + + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + message->info.text.set_format("*{0}*", text); + data.add_item(message); + + data.location_data.icons.emplace_back(MapIcon::CleanWater); + data.location_data.names.emplace_back().set_format("*{0}*", text); + + return true; + } + + bool parse_bonus_item(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int bonus_type_int; + if (!string_convert(parts[0], bonus_type_int)) { + return false; + } + + std::string bonus_item; + switch (bonus_type_int) { + case 30: + bonus_item = "Health Regeneration"; + break; + case 31: + bonus_item = "Energy Regeneration"; + break; + case 35: + bonus_item = "Extra Double Jump"; + break; + case 36: + bonus_item = "Extra Air Dash"; + break; + default: + return false; + } + + const auto line = concatenate_parts(parts); + + const auto item = std::make_shared>(); + item->variable.assign(core::api::uber_states::UberState(UberStateGroup::RandoUpgrade, bonus_type_int)); + item->value.set(1); + data.add_item(item); + + if (!data.should_add_default_messages) { + return true; + } + + const auto message = std::make_shared(); + set_location(message.get(), location); + message->should_add_to_recent = true; + message->info.text.set_format(R"(#{0}[if([state_int(4|{1})] > 1,<> x[state_int(4|{1})],)]#)", bonus_item, bonus_type_int); + data.add_item(message); + + data.location_data.icons.emplace_back(MapIcon::BonusItem); + data.location_data.names.emplace_back(message->info.text.get()); + + return true; + } + + std::string weapon_upgrade_name(const int id) { + switch (id) { + case 0: + return "#Rapid Hammer#"; + case 1: + return "#Rapid Sword#"; + case 2: + return "#Blaze Efficiency#"; + case 3: + return "#Spear Efficiency#"; + case 4: + return "#Shuriken Efficiency#"; + case 5: + return "#Sentry Efficiency#"; + case 6: + return "#Bow Efficiency#"; + case 7: + return "#Regeneration Efficiency#"; + case 8: + return "#Flash Efficiency#"; + case 9: + return "#Grenade Efficiency#"; + case 45: + return "#Exploding Spear#"; + case 46: + return "#Hammer Shockwave#"; + case 47: + return "#Static Shuriken#"; + case 48: + return "#Charge Blaze#"; + case 49: + return "#Sentry Speed#"; + default: + return "Unknown"; + } + } + + bool parse_weapon_upgrade(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int weapon_upgrade_int; + if (!string_convert(parts[0], weapon_upgrade_int)) { + return false; + } + + if (weapon_upgrade_int >= 0 && weapon_upgrade_int <= 9) { + const auto increment = std::make_shared>(); + increment->variable.assign(core::api::uber_states::UberState(4, 50 + weapon_upgrade_int)); + increment->value.set(1.f); + data.add_item(increment); + } + + const auto item = std::make_shared>(); + item->value.set(1.f); + switch (weapon_upgrade_int) { + case 0: + case 1: + item->variable.assign(core::api::uber_states::UberState(UberStateGroup::RandoUpgrade, weapon_upgrade_int)); + item->value.assign(core::set_get{ + [](auto value) {}, + [weapon_upgrade_int]() { return std::powf(1.25f, core::api::uber_states::UberState(4, 50 + weapon_upgrade_int).get()); }, + }); + break; + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + item->variable.assign(core::api::uber_states::UberState(UberStateGroup::RandoUpgrade, weapon_upgrade_int)); + item->value.assign(core::set_get{ + [](auto value) {}, + [weapon_upgrade_int]() { return std::powf(0.5f, core::api::uber_states::UberState(UberStateGroup::RandoUpgrade, 50 + weapon_upgrade_int).get()); }, + }); + break; + case 45: + item->variable.assign(core::api::uber_states::UberState(3440, 5687)); + break; + case 46: + item->variable.assign(core::api::uber_states::UberState(3440, 46488)); + break; + case 47: + item->variable.assign(core::api::uber_states::UberState(3440, 10776)); + break; + case 48: + item->variable.assign(core::api::uber_states::UberState(3440, 61898)); + break; + case 49: + item->variable.assign(core::api::uber_states::UberState(3440, 57376)); + break; + default: + return false; + } + + const auto line = concatenate_parts(parts); + data.add_item(item); + + if (!data.should_add_default_messages) { + return true; + } + + std::string name = weapon_upgrade_name(weapon_upgrade_int); + + const auto message = std::make_shared(); + message->should_add_to_recent = true; + message->info.text.set(name); + set_location(message.get(), location); + + data.add_item(message); + data.location_data.names.emplace_back(name); + // TODO: Maybe add specific icons for the vanilla opher upgrades? + data.location_data.icons.emplace_back(MapIcon::BonusItem); + + return true; + } + + bool parse_relic(location_type const& location, std::span parts, ParserData& data) { + if (parts.size() > 2) { + return false; + } + + data.data->relics.add(location, parts.size() == 2 ? std::optional(parts[1]) : std::nullopt); + + if (!data.should_add_default_messages) { + return true; + } + + const auto relic_name = std::format("@{} Relic@", std::string(data.data->relics.relic_name(location))); + const auto message = std::make_shared(); + message->should_add_to_recent = true; + message->info.text.set(relic_name); + + set_location(message.get(), location); + data.add_item(message); + data.location_data.names.emplace_back().set(relic_name); + data.location_data.icons.emplace_back(MapIcon::MapstonePickup); + + return true; + } + + /** + * \brief + * \param parts Parts array including the sys_message_type as parts[0] + * \param data + * \return + */ + bool parse_sys_message(const std::span parts, ParserData& data) { + int sys_message_type; + if (!string_convert(parts[0], sys_message_type)) { + return false; + } + + // TODO: RelicList, MapRelicList and GoalProgress should be implemented in headers/seedgen. + switch (sys_message_type) { + case 0: + // RelicList + // var rzs = RelicZones; + // if (rzs.Count == 0) + // return ""; + // int cur = 0; + // int tot = 0; + // var s = String.Join(", ", rzs.Select(z => { + // int totz = Counts[z]; + // int curz = UberGet.value(ZoneToId[z]).Byte; + // cur += curz; + // tot += totz; + // var w = ""; + // if (curz >= totz) + // w = "@"; + // else if (CurrentZone == z) + // w = "#"; + // return $"{w}{z}{w}"; + //})); + // var c = cur == tot ? "$" : ""; + // return $"{c}Relics ({cur}/{tot}):{c} {s}"; + return false; + case 1: + // MapRelicList + // var rzs = RelicZones; + // if (rzs.Count == 0) + // return ""; + // if (rzs.Contains(zone)) { + // if (UberGet.value(ZoneToId[zone]).Byte >= Counts[zone]) + // return " $(Relic found)$"; + // else + // return " (Relic not found)"; + // } + // return " (Relicless)"; + return false; + case 2: { + const auto message = std::make_shared(); + message->should_add_to_recent = true; + message->info.text.set("[state_int(6|2)]/[seed(pickup_count)]"); + data.add_item(message); + data.location_data.names.emplace_back(message->info.text); + break; + } + case 3: + // GoalProgress + // auto message = std::make_shared(); + // message->info.text = "[world(name)]"; + // message->info.should_save_as_last = true; + // data.items.push_back(message); + + // if (InterOp.Utils.get_game_state() != GameState.Game) + // return ""; // don't even try! + + // var goalMsgs = new List(); + // if (Flags.Contains(Flag.ALL_WISPS)) { + // var max = UberStateController.Wisps.Count; + // var amount = UberStateController.Wisps.Count((UberState s) = > s.GetValue().Bool); + // var w = amount == max ? met : unmet; + // goalMsgs.Add($ "{w}Wisps: {amount}/{max}{w}"); + // } + + // if (Flags.Contains(Flag.ALL_TREES)) { + // var amount = SaveController.TreeCount; + // var w = amount == 14 ? met : unmet; + // goalMsgs.Add($ "{w}Trees: {amount}/{14}{w}"); + // } + + // if (Flags.Contains(Flag.ALL_QUESTS)) { + // var max = UberStateController.Quests.Count; + // var amount = UberStateController.Quests.Count((UberState s) = > s.GetValue().Int == s.Value.Int); + // var w = amount == max ? met : unmet; + // goalMsgs.Add($ "{w}Quests: {amount}/{max}{w}"); + // } + + // var msg = String.Join(", ", goalMsgs); + // if (withRelics && Flags.Contains(Flag.RELICS)) + // msg += "\n" + Relic.RelicMessage(); + // return goalMsgs.Count > 0 ? (progress ? "\n" : "") + msg : msg; + // return SeedController.GoalModeMessages(withRelics + // : false); + return false; + case 4: { + std::vector pieces; + split_str(parts[1], pieces, ','); + if (pieces.size() % 2 != 0) { + return false; + } + + auto counter = std::make_shared(); + for (auto it = pieces.begin(); it != pieces.end(); it += 2) { + auto& target = counter->targets.emplace_back(); + core::api::uber_states::parse_condition(std::span(it, 2), target); + } + + data.add_item(counter); + data.location_data.names.emplace_back([](auto) {}, [counter]() { return counter->message_text(); }); + break; + } + case 5: { + // WorldName + const auto message = std::make_shared(); + message->info.text.set_format("[world({})]", parts[1]); + message->should_add_to_recent = true; + data.add_item(message); + data.location_data.names.emplace_back(message->info.text); + break; + } + default: + return false; + } + + return true; + } + + bool parse_wheel_name(std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + int wheel; + int item; + if (!string_convert(parts[0], wheel) || !string_convert(parts[1], item)) { + return false; + } + + const auto caller = std::make_shared(); + auto& name = parts[2]; + caller->description = std::format("wheel item {}:{} set name {}", wheel, item, name); + caller->func = [wheel, item, name]() { features::wheel::set_wheel_item_name(wheel, item, name); }; + + data.add_item(caller); + return true; + } + + bool parse_wheel_description(std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + int wheel; + int item; + if (!string_convert(parts[0], wheel) || !string_convert(parts[1], item)) { + return false; + } + + const auto caller = std::make_shared(); + auto& description = parts[2]; + caller->description = std::format("wheel item {}:{} set description {}", wheel, item, description); + caller->func = [wheel, item, description]() { features::wheel::set_wheel_item_description(wheel, item, description); }; + + data.add_item(caller); + return true; + } + + bool parse_wheel_texture(std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + int wheel; + int item; + if (!string_convert(parts[0], wheel) || !string_convert(parts[1], item)) { + return false; + } + + const auto caller = std::make_shared(); + auto& texture = parts[2]; + caller->description = std::format("wheel item {}:{} set texture {}", wheel, item, texture); + caller->func = [wheel, item, texture]() { features::wheel::set_wheel_item_texture(wheel, item, texture); }; + + data.add_item(caller); + return true; + } + + bool parse_wheel_color(std::span parts, ParserData& data) { + if (parts.size() != 6) { + return false; + } + + int wheel; + int item; + app::Color color{}; + if (!string_convert(parts[0], wheel) || !string_convert(parts[1], item) || !string_convert(parts[2], color.r) || !string_convert(parts[3], color.g) || + !string_convert(parts[4], color.b) || !string_convert(parts[5], color.a)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = std::format("wheel item {}:{} set color ({}, {}, {}, {})", wheel, item, color.r, color.g, color.b, color.a); + caller->func = [wheel, item, color]() { + features::wheel::set_wheel_item_color( + wheel, item, static_cast(color.r), static_cast(color.g), static_cast(color.b), static_cast(color.a) + ); + }; + + data.add_item(caller); + return true; + } + + bool parse_wheel_action(location_type const& location, std::span parts, ParserData& data) { + // NOLINT + if (parts.size() <= 3) { + return false; + } + + int wheel; + int item; + int binding; + if (!string_convert(parts[0], wheel) || !string_convert(parts[1], item) || !string_convert(parts[2], binding)) { + return false; + } + + auto binding_type = static_cast(binding - 1); + if (binding < 0 || binding > 2) { + return false; + } + + auto procedure_id = data.next_procedure_id++; + auto& procedure = data.data->procedures[procedure_id]; + ParserData procedure_data{ + .data = data.data, + .location_data = procedure, + .items_added = 0, + .next_location_id = data.next_location_id, + .next_procedure_id = data.next_procedure_id, + .should_add_to_always_granted = false, + .should_add_default_messages = parts.back() != "mute" + }; + + const auto action_parts = std::span(parts.begin() + 3, parts.end()); + if (!parse_action(location, action_parts, procedure_data)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = std::format("wheel item {}:{} set action '{}'", wheel, item, concatenate_parts(action_parts)); + caller->func = [wheel, item, binding_type, procedure_id]() { + if (static_cast(binding_type) == -1) { + features::wheel::set_wheel_item_callback(wheel, item, app::SpellInventory_Binding__Enum::ButtonX, [procedure_id](auto, auto, auto) { + game_seed().procedure_call(procedure_id); + }); + features::wheel::set_wheel_item_callback(wheel, item, app::SpellInventory_Binding__Enum::ButtonY, [procedure_id](auto, auto, auto) { + game_seed().procedure_call(procedure_id); + }); + features::wheel::set_wheel_item_callback(wheel, item, app::SpellInventory_Binding__Enum::ButtonB, [procedure_id](auto, auto, auto) { + game_seed().procedure_call(procedure_id); + }); + } else { + features::wheel::set_wheel_item_callback(wheel, item, binding_type, [procedure_id](auto, auto, auto) { + game_seed().procedure_call(procedure_id); + }); + } + }; + + modloader::ScopedSetter scoped(data.should_add_to_always_granted, false); + data.add_item(caller); + return true; + } + + bool parse_wheel_sticky(std::span parts, ParserData& data) { + if (parts.size() != 2) { + return false; + } + + int wheel; + bool sticky; + if (!string_convert(parts[0], wheel) || !string_convert(parts[1], sticky)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = std::format("wheel {} set sticky {}", wheel, sticky); + caller->func = [wheel, sticky]() { features::wheel::set_wheel_sticky(wheel, sticky); }; + + data.add_item(caller); + return true; + } + + bool parse_wheel_active(std::span parts, ParserData& data) { + if (parts.size() != 1) { + return false; + } + + int wheel; + if (!string_convert(parts[0], wheel)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = std::format("wheel {} set active", wheel); + caller->func = [wheel]() { features::wheel::set_active_wheel(wheel); }; + + data.add_item(caller); + return true; + } + + bool parse_wheel_clear_item(std::span parts, ParserData& data) { + if (parts.size() != 2) { + return false; + } + + int wheel; + int item; + if (!string_convert(parts[0], wheel) || !string_convert(parts[1], item)) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = std::format("wheel {}:{} clear entry", wheel, item); + caller->func = [wheel, item]() { features::wheel::clear_wheel_item(wheel, item); }; + + data.add_item(caller); + return true; + } + + bool parse_wheel_clear_all(std::span parts, ParserData& data) { + if (!parts.empty()) { + return false; + } + + const auto caller = std::make_shared(); + caller->description = "wheel clear all"; + caller->func = []() { features::wheel::clear_wheels(); }; + + data.add_item(caller); + return true; + } + + bool parse_wheel(location_type const& location, std::span parts, ParserData& data) { + // NOLINT + if (parts.empty()) { + return false; + } + + int wheel_type; + if (!string_convert(parts[0], wheel_type)) { + return false; + } + + const auto next_parts = std::span(parts.begin() + 1, parts.end()); + switch (wheel_type) { + case 0: + return parse_wheel_name(next_parts, data); + case 1: + return parse_wheel_description(next_parts, data); + case 2: + return parse_wheel_texture(next_parts, data); + case 3: + return parse_wheel_color(next_parts, data); + case 4: + return parse_wheel_action(location, next_parts, data); + case 5: + return parse_wheel_sticky(next_parts, data); + case 6: + return parse_wheel_active(next_parts, data); + case 7: + return parse_wheel_clear_item(next_parts, data); + case 8: + return parse_wheel_clear_all(next_parts, data); + default: + break; + } + + return false; + } + + bool parse_shop_icon(std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + int group; + int state; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state)) { + return false; + } + + auto icon = parts[2]; + auto shop_state = core::api::uber_states::UberState(group, state); + const auto caller = std::make_shared(); + caller->description = std::format("shop '{}' set icon {}", shop_state.to_string(), icon); + caller->func = [shop_state, icon]() { + if (const auto slot = game::shops::shop_slot_from_state(shop_state); slot != nullptr) { + slot->normal.icon = core::api::graphics::textures::get_texture(icon); + slot->locked.icon = slot->normal.icon; + } + }; + + data.add_item(caller); + return true; + } + + bool parse_shop_title(std::span parts, ParserData& data) { + if (parts.size() < 2 || parts.size() > 3) { + return false; + } + + int group; + int state; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state)) { + return false; + } + + auto title = parts.size() == 3 ? parts[2] : " "; + auto shop_state = core::api::uber_states::UberState(group, state); + const auto caller = std::make_shared(); + caller->description = std::format("shop '{}' set title {}", shop_state.to_string(), title); + caller->func = [shop_state, title]() { + if (const auto slot = game::shops::shop_slot_from_state(shop_state); slot != nullptr) { + slot->normal.name.set(title); + slot->locked.name.set(title); + slot->hidden.name.set(title); + } + }; + + data.add_item(caller); + return true; + } + + bool parse_shop_description(std::span parts, ParserData& data) { + if (parts.size() < 2 || parts.size() > 3) { + return false; + } + + int group; + int state; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state)) { + return false; + } + + auto description = parts.size() == 3 ? parts[2] : " "; + auto shop_state = core::api::uber_states::UberState(group, state); + const auto caller = std::make_shared(); + caller->description = std::format("shop '{}' set description {}", shop_state.to_string(), description); + caller->func = [shop_state, description]() { + if (const auto slot = game::shops::shop_slot_from_state(shop_state); slot != nullptr) { + slot->normal.description.set(description); + } + }; + + data.add_item(caller); + return true; + } + + bool parse_shop_locked(std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + int group; + int state; + bool locked; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state) || !string_convert(parts[2], locked)) { + return false; + } + + auto shop_state = core::api::uber_states::UberState(group, state); + const auto caller = std::make_shared(); + caller->description = std::format("shop '{}' set locked {}", shop_state.to_string(), locked); + caller->func = [shop_state, locked]() { + if (const auto slot = game::shops::shop_slot_from_state(shop_state); slot != nullptr && slot->visibility != game::shops::SlotVisibility::Hidden) { + slot->visibility = locked ? game::shops::SlotVisibility::Locked : game::shops::SlotVisibility::Visible; + } + }; + + data.add_item(caller); + return true; + } + + bool parse_shop_visible(std::span parts, ParserData& data) { + if (parts.size() != 3) { + return false; + } + + int group; + int state; + bool visible; + if (!string_convert(parts[0], group) || !string_convert(parts[1], state) || !string_convert(parts[2], visible)) { + return false; + } + + auto shop_state = core::api::uber_states::UberState(group, state); + const auto caller = std::make_shared(); + caller->description = std::format("shop '{}' set visible {}", shop_state.to_string(), visible); + caller->func = [shop_state, visible]() { + if (const auto slot = game::shops::shop_slot_from_state(shop_state); slot != nullptr) { + slot->visibility = visible ? game::shops::SlotVisibility::Visible : game::shops::SlotVisibility::Hidden; + } + }; + + data.add_item(caller); + return true; + } + + bool parse_shop(std::span parts, ParserData& data) { + if (parts.empty()) { + return false; + } + + int wheel_type; + if (!string_convert(parts[0], wheel_type)) { + return false; + } + + const auto next_parts = std::span(parts.begin() + 1, parts.end()); + switch (wheel_type) { + case 0: + return parse_shop_icon(next_parts, data); + case 1: + return parse_shop_title(next_parts, data); + case 2: + return parse_shop_description(next_parts, data); + case 3: + return parse_shop_locked(next_parts, data); + case 4: + return parse_shop_visible(next_parts, data); + default: + break; + } + + return false; + } + + bool parse_map_message(std::span parts, ParserData& data) { + const auto map_message = std::make_shared(); + map_message->message = std::accumulate(parts.begin(), parts.end(), std::string(), [](auto const& ss, auto const& s) -> decltype(auto) { + return ss.empty() ? s : ss + "|" + s; + }); + + data.add_item(map_message); + return true; + } + + bool parse_action(location_type const& location, std::span parts, ParserData& data) { + // NOLINT(*-no-recursion) + // NOLINT + if (parts.empty()) { + return false; + } + + int action_type_int; + if (!string_convert(parts[0], action_type_int)) { + return false; + } + + const auto action_type = static_cast(action_type_int); + const auto next_parts = std::span(parts.begin() + 1, parts.end()); + switch (action_type) { + case ActionType::SpiritLight: + return parse_spirit_light(location, next_parts, data); + case ActionType::Resource: + return parse_resource(location, next_parts, data); + case ActionType::Ability: + return parse_ability(location, next_parts, data); + case ActionType::Shard: + return parse_shard(location, next_parts, data); + case ActionType::SystemCommand: + return parse_sys_command(location, next_parts, data); + case ActionType::Teleporter: + return parse_teleporter(location, next_parts, data); + case ActionType::Message: + return parse_message(location, next_parts, data); + case ActionType::UberState: + return parse_uber_state(next_parts, data); + case ActionType::QuestEvent: + return parse_quest_event(location, next_parts, data); + case ActionType::BonusItem: + return parse_bonus_item(location, next_parts, data); + case ActionType::WeaponUpgrade: + return parse_weapon_upgrade(location, next_parts, data); + case ActionType::Relic: + return parse_relic(location, next_parts, data); + case ActionType::SysMessage: + return parse_sys_message(next_parts, data); + case ActionType::Wheel: + return parse_wheel(location, next_parts, data); + case ActionType::Shop: + return parse_shop(next_parts, data); + case ActionType::SetMapMessage: + return parse_map_message(next_parts, data); + default: + return false; + } + } + + bool parse_expression(int& next_location_id, int& next_procedure_id, std::span parts, std::shared_ptr data, int line_number) { + if (parts.size() < 3) { + // Need at least the trigger and an action type. + return false; + } + + bool should_always_grant = false; + core::api::uber_states::UberStateCondition trigger; + if (!parse_condition(std::span(parts.begin(), parts.begin() + 2), trigger, &should_always_grant)) { + return false; + } + + auto& location_item_data = data->locations[trigger.state][trigger]; + ParserData parser_data{ + .data = data, + .location_data = location_item_data, + .items_added = 0, + .next_location_id = next_location_id, + .next_procedure_id = next_procedure_id, + .should_add_to_always_granted = should_always_grant, + .should_add_default_messages = parts.back() != "mute", + .line_number = line_number, + .item_definition = std::string(concatenate_parts(parts)) + }; + + const int parts_end_offset = parser_data.should_add_default_messages ? 0 : 1; + return parse_action(trigger, std::span(parts.begin() + 2, parts.end() - parts_end_offset), parser_data); + } + + /** + * \brief Check if a line is a config line, and if yes, parse its contents into the seed data + * \param line The current line as raw string (escape sequences have NOT been replaced yet) + * \param data Seed data + */ + void parse_config(std::string_view line, std::shared_ptr data) { + if (line.starts_with("// This World:")) { + const std::string str(line.substr(14)); + data->info.meta.world_index = std::stoi(str); + } else if (line.starts_with("// Format Version:")) { + } else if (line.starts_with("// Generator Version:")) { + } else if (line.starts_with("// Slug:")) { + data->info.meta.slug = trim_copy(line.substr(sizeof("// Slug:"))); + } + + // If we don't match anything here it's a comment, and we can ignore it. + } + + bool is_seed_version_supported(semver::version version) { + return semver::range::satisfies(version, ">=1.0.0 <=1.0.0"); + } + + std::variant parse_meta_data(const std::string_view content) { + std::istringstream seed_file(content.data()); + + std::optional seedgen_config; + Seed::SeedMetaData meta; + std::string line; + while (std::getline(seed_file, line)) { + if (line.starts_with("// Format Version: ")) { + const auto version = trim_copy(line.substr(19)); + const auto parsed = semver::from_string_noexcept(version); + if (parsed.has_value()) { + meta.version = parsed.value(); + } + + if (!is_seed_version_supported(meta.version)) { + return ParserError::WrongVersion; + } + } else if (line.starts_with("// This World:")) { + const std::string str(line.substr(14)); + meta.world_index = std::stoi(str); + } else if (line.starts_with("// Slug:")) { + meta.slug = trim_copy(line.substr(sizeof("// Slug:"))); + } else if (line.starts_with("// Config:")) { + seedgen_config = nlohmann::json::parse(line.begin() + sizeof("// Config:"), line.end()); + } else { + // Remove comments. + line = line.substr(0, line.find("//")); + + if (line.starts_with("Flags:")) { + split_str(line.substr(6), meta.flags, ','); + for (auto& flag: meta.flags) { + trim(flag); + } + } else if (line.starts_with("Spawn:")) { + std::vector coords; + split_str(line.substr(6), coords, ','); + if (coords.size() != 2) { + continue; + } + + app::Vector3 position{}; + if (!string_convert(coords[0], position.x) || !string_convert(coords[1], position.y)) { + continue; + } + + meta.start_position = position; + } + } + } + + if (seedgen_config.has_value()) { + meta.intended_difficulty = (*seedgen_config)["worldSettings"][meta.world_index].value("hard", false) + ? app::GameController_GameDifficultyModes__Enum::Hard + : app::GameController_GameDifficultyModes__Enum::Normal; + } + + return meta; + } + + bool parse(const std::string_view content, location_data::LocationCollection const& location_data, std::shared_ptr data) { + std::istringstream seed_file(content.data()); + + current_location_data = &location_data; + int next_location_id = 0; + int next_procedure_id = 0; + int line_number = 0; + std::optional seedgen_config; + + std::string line; + while (std::getline(seed_file, line)) { + if (line.starts_with("// Format Version: ")) { + const auto version = trim_copy(line.substr(19)); + const auto parsed = semver::from_string_noexcept(version); + if (parsed.has_value()) { + data->info.meta.version = parsed.value(); + } + + break; + } + } + + if (!is_seed_version_supported(data->info.meta.version)) { + modloader::warn("legacy_seed_parser", "Failed to load seed due to incompatible version"); + data->info.parser_error = std::format("Failed to load seed\ndue to version incompatibility"); + return false; + } + + seed_file.seekg(0); + while (std::getline(seed_file, line)) { + data->info.content += line; + data->info.content += "\n"; + + // Config needs to do its own escape handling (\n, \t etc) + if (line.starts_with("//")) { + if (line.starts_with("// Config:")) { + seedgen_config = nlohmann::json::parse(line.begin() + sizeof("// Config:"), line.end()); + } else { + parse_config(line, data); + } + + ++line_number; + continue; + } + + replace_all(line, "\\n", "\n"); + replace_all(line, "\\t", "\t"); + + // Remove comments. + line = line.substr(0, line.find("//")); + if (line.starts_with("Flags:")) { + split_str(line.substr(6), data->info.meta.flags, ','); + for (auto& flag: data->info.meta.flags) { + trim(flag); + } + } else if (line.starts_with("Spawn:")) { + std::vector coords; + split_str(line.substr(6), coords, ','); + if (coords.size() != 2) { + ++line_number; + continue; + } + + app::Vector3 position{}; + if (!string_convert(coords[0], position.x) || !string_convert(coords[1], position.y)) { + ++line_number; + continue; + } + + data->info.meta.start_position = position; + } else if (line.starts_with("timer:")) { + std::vector parts; + auto timer_states = line.substr(6); + trim(timer_states); + parse_parts(timer_states, parts); + + if (parts.size() != 4) { + ++line_number; + continue; + } + + int toggle_group; + int toggle_state; + int value_group; + int value_state; + + if ( + !string_convert(parts[0], toggle_group) || + !string_convert(parts[1], toggle_state) || + !string_convert(parts[2], value_group) || + !string_convert(parts[3], value_state) + ) { + ++line_number; + continue; + } + + data->timers.push_back({ + {toggle_group, toggle_state}, + {value_group, value_state} + }); + + } else if (!line.empty()) { + std::vector parts; + parse_parts(line, parts); + parse_expression(next_location_id, next_procedure_id, std::span(parts.begin(), parts.end()), data, line_number); + } + + ++line_number; + } + + if (seedgen_config.has_value()) { + data->info.meta.intended_difficulty = (*seedgen_config)["worldSettings"][data->info.meta.world_index].value("hard", false) + ? app::GameController_GameDifficultyModes__Enum::Hard + : app::GameController_GameDifficultyModes__Enum::Normal; + } + + return true; + } + + std::optional parse_action(const std::string_view action) { + auto data = std::make_shared(); + const location_type location{ + {}, + BooleanOperator::Equals, + 0, + }; + + ItemData& location_data = data->locations[location.state][location]; + int next_location_id = 0; + int next_procedure_id = 0; + + std::vector parts; + parse_parts(action, parts); + + ParserData parser_data{ + .data = data, + .location_data = location_data, + .items_added = 0, + .next_location_id = next_location_id, + .next_procedure_id = next_procedure_id, + .should_add_to_always_granted = false, + .should_add_default_messages = parts.back() != "mute", + .line_number = 0, + .item_definition = std::string(action) + }; + + core::reactivity::ScopedReactivityBlocker blocker; + return parse_action(location, parts, parser_data) ? std::optional(location_data) : std::nullopt; + } +} // namespace randomizer::seed::legacy_parser diff --git a/projects/Randomizer/tracking/game_tracker.cpp b/projects/Randomizer/tracking/game_tracker.cpp index 91f53159c7e..9044c1c0237 100644 --- a/projects/Randomizer/tracking/game_tracker.cpp +++ b/projects/Randomizer/tracking/game_tracker.cpp @@ -81,7 +81,7 @@ namespace randomizer::timing { } bool timer_should_run() { - return loaded_any_save_file && !game_finished; + return loaded_any_save_file && !game_finished && !timer_should_pause(); } namespace { diff --git a/projects/Randomizer/uber_states/uber_state_initialization.cpp b/projects/Randomizer/uber_states/uber_state_initialization.cpp index 5f9f211a97a..fb8bf36b3e8 100644 --- a/projects/Randomizer/uber_states/uber_state_initialization.cpp +++ b/projects/Randomizer/uber_states/uber_state_initialization.cpp @@ -388,6 +388,8 @@ namespace randomizer { add_state(UberStateGroup::RandoConfig, "allowTeleportingUnderwater", 18, false), add_state(UberStateGroup::RandoConfig, "allowTeleportingDuringCombatShrineFights", 19, false), add_state(UberStateGroup::RandoConfig, "disableWillowHeartCutscenes", 20, false), + add_state(UberStateGroup::RandoConfig, "allowTeleportingAtRuinsMapstone", 21, false), + add_state(UberStateGroup::RandoConfig, "keepShriekHealthBarDuringEscape", 22, false), add_state(UberStateGroup::RandoConfig, "removeShriekEscapeSand", 100, false), add_state(UberStateGroup::RandoConfig, "removeFeedingGroundsToElevatorSand", 101, false), add_state(UberStateGroup::RandoConfig, "knockKnockWellspring", 102, false), diff --git a/projects/Randomizer/ui/main_menu_seed_info.cpp b/projects/Randomizer/ui/main_menu_seed_info.cpp index c24cce37a80..d6eb62d0551 100644 --- a/projects/Randomizer/ui/main_menu_seed_info.cpp +++ b/projects/Randomizer/ui/main_menu_seed_info.cpp @@ -9,18 +9,20 @@ #include #include #include +#include +#include #include #include +#include +#include #include +#include #include #include #include #include #include #include -#include -#include -#include #include #include #include @@ -28,11 +30,10 @@ #include #include #include +#include #include #include #include -#include -#include using namespace utils; using namespace app::classes; @@ -444,22 +445,11 @@ namespace randomizer::main_menu_seed_info { if (save_file_exists) { on_seed_loaded_handle = nullptr; - const auto save_controller = core::api::game::save_controller(); - - app::Byte__Array* data; - - if (Grdk::Wrapper::get_InitializedOk()) { // Handle GRDK (Xbox live) saves - data = Grdk::Wrapper::Load_1(index, -1); - } else { - const auto save_info = SaveGameController::GetSaveFileInfo(save_controller, index, -1); - data = System::IO::File::ReadAllBytes(save_info->fields.m_FullSaveFilePath); - } - auto seed_meta_data = std::make_shared(); auto seed_archive_data = std::make_shared(); const auto read_slots = core::save_meta::read_save_meta_slots_from_byte_array( - data, + core::api::save_files::get_byte_array(index), { {SaveMetaSlot::SeedMetaData, seed_meta_data}, {SaveMetaSlot::SeedArchiveData, seed_archive_data}, diff --git a/vcpkg.json b/vcpkg.json index 9f37dd1474a..54202f94faa 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -4,7 +4,7 @@ "builtin-baseline" : "cd5e746ec203c8c3c61647e0886a8df8c1e78e41", "dependencies" : [ { "name" : "stb", - "version>=" : "2023-04-11#1" + "version>=" : "2024-07-29#1" }, { "name" : "ixwebsocket", "version>=" : "11.4.5#0", @@ -34,7 +34,7 @@ "features": ["zstd"] }, { "name" : "libremidi", - "version>=" : "4.3.0" + "version>=" : "4.5.0" }, { "name" : "magic-enum", "version>=" : "0.9.5" @@ -46,12 +46,15 @@ "version>=" : "3.11.3" }, { "name" : "openssl", - "version>=" : "3.3.0" + "version>=" : "3.4.0" }, { "name" : "protobuf", - "version>=" : "3.21.12#3" + "version>=" : "3.21.12#4" }, { "name" : "sdl2", - "version>=" : "2.30.3" + "version>=" : "2.30.8" + }, { + "name" : "stduuid", + "version>=" : "1.2.3" } ] }