diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp b/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp index fcdf879e8dc3..aeb0d394eba7 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp +++ b/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.cpp @@ -13,6 +13,7 @@ #include "Common/FileUtil.h" #include "Common/Logging/Log.h" #include "Common/StringUtil.h" +#include "Common/VariantUtil.h" #include "Core/ConfigManager.h" #include "InputCommon/ControllerEmu/ControllerEmu.h" #include "InputCommon/DynamicInputTextures/DITSpecification.h" @@ -72,7 +73,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", @@ -80,8 +89,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; @@ -109,7 +116,6 @@ bool Configuration::GenerateTexture(const IniFile& file, auto image_to_write = original_image; bool dirty = false; - for (const auto& controller_name : controller_names) { auto* sec = file.GetSection(controller_name); @@ -145,7 +151,7 @@ bool Configuration::GenerateTexture(const IniFile& file, } } - for (auto& [emulated_key, rects] : emulated_controls_iter->second) + for (auto& emulated_entry : emulated_controls_iter->second) { if (!device_found) { @@ -155,43 +161,11 @@ bool Configuration::GenerateTexture(const IniFile& file, continue; } - std::string host_key; - sec->Get(emulated_key, &host_key); - - const auto input_image_iter = host_devices_iter->second.find(host_key); - if (input_image_iter == host_devices_iter->second.end()) + if (ApplyEmulatedEntry(host_devices_iter->second, emulated_entry, sec, *image_to_write, + texture_data.m_preserve_aspect_ratio)) { dirty = true; } - else - { - const auto host_key_image = LoadImage(m_base_path + input_image_iter->second); - - for (const auto& rect : rects) - { - InputCommon::ImagePixelData pixel_data; - if (host_key_image->width == rect.GetWidth() && - host_key_image->height == rect.GetHeight()) - { - pixel_data = *host_key_image; - } - else if (texture_data.m_preserve_aspect_ratio) - { - pixel_data = - ResizeKeepAspectRatio(ResizeMode::Nearest, *host_key_image, rect.GetWidth(), - rect.GetHeight(), Pixel{0, 0, 0, 0}); - } - else - { - pixel_data = - Resize(ResizeMode::Nearest, *host_key_image, rect.GetWidth(), rect.GetHeight()); - } - - CopyImageRegion(pixel_data, *image_to_write, - Rect{0, 0, rect.GetWidth(), rect.GetHeight()}, rect); - dirty = true; - } - } } } @@ -218,4 +192,132 @@ bool Configuration::GenerateTexture(const IniFile& file, return false; } + +bool Configuration::ApplyEmulatedEntry(const Configuration::HostEntries& host_entries, + const Data::EmulatedEntry& emulated_entry, + const IniFile::Section* section, + 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); +} + +bool Configuration::ApplyEmulatedSingleEntry(const Configuration::HostEntries& host_entries, + const std::vector keys, + const std::optional tag, + const Rect& region, ImagePixelData& image_to_write, + bool preserve_aspect_ratio) const +{ + for (auto& host_entry : host_entries) + { + if (keys == host_entry.m_keys && tag == host_entry.m_tag) + { + const auto host_key_image = LoadImage(m_base_path + host_entry.m_path); + ImagePixelData pixel_data; + if (host_key_image->width == region.GetWidth() && + host_key_image->height == region.GetHeight()) + { + pixel_data = *host_key_image; + } + else if (preserve_aspect_ratio) + { + pixel_data = ResizeKeepAspectRatio(ResizeMode::Nearest, *host_key_image, region.GetWidth(), + region.GetHeight(), Pixel{0, 0, 0, 0}); + } + else + { + pixel_data = + Resize(ResizeMode::Nearest, *host_key_image, region.GetWidth(), region.GetHeight()); + } + + CopyImageRegion(pixel_data, image_to_write, Rect{0, 0, region.GetWidth(), region.GetHeight()}, + region); + + return true; + } + } + + return false; +} + +bool Configuration::ApplyEmulatedMultiEntry(const Configuration::HostEntries& host_entries, + const Data::EmulatedMultiEntry& emulated_entry, + const IniFile::Section* section, + InputCommon::ImagePixelData& image_to_write, + bool preserve_aspect_ratio) const +{ + // Try to apply our group'd region first + const auto emulated_keys = GetKeysFrom(emulated_entry); + std::vector host_keys; + host_keys.reserve(emulated_keys.size()); + for (const auto& emulated_key : emulated_keys) + { + std::string host_key; + section->Get(emulated_key, &host_key); + host_keys.push_back(host_key); + } + if (ApplyEmulatedSingleEntry(host_entries, host_keys, emulated_entry.m_combined_tag, + emulated_entry.m_combined_region, image_to_write, + preserve_aspect_ratio)) + { + return true; + } + + ImagePixelData temporary_pixel_data(emulated_entry.m_combined_region.GetWidth(), + emulated_entry.m_combined_region.GetHeight()); + bool apply = false; + for (const auto& sub_entry : emulated_entry.m_sub_entries) + { + apply |= ApplyEmulatedEntry(host_entries, sub_entry, section, temporary_pixel_data, + preserve_aspect_ratio); + } + + if (apply) + { + CopyImageRegion(temporary_pixel_data, image_to_write, + Rect{0, 0, emulated_entry.m_combined_region.GetWidth(), + emulated_entry.m_combined_region.GetHeight()}, + emulated_entry.m_combined_region); + } + + return apply; +} + +std::vector Configuration::GetKeysFrom(const Data::EmulatedEntry& emulated_entry) const +{ + return std::visit( + overloaded{ + [&, this](const Data::EmulatedSingleEntry& entry) { + return std::vector{entry.m_key}; + }, + [&, this](const Data::EmulatedMultiEntry& entry) { return GetKeysFrom(entry); }, + }, + emulated_entry); +} + +std::vector +Configuration::GetKeysFrom(const Data::EmulatedMultiEntry& emulated_entry) const +{ + std::vector result; + for (const auto& sub_entry : emulated_entry.m_sub_entries) + { + const auto sub_entry_keys = GetKeysFrom(sub_entry); + result.reserve(result.size() + sub_entry_keys.size()); + result.insert(result.end(), sub_entry_keys.begin(), sub_entry_keys.end()); + } + return result; +} } // namespace InputCommon::DynamicInputTextures diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.h b/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.h index 635e1883a171..e6fbc9c9aec7 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.h +++ b/Source/Core/InputCommon/DynamicInputTextures/DITConfiguration.h @@ -5,11 +5,13 @@ #include #include +#include #include #include "Common/CommonTypes.h" #include "Common/IniFile.h" #include "InputCommon/DynamicInputTextures/DITData.h" +#include "InputCommon/ImageOperations.h" namespace InputCommon::DynamicInputTextures { @@ -25,6 +27,24 @@ class Configuration bool GenerateTexture(const IniFile& file, const std::vector& controller_names, const Data& texture_data) const; + using HostEntries = std::vector; + bool ApplyEmulatedEntry(const HostEntries& host_entries, + const Data::EmulatedEntry& emulated_entry, + const IniFile::Section* section, ImagePixelData& image_to_write, + bool preserve_aspect_ratio) const; + bool ApplyEmulatedSingleEntry(const HostEntries& host_entries, + const std::vector keys, + const std::optional tag, const Rect& region, + ImagePixelData& image_to_write, bool preserve_aspect_ratio) const; + bool ApplyEmulatedMultiEntry(const HostEntries& host_entries, + const Data::EmulatedMultiEntry& emulated_entry, + const IniFile::Section* section, ImagePixelData& image_to_write, + bool preserve_aspect_ratio) const; + + std::vector GetKeysFrom(const Data::EmulatedEntry& emulated_entry) const; + + std::vector GetKeysFrom(const Data::EmulatedMultiEntry& emulated_entry) const; + std::vector m_dynamic_input_textures; std::string m_base_path; bool m_valid = true; diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITData.h b/Source/Core/InputCommon/DynamicInputTextures/DITData.h index 22deb25e9966..03f0f3fbd27f 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITData.h +++ b/Source/Core/InputCommon/DynamicInputTextures/DITData.h @@ -1,10 +1,9 @@ -// Copyright 2021 Dolphin Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - #pragma once +#include #include #include +#include #include #include "InputCommon/ImageOperations.h" @@ -17,11 +16,35 @@ struct Data std::string m_hires_texture_name; std::string m_generated_folder_name; - using EmulatedKeyToRegionsMap = std::unordered_map>; - std::unordered_map m_emulated_controllers; + struct EmulatedSingleEntry; + struct EmulatedMultiEntry; + using EmulatedEntry = std::variant; + + struct EmulatedSingleEntry + { + std::string m_key; + std::optional m_tag; + Rect m_region; + }; + + struct EmulatedMultiEntry + { + std::string m_combined_tag; + Rect m_combined_region; + + std::vector m_sub_entries; + }; + + std::unordered_map> m_emulated_controllers; + + struct HostEntry + { + std::vector m_keys; + std::optional m_tag; + std::string m_path; + }; - using HostKeyToImagePath = std::unordered_map; - std::unordered_map m_host_devices; + std::unordered_map> m_host_devices; bool m_preserve_aspect_ratio = true; }; } // namespace InputCommon::DynamicInputTextures diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp index f5409b518fb9..53248bc927c9 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp +++ b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.cpp @@ -9,9 +9,193 @@ #include "Common/IOFile.h" #include "Common/Logging/Log.h" #include "Core/ConfigManager.h" +namespace fs = std::filesystem; + +namespace +{ +std::string GetStreamAsString(std::ifstream& stream) +{ + std::stringstream ss; + ss << stream.rdbuf(); + return ss.str(); +} +} // namespace 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 offset, + 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 = offset + *path; + + return entry; +} +} // namespace bool ProcessSpecificationV1(picojson::value& root, std::vector& input_textures, const std::string& base_path, const std::string& json_file) { @@ -96,7 +280,7 @@ bool ProcessSpecificationV1(picojson::value& root, std::vector& input_text return false; } - auto& key_to_regions = texture_data.m_emulated_controllers[emulated_controller_name]; + auto& entries_vector = texture_data.m_emulated_controllers[emulated_controller_name]; for (auto& [emulated_control, regions_array] : map.get()) { if (!regions_array.is()) @@ -109,7 +293,6 @@ bool ProcessSpecificationV1(picojson::value& root, std::vector& input_text return false; } - std::vector region_rects; for (auto& region : regions_array.get()) { Rect r; @@ -151,9 +334,10 @@ bool ProcessSpecificationV1(picojson::value& root, std::vector& input_text r.top = static_cast(region_offsets[1].get()); r.right = static_cast(region_offsets[2].get()); r.bottom = static_cast(region_offsets[3].get()); - region_rects.push_back(r); + + entries_vector.push_back( + Data::EmulatedSingleEntry{emulated_control, std::nullopt, std::move(r)}); } - key_to_regions.insert_or_assign(emulated_control, std::move(region_rects)); } } @@ -185,10 +369,241 @@ bool ProcessSpecificationV1(picojson::value& root, std::vector& input_text json_file, host_device); return false; } - auto& host_control_to_imagename = texture_data.m_host_devices[host_device]; + auto& host_control_entries = texture_data.m_host_devices[host_device]; for (auto& [host_control, image_name] : map.get()) { - host_control_to_imagename.insert_or_assign(host_control, image_name.to_str()); + host_control_entries.push_back(Data::HostEntry{std::vector{host_control}, + std::nullopt, image_name.to_str()}); + } + } + + input_textures.emplace_back(std::move(texture_data)); + } + + 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; + std::string host_controls_path = base_path; + if (default_host_controls_json.is()) + { + default_host_controls = default_host_controls_json.get(); + } + else if (default_host_controls_json.is()) + { + const std::string default_host_controls_full_path = base_path + default_host_controls_json.get(); + if (!File::Exists(default_host_controls_full_path)) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because the json file '{}' " + "could not be loaded", + json_file, default_host_controls_full_path); + return false; + } + std::ifstream json_stream; + File::OpenFStream(json_stream, default_host_controls_full_path, std::ios_base::in); + if (!json_stream.is_open()) + { + ERROR_LOG_FMT(VIDEO, "Failed to load dynamic input json file '{}'", + default_host_controls_full_path); + return false; + } + + picojson::value new_root; + const auto error = picojson::parse(new_root, GetStreamAsString(json_stream)); + + if (!error.empty()) + { + ERROR_LOG_FMT(VIDEO, "Failed to load dynamic input json file '{}' due to parse error: {}", + default_host_controls_full_path, error); + return false; + } + default_host_controls = new_root.get(); + SplitPath(default_host_controls_full_path, &host_controls_path, nullptr, nullptr); + } + + 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(); + host_controls_path = base_path; + } + + 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) + { + std::string host_device_path = host_controls_path; + if (arr.is()) + const std::string host_device_full_path = host_controls_path + arr.get(); + SplitPath(host_device_full_path, &host_device_path, nullptr, nullptr); + if (!File::Exists(host_device_full_path)) + { + ERROR_LOG_FMT(VIDEO, + "Failed to load dynamic input json file '{}' because the json file '{}' " + "could not be loaded", + json_file, host_device_full_path); + return false; + } + std::ifstream json_stream; + File::OpenFStream(json_stream, host_device_full_path, std::ios_base::in); + if (!json_stream.is_open()) + { + ERROR_LOG_FMT(VIDEO, "Failed to load dynamic input json file '{}'", + host_device_full_path); + return false; + } + + picojson::value new_root; + const auto error = picojson::parse(new_root, GetStreamAsString(json_stream)); + + if (!error.empty()) + { + ERROR_LOG_FMT(VIDEO, "Failed to load dynamic input json file '{}' due to parse error: {}", + host_device_full_path, error); + return false; + } + arr = new_root; + } + + if (!arr.is()) + "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; + } + // Account for relative file links + fs::path diff = fs::path(host_device_path).lexically_relative(fs::path(base_path)); + + + const auto entry = GetHostEntry(entry_json.get(), diff.string(), json_file); + if (!entry) + { + return false; + } + host_control_entries.push_back(*entry); } } diff --git a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h index b6693dca3248..b72b130d3700 100644 --- a/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h +++ b/Source/Core/InputCommon/DynamicInputTextures/DITSpecification.h @@ -14,4 +14,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 diff --git a/docs/DynamicInputTextures.md b/docs/DynamicInputTextures/DynamicInputTexturesV1.md similarity index 94% rename from docs/DynamicInputTextures.md rename to docs/DynamicInputTextures/DynamicInputTexturesV1.md index 770bb1dabae2..21fc14ae46e4 100644 --- a/docs/DynamicInputTextures.md +++ b/docs/DynamicInputTextures/DynamicInputTexturesV1.md @@ -32,11 +32,11 @@ You need at least a single json file that describes the generation parameters. In each json, one or more generated textures can be specified. Each of those textures can have the following fields: -|Identifier |Required | Since | -|-------------------------|---------|-------| -|``image`` | **Yes** | v1 | -|``emulated_controls`` | **Yes** | v1 | -|``host_controls`` | No | v1 | +|Identifier |Required | +|-------------------------|---------| +|``image`` | **Yes** | +|``emulated_controls`` | **Yes** | +|``host_controls`` | No | *image* - the image that has the input buttons you wish to replace, can be upscaled/redrawn if desired. @@ -48,11 +48,11 @@ In each json, one or more generated textures can be specified. Each of those te The following fields apply to all textures in the json file: -|Identifier | Since | -|-------------------------|-------| -|``generated_folder_name``| v1 | -|``preserve_aspect_ratio``| v1 | -|``default_host_controls``| v1 | +|Identifier | +|-------------------------| +|``generated_folder_name``| +|``preserve_aspect_ratio``| +|``default_host_controls``| *generated_folder_name* - the folder name that the textures will be generated into. Optional, defaults to '_Generated' diff --git a/docs/DynamicInputTextures/DynamicInputTexturesV2.md b/docs/DynamicInputTextures/DynamicInputTexturesV2.md new file mode 100644 index 000000000000..0856f1b03797 --- /dev/null +++ b/docs/DynamicInputTextures/DynamicInputTexturesV2.md @@ -0,0 +1,405 @@ +# Dolphin Dynamic Input Textures Specification (v2) + +## Format +Dynamic Input Textures are generated textures based on a user's input formed from a group of png files and json files. + +``` +\__ Dolphin User Directory + \__ Load (Directory) + \__ DynamicInputTextures (Directory) + \__ FOLDER (Directory) + \__ PNG and JSON GO HERE +``` + +``FOLDER`` can be one or multiple directories which are named after: +* a complete Game ID (e.g. ``SMNE01`` for "New Super Mario Bros. Wii (NTSC)") +* one without a region (e.g. ``SMN`` for "New Super Mario Bros. Wii (All regions)"). +* Any folder name but with an empty ``.txt`` underneath it + +## How to enable + +Place the files in the format above and ensure that "Load Custom Textures" is enabled under the advanced tab of the graphics settings. + +### PNG files + +At a minimum two images are required to support the generation and any number of 'button' images. These need to be in PNG format. + +### JSON files + +You need at least a single json file that describes the generation parameters. You may have multiple JSON files if you prefer that from an organizational standpoint. + +#### Possible fields in the JSON for a texture + +In each json, one or more generated textures can be specified. Each of those textures can have the following fields: + +|Identifier |Required | +|-------------------------|---------| +|``image`` | **Yes** | +|``emulated_controls`` | **Yes** | +|``host_controls`` | No | + +*image* - the image that has the input buttons you wish to replace, can be upscaled/redrawn if desired. + +*emulated_controls* - a map of emulated devices (ex: ``Wiimote1``, ``GCPad2``) each with their own array of emulated keys. These keys are described below. + +*host_controls* - a map of devices (ex: ``DInput/0/Keyboard Mouse``, ``XInput/1/Gamepad``, or blank for wildcard) each with their own array of host keys. Required if ``default_host_controls`` is not defined in the global section. + +#### Emulated Keys + +There are two emulated key types supported one that covers a single icon representing a single key in Dolphin and the other covers a single icon that represents multiple keys in Dolphin. + +##### Single Key + +A key is a json map that has various attributes. These attributes depend on what the "bind_type" is. By default the "bind_type" is ``single`` to denote this key matching a single key in Dolphin. + +Here is an example of a single key: + +```js +{ + "bind_type": "single", + "key": "Buttons/A", + "region": [0, 0, 30, 30] +} +``` + +Since ``single`` binding is the default, you can leave it off too: + +```js +{ + "key": "Buttons/A", + "region": [0, 0, 30, 30] +} +``` + +Single keys can also optionally have a "tag". This can be used to denote various things, like for instance a game that has a pressed icon. + +Here is an example of a single key with a tag: + +```js +{ + "bind_type": "single", + "key": "Buttons/A", + "tag": "pressed", + "region": [0, 0, 30, 30] +} +``` + +##### Multi Key + +The single key case works well when an image maps directly to Dolphin's emulated controller bindings. However, what about the case where there is a dpad icon? Dolphin has four mappings for that. This is where a "bind_type" of ``multi`` is helpful. + +When binding a multi button icon, there are two scenarios. 1) The user has all the buttons mapped and there is a corresponding host icon for that. 2) The user hasn't mapped all the buttons, so we must map each to a portion of the image. + +Here's an example of that: + +```js +{ + "bind_type": "multi", + "tag": "dpad", + "region": [0, 0, 45, 45], + "sub_entries": [ + { + "bind_type": "single", + "key": "D-Pad/Up", + "region": [7.5, 0, 22.5, 15] + }, + { + "bind_type": "single", + "key": "D-Pad/Left", + "region": [0, 15, 15, 30] + }, + { + "bind_type": "single", + "key": "D-Pad/Right", + "region": [15, 15, 30, 30] + }, + { + "bind_type": "single", + "key": "D-Pad/Down", + "region": [7.5, 30, 22.5, 45] + } + ] +} +``` + +All the keys are first checked and if a match occurs, then the top ``region`` above is used. If a match doesn't occur, then individual replacements happen for the individual keys like normal. + + +#### Host Keys + +At the moment, there is only a single host key type. Keys must match all elements of the ``keys`` array in order for a ``multi`` bind type to match. For instance, in the above example, the following host controls would match assuming normal bindings, while other orderings would not. + +```js +{ + "host_controls": { + "DInput/0/Keyboard Mouse": [ + { + "keys": ["UP", "LEFT", "DOWN", "RIGHT"], + "tag": "dpad", + "image": "keyboard/wasd.png" + } + ], + "XInput/0/Gamepad": [ + { + "keys": ["`Pad N`", "`Pad E`", "`Pad S`", "`Pad W`"], + "tag": "dpad", + "image": "gamepad/dpad.png" + } + ] + } +} +``` + +#### Global fields in the JSON applied to all textures + +The following fields apply to all textures in the json file: + +|Identifier | +|-------------------------| +|``generated_folder_name``| +|``preserve_aspect_ratio``| +|``default_host_controls``| + +*generated_folder_name* - the folder name that the textures will be generated into. Optional, defaults to '_Generated' + +*preserve_aspect_ratio* - will preserve the aspect ratio when replacing the colored boxes with the image. Optional, defaults to on + +*default_host_controls* - a default map of devices to a map of host controls (keyboard or gamepad values) that each maps to an image. + +#### Full Examples + +Here's an example of generating a single image with the "A" and "B" Wiimote1 buttons replaced to either keyboard buttons or gamepad buttons depending on the user device and the input mapped: + +```js +{ + "specification": 2.0, + "generated_folder_name": "MyDynamicTexturePack", + "preserve_aspect_ratio": false, + "output_textures": + { + "tex1_128x128_02870c3b015d8b40_5.png": + { + "image": "icons.png", + "emulated_controls": { + "Wiimote1": [ + { + "key": "Buttons/A", + "region": [0, 0, 30, 30] + }, + { + "key": "Buttons/A", + "region": [500, 550, 530, 580] + }, + { + "key": "Buttons/B", + "region": [100, 342, 132, 374] + } + ] + }, + "host_controls": { + "DInput/0/Keyboard Mouse": [ + { + "keys": ["A"], + "image": "keyboard/a.png" + }, + { + "keys": ["B"], + "image": "keyboard/b.png" + } + ], + "XInput/0/Gamepad": [ + { + "keys": ["`Button A`"], + "image": "gamepad/a.png" + }, + { + "keys": ["`Button B`"], + "image": "gamepad/b.png" + } + ] + } + } + } +} +``` + +Here's the same example but the "A" button has a "pressed" state for one of its icons: + +```js +{ + "specification": 2.0, + "generated_folder_name": "MyDynamicTexturePack", + "preserve_aspect_ratio": false, + "output_textures": + { + "tex1_128x128_02870c3b015d8b40_5.png": + { + "image": "icons.png", + "emulated_controls": { + "Wiimote1": [ + { + "key": "Buttons/A", + "region": [0, 0, 30, 30] + }, + { + "key": "Buttons/A", + "tag": "pressed", + "region": [500, 550, 530, 580] + }, + { + "key": "Buttons/B", + "region": [100, 342, 132, 374] + } + ] + }, + "host_controls": { + "DInput/0/Keyboard Mouse": [ + { + "keys": ["A"], + "image": "keyboard/a.png" + }, + { + "keys": ["A"], + "tag": "pressed", + "image": "keyboard/a_pressed.png" + }, + { + "keys": ["B"], + "image": "keyboard/b.png" + } + ], + "XInput/0/Gamepad": [ + { + "keys": ["`Button A`"], + "image": "gamepad/a.png" + }, + { + "keys": ["`Button A`"], + "tag": "pressed", + "image": "gamepad/a_pressed.png" + }, + { + "keys": ["`Button B`"], + "image": "gamepad/b.png" + } + ] + } + } + } +} +``` + +As an example, you are writing a pack for a single-player game. You may want to provide DS4 controller icons but not care what device the user is using. You can use a wildcard for that: + +```js +{ + "specification": 2.0, + "preserve_aspect_ratio": false, + "output_textures": + { + "tex1_128x128_02870c3b015d8b40_5.png": + { + "image": "icons.png", + "emulated_controls": { + "Wiimote1": [ + { + "key": "Buttons/A", + "region": [0, 0, 30, 30] + }, + { + "key": "Buttons/A", + "region": [500, 550, 530, 580] + }, + { + "key": "Buttons/B" + "region": [100, 342, 132, 374] + } + ] + }, + "host_controls": { + "": [ + {"keys": ["`Button X`"], "image": "ds4/x.png"}, + {"keys": ["`Button Y`"], "image": "ds4/y.png"} + ] + } + } + } +} +``` + +Here's an example of generating multiple images but using defaults from the global section except for one texture: + +```js +{ + "specification": 2.0, + "default_host_controls": { + "DInput/0/Keyboard Mouse": [ + {"keys": ["A"], "image": "keyboard/a.png"}, + {"keys": ["B"], "image": "keyboard/b.png"} + ] + }, + "default_device": "DInput/0/Keyboard Mouse", + "output_textures": + { + "tex1_21x26_5cbc6943a74cb7ca_67a541879d5fe615_9.png": + { + "image": "icons1.png", + "emulated_controls": { + "Wiimote1": [] + { + "key": "Buttons/A", + "region": [62, 0, 102, 40] + }, + { + "key": "Buttons/B", + "region": [100, 342, 132, 374] + } + ] + } + }, + "tex1_21x26_5e71c27dad9cda76_3d76bd5d1e73c3b1_9.png": + { + "image": "icons2.png", + "emulated_controls": { + "Wiimote1": [ + { + "key": "Buttons/A", + "region": [857, 682, 907, 732] + }, + { + "key" :"Buttons/B", + "region": [100, 342, 132, 374] + } + ] + } + }, + "tex1_24x24_003f3a17f66f1704_82848f47946caa41_9.png": + { + "image": "icons3.png", + "emulated_controls": { + "Wiimote1": [ + { + "key": "Buttons/A", + "region": [0, 0, 30, 30] + }, + { + "key": "Buttons/A", + "region": [500, 550, 530, 580] + }, + { + "key": "Buttons/B", + "region": [100, 342, 132, 374] + } + ] + }, + "host_controls": + { + "DInput/0/Keyboard Mouse": [ + {"keys": ["A"], "image": "keyboard/a_special.png"}, + {"keys": ["B"], "image": "keyboard/b.png"} + ] + } + } + } +} +```