diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp b/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp index 8e75b13e56a2..c54ad73e9e16 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp +++ b/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp @@ -74,7 +74,15 @@ Configuration::Configuration(const std::string& json_file) specification = static_cast(spec_from_json); } - if (specification != 1) + if (specification == 1) + { + m_valid = ProcessSpecificationV1(root, m_dynamic_input_textures, m_base_path, json_file); + } + else if (specification == 2) + { + m_valid = ProcessSpecificationV2(root, m_dynamic_input_textures, m_base_path, json_file); + } + else { ERROR_LOG_FMT(VIDEO, "Failed to load dynamic input json file '{}', specification '{}' is invalid", @@ -82,8 +90,6 @@ Configuration::Configuration(const std::string& json_file) m_valid = false; return; } - - m_valid = ProcessSpecificationV1(root, m_dynamic_input_textures, m_base_path, json_file); } Configuration::~Configuration() = default; @@ -111,33 +117,56 @@ bool Configuration::GenerateTexture(const IniFile& file, auto image_to_write = original_image; bool dirty = false; - for (auto& emulated_entry : emulated_controls_iter->second) + for (const auto& controller_name : controller_names) { - /*auto apply_original = [&] { - CopyImageRegion(*original_image, *image_to_write, emulated_entry.m_region, - emulated_entry.m_region); - dirty = true; - }; + auto* sec = file.GetSection(controller_name); + if (!sec) + { + continue; + } - if (!device_found) + std::string device_name; + if (!sec->Get("Device", &device_name)) { - // If we get here, that means the controller is set to a - // device not exposed to the pack - // We still apply the original image, in case the user - // switched devices and wants to see the changes - apply_original(); continue; - }*/ + } - if (ApplyEmulatedEntry(host_devices_iter->second, emulated_entry, sec, *image_to_write, texture_data.m_preserve_aspect_ratio)) + auto emulated_controls_iter = texture_data.m_emulated_controllers.find(controller_name); + if (emulated_controls_iter == texture_data.m_emulated_controllers.end()) { - dirty = true; + continue; + } + + bool device_found = true; + auto host_devices_iter = texture_data.m_host_devices.find(device_name); + if (host_devices_iter == texture_data.m_host_devices.end()) + { + // If we fail to find our exact device, + // it's possible the creator doesn't care (single player game) + // and has used a wildcard for any device + host_devices_iter = texture_data.m_host_devices.find(""); + + if (host_devices_iter == texture_data.m_host_devices.end()) + { + device_found = false; + } } - for (auto& [emulated_key, rects] : emulated_controls_iter->second) + for (auto& emulated_entry : emulated_controls_iter->second) { - dirty = true; - //apply_original(); + if (!device_found) + { + // If we get here, that means the controller is set to a + // device not exposed to the pack + dirty = true; + continue; + } + + if (ApplyEmulatedEntry(host_devices_iter->second, emulated_entry, sec, *image_to_write, + texture_data.m_preserve_aspect_ratio)) + { + dirty = true; + } } } @@ -171,21 +200,20 @@ bool Configuration::ApplyEmulatedEntry(const Configuration::HostEntries& host_en ImagePixelData& image_to_write, bool preserve_aspect_ratio) const { - return std::visit( - overloaded{ - [&, this](const Data::EmulatedSingleEntry& entry) { - std::string host_key; - section->Get(entry.m_key, &host_key); - return ApplyEmulatedSingleEntry(host_entries, std::vector{host_key}, - entry.m_tag, entry.m_region, image_to_write, - preserve_aspect_ratio); - }, - [&, this](const Data::EmulatedMultiEntry& entry) { - return ApplyEmulatedMultiEntry(host_entries, entry, section, image_to_write, - preserve_aspect_ratio); - }, - }, - emulated_entry); + return std::visit(overloaded{ + [&, this](const Data::EmulatedSingleEntry& entry) { + std::string host_key; + section->Get(entry.m_key, &host_key); + return ApplyEmulatedSingleEntry( + host_entries, std::vector{host_key}, entry.m_tag, + entry.m_region, image_to_write, preserve_aspect_ratio); + }, + [&, this](const Data::EmulatedMultiEntry& entry) { + return ApplyEmulatedMultiEntry(host_entries, entry, section, + image_to_write, preserve_aspect_ratio); + }, + }, + emulated_entry); } bool Configuration::ApplyEmulatedSingleEntry(const Configuration::HostEntries& host_entries, diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp index 3bc88e6246f6..e8071fab0cb3 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp +++ b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp @@ -13,6 +13,178 @@ namespace InputCommon::DynamicInputTextures { +namespace +{ +template +std::optional GetJsonValueFromMap(const picojson::object& obj, const std::string& name, + const std::string& json_file, bool required = true) +{ + auto iter = obj.find(name); + if (iter == obj.end()) + { + if (required) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because required field " + "'{}' is missing", + json_file, name); + } + return std::nullopt; + } + + if (!iter->second.is()) + { + if (required) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because required field " + "'{}' is the incorrect type '{}'", + json_file, name, typeid(T).name()); + } + return std::nullopt; + } + + return iter->second.get(); +} + +std::optional GetRect(const picojson::object& obj, const std::string& name, + const std::string& json_file) +{ + const auto rect_json_array = GetJsonValueFromMap(obj, name, json_file, true); + + if (!rect_json_array) + return std::nullopt; + + const picojson::array& rect_region_json_array = *rect_json_array; + if (rect_region_json_array.size() != 4) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because rect '{}' " + "does not have 4 offsets (left, top, right, bottom).", + json_file, name); + return std::nullopt; + } + + if (!std::all_of(rect_region_json_array.begin(), rect_region_json_array.end(), + [](picojson::value val) { return val.is(); })) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because rect '{}' " + "has an offset with the incorrect type.", + json_file, name); + return std::nullopt; + } + + Rect r; + r.left = static_cast(rect_region_json_array[0].get()); + r.top = static_cast(rect_region_json_array[1].get()); + r.right = static_cast(rect_region_json_array[2].get()); + r.bottom = static_cast(rect_region_json_array[3].get()); + return r; +} + +std::optional GetEmulatedEntry(const picojson::object& entry_obj, + std::string json_file) +{ + auto bind_type = GetJsonValueFromMap(entry_obj, "bind_type", json_file, false); + if (!bind_type) + bind_type = "single"; + + if (*bind_type == "single") + { + Data::EmulatedSingleEntry entry; + const auto entry_key = GetJsonValueFromMap(entry_obj, "key", json_file); + if (!entry_key) + return std::nullopt; + entry.m_key = *entry_key; + entry.m_tag = GetJsonValueFromMap(entry_obj, "tag", json_file, false); + + const auto entry_region = GetRect(entry_obj, "region", json_file); + if (!entry_region) + return std::nullopt; + entry.m_region = *entry_region; + + return entry; + } + else if (*bind_type == "multi") + { + Data::EmulatedMultiEntry entry; + const auto tag = GetJsonValueFromMap(entry_obj, "tag", json_file); + if (!tag) + return std::nullopt; + entry.m_combined_tag = *tag; + + const auto sub_entries_json = + GetJsonValueFromMap(entry_obj, "sub_entries", json_file); + if (!sub_entries_json) + return std::nullopt; + + for (auto& sub_entry_json : *sub_entries_json) + { + if (!sub_entry_json.is()) + { + return std::nullopt; + } + + const auto sub_entry = GetEmulatedEntry(sub_entry_json.get(), json_file); + if (!sub_entry) + { + return std::nullopt; + } + + entry.m_sub_entries.push_back(*sub_entry); + } + + const auto entry_region = GetRect(entry_obj, "region", json_file); + if (!entry_region) + return std::nullopt; + entry.m_combined_region = *entry_region; + + return entry; + } + + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because required field " + "'bind_type' had invalid value '{}'", + json_file, *bind_type); + + return std::nullopt; +} + +std::optional GetHostEntry(const picojson::object& entry_obj, + std::string json_file) +{ + const auto keys_json_array = GetJsonValueFromMap(entry_obj, "keys", json_file); + if (!keys_json_array) + return std::nullopt; + + const picojson::array& keys_json_array_value = *keys_json_array; + if (!std::all_of(keys_json_array_value.begin(), keys_json_array_value.end(), + [](picojson::value val) { return val.is(); })) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because 'keys' " + "has an invalid typed entry.", + json_file); + return std::nullopt; + } + + Data::HostEntry entry; + for (const auto& key : keys_json_array_value) + { + entry.m_keys.push_back(key.get()); + } + + entry.m_tag = GetJsonValueFromMap(entry_obj, "tag", json_file, false); + + const auto path = GetJsonValueFromMap(entry_obj, "image", json_file); + if (!path) + return std::nullopt; + entry.m_path = *path; + + return entry; +} +} // namespace bool ProcessSpecificationV1(picojson::value& root, std::vector& input_textures, const std::string& base_path, const std::string& json_file) { @@ -199,4 +371,165 @@ bool ProcessSpecificationV1(picojson::value& root, std::vector& input_text return true; } + +bool ProcessSpecificationV2(picojson::value& root, std::vector& input_textures, + const std::string& base_path, const std::string& json_file) +{ + const picojson::value& output_textures_json = root.get("output_textures"); + if (!output_textures_json.is()) + { + ERROR_LOG_FMT( + VIDEO, + "Failed to load dynamic input json file '{}' because 'output_textures' is missing or " + "was not of type object", + json_file); + return false; + } + + const picojson::value& preserve_aspect_ratio_json = root.get("preserve_aspect_ratio"); + + bool preserve_aspect_ratio = true; + if (preserve_aspect_ratio_json.is()) + { + preserve_aspect_ratio = preserve_aspect_ratio_json.get(); + } + + const picojson::value& generated_folder_name_json = root.get("generated_folder_name"); + + const std::string& game_id = SConfig::GetInstance().GetGameID(); + std::string generated_folder_name = fmt::format("{}_Generated", game_id); + if (generated_folder_name_json.is()) + { + generated_folder_name = generated_folder_name_json.get(); + } + + const picojson::value& default_host_controls_json = root.get("default_host_controls"); + picojson::object default_host_controls; + if (default_host_controls_json.is()) + { + default_host_controls = default_host_controls_json.get(); + } + + const auto output_textures = output_textures_json.get(); + for (auto& [name, data] : output_textures) + { + Data texture_data; + texture_data.m_hires_texture_name = name; + + // Required fields + const picojson::value& image = data.get("image"); + const picojson::value& emulated_controls = data.get("emulated_controls"); + + if (!image.is() || !emulated_controls.is()) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because required fields " + "'image', or 'emulated_controls' are either " + "missing or the incorrect type", + json_file); + return false; + } + + texture_data.m_image_name = image.to_str(); + texture_data.m_preserve_aspect_ratio = preserve_aspect_ratio; + texture_data.m_generated_folder_name = generated_folder_name; + + const std::string image_full_path = base_path + texture_data.m_image_name; + if (!File::Exists(image_full_path)) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because the image '{}' " + "could not be loaded", + json_file, image_full_path); + return false; + } + + const auto& emulated_controls_json = emulated_controls.get(); + for (auto& [emulated_controller_name, arr] : emulated_controls_json) + { + if (!arr.is()) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because 'emulated_controls' " + "map key '{}' is incorrect type. Expected array", + json_file, emulated_controller_name); + return false; + } + + auto& entries_vector = texture_data.m_emulated_controllers[emulated_controller_name]; + for (auto& entry_json : arr.get()) + { + if (!entry_json.is()) + { + ERROR_LOG_FMT( + VIDEO, + "Failed to load dynamic input json file '{}' because 'emulated_controls' " + "map key '{}' has an array entry that is an incorrect type. Expected object", + json_file, emulated_controller_name); + return false; + } + const auto entry = GetEmulatedEntry(entry_json.get(), json_file); + if (!entry) + { + return false; + } + entries_vector.push_back(*entry); + } + } + + // Default to the default controls but overwrite if the creator + // has provided something specific + picojson::object host_controls = default_host_controls; + const picojson::value& host_controls_json = data.get("host_controls"); + if (host_controls_json.is()) + { + host_controls = host_controls_json.get(); + } + + if (host_controls.empty()) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because field " + "'host_controls' is missing ", + json_file); + return false; + } + + for (auto& [host_device, arr] : host_controls) + { + if (!arr.is()) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because 'host_controls' " + "map key '{}' is incorrect type. Expected array", + json_file, host_device); + return false; + } + + auto& host_control_entries = texture_data.m_host_devices[host_device]; + for (auto& entry_json : arr.get()) + { + if (!entry_json.is()) + { + ERROR_LOG_FMT( + VIDEO, + "Failed to load dynamic input json file '{}' because 'host_controls' " + "map key '{}' has an array entry that is an incorrect type. Expected object", + json_file, host_device); + return false; + } + const auto entry = GetHostEntry(entry_json.get(), json_file); + if (!entry) + { + return false; + } + host_control_entries.push_back(*entry); + } + } + + input_textures.emplace_back(std::move(texture_data)); + } + + return true; +} } // namespace InputCommon::DynamicInputTextures diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h index 2a04ab81d7b5..a5ef24839323 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h +++ b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h @@ -15,4 +15,7 @@ namespace InputCommon::DynamicInputTextures { bool ProcessSpecificationV1(picojson::value& root, std::vector& input_textures, const std::string& base_path, const std::string& json_file); -} + +bool ProcessSpecificationV2(picojson::value& root, std::vector& input_textures, + const std::string& base_path, const std::string& json_file); +} // namespace InputCommon::DynamicInputTextures