forked from eden-emu/eden
		
	Merge pull request #6485 from MonsterDruide1/tas
input_common: TAS with script playback & recording
This commit is contained in:
		
						commit
						6e376c27a3
					
				
					 27 changed files with 1426 additions and 21 deletions
				
			
		|  | @ -21,6 +21,7 @@ | ||||||
| #define SCREENSHOTS_DIR "screenshots" | #define SCREENSHOTS_DIR "screenshots" | ||||||
| #define SDMC_DIR "sdmc" | #define SDMC_DIR "sdmc" | ||||||
| #define SHADER_DIR "shader" | #define SHADER_DIR "shader" | ||||||
|  | #define TAS_DIR "tas" | ||||||
| 
 | 
 | ||||||
| // yuzu-specific files
 | // yuzu-specific files
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -116,6 +116,7 @@ private: | ||||||
|         GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); |         GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); | ||||||
|         GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); |         GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); | ||||||
|         GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); |         GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); | ||||||
|  |         GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ~PathManagerImpl() = default; |     ~PathManagerImpl() = default; | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ enum class YuzuPath { | ||||||
|     ScreenshotsDir, // Where yuzu screenshots are stored.
 |     ScreenshotsDir, // Where yuzu screenshots are stored.
 | ||||||
|     SDMCDir,        // Where the emulated SDMC is stored.
 |     SDMCDir,        // Where the emulated SDMC is stored.
 | ||||||
|     ShaderDir,      // Where shaders are stored.
 |     ShaderDir,      // Where shaders are stored.
 | ||||||
|  |     TASDir,         // Where TAS scripts are stored.
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|  |  | ||||||
|  | @ -512,6 +512,11 @@ struct Values { | ||||||
|                                             "motion_device"}; |                                             "motion_device"}; | ||||||
|     BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"}; |     BasicSetting<std::string> udp_input_servers{"127.0.0.1:26760", "udp_input_servers"}; | ||||||
| 
 | 
 | ||||||
|  |     BasicSetting<bool> pause_tas_on_load{true, "pause_tas_on_load"}; | ||||||
|  |     BasicSetting<bool> tas_enable{false, "tas_enable"}; | ||||||
|  |     BasicSetting<bool> tas_loop{false, "tas_loop"}; | ||||||
|  |     BasicSetting<bool> tas_swap_controllers{true, "tas_swap_controllers"}; | ||||||
|  | 
 | ||||||
|     BasicSetting<bool> mouse_panning{false, "mouse_panning"}; |     BasicSetting<bool> mouse_panning{false, "mouse_panning"}; | ||||||
|     BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"}; |     BasicRangedSetting<u8> mouse_panning_sensitivity{10, 1, 100, "mouse_panning_sensitivity"}; | ||||||
|     BasicSetting<bool> mouse_enabled{false, "mouse_enabled"}; |     BasicSetting<bool> mouse_enabled{false, "mouse_enabled"}; | ||||||
|  |  | ||||||
|  | @ -21,6 +21,10 @@ add_library(input_common STATIC | ||||||
|     mouse/mouse_poller.h |     mouse/mouse_poller.h | ||||||
|     sdl/sdl.cpp |     sdl/sdl.cpp | ||||||
|     sdl/sdl.h |     sdl/sdl.h | ||||||
|  |     tas/tas_input.cpp | ||||||
|  |     tas/tas_input.h | ||||||
|  |     tas/tas_poller.cpp | ||||||
|  |     tas/tas_poller.h | ||||||
|     udp/client.cpp |     udp/client.cpp | ||||||
|     udp/client.h |     udp/client.h | ||||||
|     udp/protocol.cpp |     udp/protocol.cpp | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <thread> | #include <thread> | ||||||
| #include "common/param_package.h" | #include "common/param_package.h" | ||||||
|  | #include "common/settings.h" | ||||||
| #include "input_common/analog_from_button.h" | #include "input_common/analog_from_button.h" | ||||||
| #include "input_common/gcadapter/gc_adapter.h" | #include "input_common/gcadapter/gc_adapter.h" | ||||||
| #include "input_common/gcadapter/gc_poller.h" | #include "input_common/gcadapter/gc_poller.h" | ||||||
|  | @ -13,6 +14,8 @@ | ||||||
| #include "input_common/motion_from_button.h" | #include "input_common/motion_from_button.h" | ||||||
| #include "input_common/mouse/mouse_input.h" | #include "input_common/mouse/mouse_input.h" | ||||||
| #include "input_common/mouse/mouse_poller.h" | #include "input_common/mouse/mouse_poller.h" | ||||||
|  | #include "input_common/tas/tas_input.h" | ||||||
|  | #include "input_common/tas/tas_poller.h" | ||||||
| #include "input_common/touch_from_button.h" | #include "input_common/touch_from_button.h" | ||||||
| #include "input_common/udp/client.h" | #include "input_common/udp/client.h" | ||||||
| #include "input_common/udp/udp.h" | #include "input_common/udp/udp.h" | ||||||
|  | @ -60,6 +63,12 @@ struct InputSubsystem::Impl { | ||||||
|         Input::RegisterFactory<Input::MotionDevice>("mouse", mousemotion); |         Input::RegisterFactory<Input::MotionDevice>("mouse", mousemotion); | ||||||
|         mousetouch = std::make_shared<MouseTouchFactory>(mouse); |         mousetouch = std::make_shared<MouseTouchFactory>(mouse); | ||||||
|         Input::RegisterFactory<Input::TouchDevice>("mouse", mousetouch); |         Input::RegisterFactory<Input::TouchDevice>("mouse", mousetouch); | ||||||
|  | 
 | ||||||
|  |         tas = std::make_shared<TasInput::Tas>(); | ||||||
|  |         tasbuttons = std::make_shared<TasButtonFactory>(tas); | ||||||
|  |         Input::RegisterFactory<Input::ButtonDevice>("tas", tasbuttons); | ||||||
|  |         tasanalog = std::make_shared<TasAnalogFactory>(tas); | ||||||
|  |         Input::RegisterFactory<Input::AnalogDevice>("tas", tasanalog); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     void Shutdown() { |     void Shutdown() { | ||||||
|  | @ -94,6 +103,12 @@ struct InputSubsystem::Impl { | ||||||
|         mouseanalog.reset(); |         mouseanalog.reset(); | ||||||
|         mousemotion.reset(); |         mousemotion.reset(); | ||||||
|         mousetouch.reset(); |         mousetouch.reset(); | ||||||
|  | 
 | ||||||
|  |         Input::UnregisterFactory<Input::ButtonDevice>("tas"); | ||||||
|  |         Input::UnregisterFactory<Input::AnalogDevice>("tas"); | ||||||
|  | 
 | ||||||
|  |         tasbuttons.reset(); | ||||||
|  |         tasanalog.reset(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     [[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const { |     [[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const { | ||||||
|  | @ -101,6 +116,10 @@ struct InputSubsystem::Impl { | ||||||
|             Common::ParamPackage{{"display", "Any"}, {"class", "any"}}, |             Common::ParamPackage{{"display", "Any"}, {"class", "any"}}, | ||||||
|             Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}}, |             Common::ParamPackage{{"display", "Keyboard/Mouse"}, {"class", "keyboard"}}, | ||||||
|         }; |         }; | ||||||
|  |         if (Settings::values.tas_enable) { | ||||||
|  |             devices.emplace_back( | ||||||
|  |                 Common::ParamPackage{{"display", "TAS Controller"}, {"class", "tas"}}); | ||||||
|  |         } | ||||||
| #ifdef HAVE_SDL2 | #ifdef HAVE_SDL2 | ||||||
|         auto sdl_devices = sdl->GetInputDevices(); |         auto sdl_devices = sdl->GetInputDevices(); | ||||||
|         devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end()); |         devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end()); | ||||||
|  | @ -120,6 +139,9 @@ struct InputSubsystem::Impl { | ||||||
|         if (params.Get("class", "") == "gcpad") { |         if (params.Get("class", "") == "gcpad") { | ||||||
|             return gcadapter->GetAnalogMappingForDevice(params); |             return gcadapter->GetAnalogMappingForDevice(params); | ||||||
|         } |         } | ||||||
|  |         if (params.Get("class", "") == "tas") { | ||||||
|  |             return tas->GetAnalogMappingForDevice(params); | ||||||
|  |         } | ||||||
| #ifdef HAVE_SDL2 | #ifdef HAVE_SDL2 | ||||||
|         if (params.Get("class", "") == "sdl") { |         if (params.Get("class", "") == "sdl") { | ||||||
|             return sdl->GetAnalogMappingForDevice(params); |             return sdl->GetAnalogMappingForDevice(params); | ||||||
|  | @ -136,6 +158,9 @@ struct InputSubsystem::Impl { | ||||||
|         if (params.Get("class", "") == "gcpad") { |         if (params.Get("class", "") == "gcpad") { | ||||||
|             return gcadapter->GetButtonMappingForDevice(params); |             return gcadapter->GetButtonMappingForDevice(params); | ||||||
|         } |         } | ||||||
|  |         if (params.Get("class", "") == "tas") { | ||||||
|  |             return tas->GetButtonMappingForDevice(params); | ||||||
|  |         } | ||||||
| #ifdef HAVE_SDL2 | #ifdef HAVE_SDL2 | ||||||
|         if (params.Get("class", "") == "sdl") { |         if (params.Get("class", "") == "sdl") { | ||||||
|             return sdl->GetButtonMappingForDevice(params); |             return sdl->GetButtonMappingForDevice(params); | ||||||
|  | @ -174,9 +199,12 @@ struct InputSubsystem::Impl { | ||||||
|     std::shared_ptr<MouseAnalogFactory> mouseanalog; |     std::shared_ptr<MouseAnalogFactory> mouseanalog; | ||||||
|     std::shared_ptr<MouseMotionFactory> mousemotion; |     std::shared_ptr<MouseMotionFactory> mousemotion; | ||||||
|     std::shared_ptr<MouseTouchFactory> mousetouch; |     std::shared_ptr<MouseTouchFactory> mousetouch; | ||||||
|  |     std::shared_ptr<TasButtonFactory> tasbuttons; | ||||||
|  |     std::shared_ptr<TasAnalogFactory> tasanalog; | ||||||
|     std::shared_ptr<CemuhookUDP::Client> udp; |     std::shared_ptr<CemuhookUDP::Client> udp; | ||||||
|     std::shared_ptr<GCAdapter::Adapter> gcadapter; |     std::shared_ptr<GCAdapter::Adapter> gcadapter; | ||||||
|     std::shared_ptr<MouseInput::Mouse> mouse; |     std::shared_ptr<MouseInput::Mouse> mouse; | ||||||
|  |     std::shared_ptr<TasInput::Tas> tas; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} | InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} | ||||||
|  | @ -207,6 +235,14 @@ const MouseInput::Mouse* InputSubsystem::GetMouse() const { | ||||||
|     return impl->mouse.get(); |     return impl->mouse.get(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | TasInput::Tas* InputSubsystem::GetTas() { | ||||||
|  |     return impl->tas.get(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const TasInput::Tas* InputSubsystem::GetTas() const { | ||||||
|  |     return impl->tas.get(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const { | std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const { | ||||||
|     return impl->GetInputDevices(); |     return impl->GetInputDevices(); | ||||||
| } | } | ||||||
|  | @ -287,6 +323,22 @@ const MouseTouchFactory* InputSubsystem::GetMouseTouch() const { | ||||||
|     return impl->mousetouch.get(); |     return impl->mousetouch.get(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | TasButtonFactory* InputSubsystem::GetTasButtons() { | ||||||
|  |     return impl->tasbuttons.get(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const TasButtonFactory* InputSubsystem::GetTasButtons() const { | ||||||
|  |     return impl->tasbuttons.get(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TasAnalogFactory* InputSubsystem::GetTasAnalogs() { | ||||||
|  |     return impl->tasanalog.get(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const TasAnalogFactory* InputSubsystem::GetTasAnalogs() const { | ||||||
|  |     return impl->tasanalog.get(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void InputSubsystem::ReloadInputDevices() { | void InputSubsystem::ReloadInputDevices() { | ||||||
|     if (!impl->udp) { |     if (!impl->udp) { | ||||||
|         return; |         return; | ||||||
|  |  | ||||||
|  | @ -29,6 +29,10 @@ namespace MouseInput { | ||||||
| class Mouse; | class Mouse; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | namespace TasInput { | ||||||
|  | class Tas; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| namespace InputCommon { | namespace InputCommon { | ||||||
| namespace Polling { | namespace Polling { | ||||||
| 
 | 
 | ||||||
|  | @ -64,6 +68,8 @@ class MouseButtonFactory; | ||||||
| class MouseAnalogFactory; | class MouseAnalogFactory; | ||||||
| class MouseMotionFactory; | class MouseMotionFactory; | ||||||
| class MouseTouchFactory; | class MouseTouchFactory; | ||||||
|  | class TasButtonFactory; | ||||||
|  | class TasAnalogFactory; | ||||||
| class Keyboard; | class Keyboard; | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|  | @ -103,6 +109,11 @@ public: | ||||||
|     /// Retrieves the underlying mouse device.
 |     /// Retrieves the underlying mouse device.
 | ||||||
|     [[nodiscard]] const MouseInput::Mouse* GetMouse() const; |     [[nodiscard]] const MouseInput::Mouse* GetMouse() const; | ||||||
| 
 | 
 | ||||||
|  |     /// Retrieves the underlying tas device.
 | ||||||
|  |     [[nodiscard]] TasInput::Tas* GetTas(); | ||||||
|  | 
 | ||||||
|  |     /// Retrieves the underlying tas device.
 | ||||||
|  |     [[nodiscard]] const TasInput::Tas* GetTas() const; | ||||||
|     /**
 |     /**
 | ||||||
|      * Returns all available input devices that this Factory can create a new device with. |      * Returns all available input devices that this Factory can create a new device with. | ||||||
|      * Each returned ParamPackage should have a `display` field used for display, a class field for |      * Each returned ParamPackage should have a `display` field used for display, a class field for | ||||||
|  | @ -144,30 +155,42 @@ public: | ||||||
|     /// Retrieves the underlying udp touch handler.
 |     /// Retrieves the underlying udp touch handler.
 | ||||||
|     [[nodiscard]] const UDPTouchFactory* GetUDPTouch() const; |     [[nodiscard]] const UDPTouchFactory* GetUDPTouch() const; | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying GameCube button handler.
 |     /// Retrieves the underlying mouse button handler.
 | ||||||
|     [[nodiscard]] MouseButtonFactory* GetMouseButtons(); |     [[nodiscard]] MouseButtonFactory* GetMouseButtons(); | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying GameCube button handler.
 |     /// Retrieves the underlying mouse button handler.
 | ||||||
|     [[nodiscard]] const MouseButtonFactory* GetMouseButtons() const; |     [[nodiscard]] const MouseButtonFactory* GetMouseButtons() const; | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying udp touch handler.
 |     /// Retrieves the underlying mouse analog handler.
 | ||||||
|     [[nodiscard]] MouseAnalogFactory* GetMouseAnalogs(); |     [[nodiscard]] MouseAnalogFactory* GetMouseAnalogs(); | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying udp touch handler.
 |     /// Retrieves the underlying mouse analog handler.
 | ||||||
|     [[nodiscard]] const MouseAnalogFactory* GetMouseAnalogs() const; |     [[nodiscard]] const MouseAnalogFactory* GetMouseAnalogs() const; | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying udp motion handler.
 |     /// Retrieves the underlying mouse motion handler.
 | ||||||
|     [[nodiscard]] MouseMotionFactory* GetMouseMotions(); |     [[nodiscard]] MouseMotionFactory* GetMouseMotions(); | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying udp motion handler.
 |     /// Retrieves the underlying mouse motion handler.
 | ||||||
|     [[nodiscard]] const MouseMotionFactory* GetMouseMotions() const; |     [[nodiscard]] const MouseMotionFactory* GetMouseMotions() const; | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying udp touch handler.
 |     /// Retrieves the underlying mouse touch handler.
 | ||||||
|     [[nodiscard]] MouseTouchFactory* GetMouseTouch(); |     [[nodiscard]] MouseTouchFactory* GetMouseTouch(); | ||||||
| 
 | 
 | ||||||
|     /// Retrieves the underlying udp touch handler.
 |     /// Retrieves the underlying mouse touch handler.
 | ||||||
|     [[nodiscard]] const MouseTouchFactory* GetMouseTouch() const; |     [[nodiscard]] const MouseTouchFactory* GetMouseTouch() const; | ||||||
| 
 | 
 | ||||||
|  |     /// Retrieves the underlying tas button handler.
 | ||||||
|  |     [[nodiscard]] TasButtonFactory* GetTasButtons(); | ||||||
|  | 
 | ||||||
|  |     /// Retrieves the underlying tas button handler.
 | ||||||
|  |     [[nodiscard]] const TasButtonFactory* GetTasButtons() const; | ||||||
|  | 
 | ||||||
|  |     /// Retrieves the underlying tas analogs handler.
 | ||||||
|  |     [[nodiscard]] TasAnalogFactory* GetTasAnalogs(); | ||||||
|  | 
 | ||||||
|  |     /// Retrieves the underlying tas analogs handler.
 | ||||||
|  |     [[nodiscard]] const TasAnalogFactory* GetTasAnalogs() const; | ||||||
|  | 
 | ||||||
|     /// Reloads the input devices
 |     /// Reloads the input devices
 | ||||||
|     void ReloadInputDevices(); |     void ReloadInputDevices(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										455
									
								
								src/input_common/tas/tas_input.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										455
									
								
								src/input_common/tas/tas_input.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,455 @@ | ||||||
|  | // Copyright 2021 yuzu Emulator Project
 | ||||||
|  | // Licensed under GPLv2+
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <cstring> | ||||||
|  | #include <regex> | ||||||
|  | 
 | ||||||
|  | #include "common/fs/file.h" | ||||||
|  | #include "common/fs/fs_types.h" | ||||||
|  | #include "common/fs/path_util.h" | ||||||
|  | #include "common/logging/log.h" | ||||||
|  | #include "common/settings.h" | ||||||
|  | #include "input_common/tas/tas_input.h" | ||||||
|  | 
 | ||||||
|  | namespace TasInput { | ||||||
|  | 
 | ||||||
|  | // Supported keywords and buttons from a TAS file
 | ||||||
|  | constexpr std::array<std::pair<std::string_view, TasButton>, 20> text_to_tas_button = { | ||||||
|  |     std::pair{"KEY_A", TasButton::BUTTON_A}, | ||||||
|  |     {"KEY_B", TasButton::BUTTON_B}, | ||||||
|  |     {"KEY_X", TasButton::BUTTON_X}, | ||||||
|  |     {"KEY_Y", TasButton::BUTTON_Y}, | ||||||
|  |     {"KEY_LSTICK", TasButton::STICK_L}, | ||||||
|  |     {"KEY_RSTICK", TasButton::STICK_R}, | ||||||
|  |     {"KEY_L", TasButton::TRIGGER_L}, | ||||||
|  |     {"KEY_R", TasButton::TRIGGER_R}, | ||||||
|  |     {"KEY_PLUS", TasButton::BUTTON_PLUS}, | ||||||
|  |     {"KEY_MINUS", TasButton::BUTTON_MINUS}, | ||||||
|  |     {"KEY_DLEFT", TasButton::BUTTON_LEFT}, | ||||||
|  |     {"KEY_DUP", TasButton::BUTTON_UP}, | ||||||
|  |     {"KEY_DRIGHT", TasButton::BUTTON_RIGHT}, | ||||||
|  |     {"KEY_DDOWN", TasButton::BUTTON_DOWN}, | ||||||
|  |     {"KEY_SL", TasButton::BUTTON_SL}, | ||||||
|  |     {"KEY_SR", TasButton::BUTTON_SR}, | ||||||
|  |     {"KEY_CAPTURE", TasButton::BUTTON_CAPTURE}, | ||||||
|  |     {"KEY_HOME", TasButton::BUTTON_HOME}, | ||||||
|  |     {"KEY_ZL", TasButton::TRIGGER_ZL}, | ||||||
|  |     {"KEY_ZR", TasButton::TRIGGER_ZR}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Tas::Tas() { | ||||||
|  |     if (!Settings::values.tas_enable) { | ||||||
|  |         needs_reset = true; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     LoadTasFiles(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Tas::~Tas() { | ||||||
|  |     Stop(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | void Tas::LoadTasFiles() { | ||||||
|  |     script_length = 0; | ||||||
|  |     for (size_t i = 0; i < commands.size(); i++) { | ||||||
|  |         LoadTasFile(i); | ||||||
|  |         if (commands[i].size() > script_length) { | ||||||
|  |             script_length = commands[i].size(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::LoadTasFile(size_t player_index) { | ||||||
|  |     if (!commands[player_index].empty()) { | ||||||
|  |         commands[player_index].clear(); | ||||||
|  |     } | ||||||
|  |     std::string file = | ||||||
|  |         Common::FS::ReadStringFromFile(Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / | ||||||
|  |                                            fmt::format("script0-{}.txt", player_index + 1), | ||||||
|  |                                        Common::FS::FileType::BinaryFile); | ||||||
|  |     std::stringstream command_line(file); | ||||||
|  |     std::string line; | ||||||
|  |     int frame_no = 0; | ||||||
|  |     while (std::getline(command_line, line, '\n')) { | ||||||
|  |         if (line.empty()) { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         LOG_DEBUG(Input, "Loading line: {}", line); | ||||||
|  |         std::smatch m; | ||||||
|  | 
 | ||||||
|  |         std::stringstream linestream(line); | ||||||
|  |         std::string segment; | ||||||
|  |         std::vector<std::string> seglist; | ||||||
|  | 
 | ||||||
|  |         while (std::getline(linestream, segment, ' ')) { | ||||||
|  |             seglist.push_back(segment); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (seglist.size() < 4) { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         while (frame_no < std::stoi(seglist.at(0))) { | ||||||
|  |             commands[player_index].push_back({}); | ||||||
|  |             frame_no++; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         TASCommand command = { | ||||||
|  |             .buttons = ReadCommandButtons(seglist.at(1)), | ||||||
|  |             .l_axis = ReadCommandAxis(seglist.at(2)), | ||||||
|  |             .r_axis = ReadCommandAxis(seglist.at(3)), | ||||||
|  |         }; | ||||||
|  |         commands[player_index].push_back(command); | ||||||
|  |         frame_no++; | ||||||
|  |     } | ||||||
|  |     LOG_INFO(Input, "TAS file loaded! {} frames", frame_no); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::WriteTasFile(std::u8string file_name) { | ||||||
|  |     std::string output_text; | ||||||
|  |     for (size_t frame = 0; frame < record_commands.size(); frame++) { | ||||||
|  |         if (!output_text.empty()) { | ||||||
|  |             output_text += "\n"; | ||||||
|  |         } | ||||||
|  |         const TASCommand& line = record_commands[frame]; | ||||||
|  |         output_text += std::to_string(frame) + " " + WriteCommandButtons(line.buttons) + " " + | ||||||
|  |                        WriteCommandAxis(line.l_axis) + " " + WriteCommandAxis(line.r_axis); | ||||||
|  |     } | ||||||
|  |     const auto bytes_written = Common::FS::WriteStringToFile( | ||||||
|  |         Common::FS::GetYuzuPath(Common::FS::YuzuPath::TASDir) / file_name, | ||||||
|  |         Common::FS::FileType::TextFile, output_text); | ||||||
|  |     if (bytes_written == output_text.size()) { | ||||||
|  |         LOG_INFO(Input, "TAS file written to file!"); | ||||||
|  |     } else { | ||||||
|  |         LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written, | ||||||
|  |                   output_text.size()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::pair<float, float> Tas::FlipAxisY(std::pair<float, float> old) { | ||||||
|  |     auto [x, y] = old; | ||||||
|  |     return {x, -y}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes) { | ||||||
|  |     last_input = {buttons, FlipAxisY(axes[0]), FlipAxisY(axes[1])}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::tuple<TasState, size_t, size_t> Tas::GetStatus() const { | ||||||
|  |     TasState state; | ||||||
|  |     if (is_recording) { | ||||||
|  |         return {TasState::Recording, 0, record_commands.size()}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (is_running) { | ||||||
|  |         state = TasState::Running; | ||||||
|  |     } else { | ||||||
|  |         state = TasState::Stopped; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return {state, current_command, script_length}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string Tas::DebugButtons(u32 buttons) const { | ||||||
|  |     return fmt::format("{{ {} }}", TasInput::Tas::ButtonsToString(buttons)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string Tas::DebugJoystick(float x, float y) const { | ||||||
|  |     return fmt::format("[ {} , {} ]", std::to_string(x), std::to_string(y)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string Tas::DebugInput(const TasData& data) const { | ||||||
|  |     return fmt::format("{{ {} , {} , {} }}", DebugButtons(data.buttons), | ||||||
|  |                        DebugJoystick(data.axis[0], data.axis[1]), | ||||||
|  |                        DebugJoystick(data.axis[2], data.axis[3])); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string Tas::DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const { | ||||||
|  |     std::string returns = "[ "; | ||||||
|  |     for (size_t i = 0; i < arr.size(); i++) { | ||||||
|  |         returns += DebugInput(arr[i]); | ||||||
|  |         if (i != arr.size() - 1) { | ||||||
|  |             returns += " , "; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return returns + "]"; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string Tas::ButtonsToString(u32 button) const { | ||||||
|  |     std::string returns; | ||||||
|  |     for (auto [text_button, tas_button] : text_to_tas_button) { | ||||||
|  |         if ((button & static_cast<u32>(tas_button)) != 0) | ||||||
|  |             returns += fmt::format(", {}", text_button.substr(4)); | ||||||
|  |     } | ||||||
|  |     return returns.empty() ? "" : returns.substr(2); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::UpdateThread() { | ||||||
|  |     if (!Settings::values.tas_enable) { | ||||||
|  |         if (is_running) { | ||||||
|  |             Stop(); | ||||||
|  |         } | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (is_recording) { | ||||||
|  |         record_commands.push_back(last_input); | ||||||
|  |     } | ||||||
|  |     if (needs_reset) { | ||||||
|  |         current_command = 0; | ||||||
|  |         needs_reset = false; | ||||||
|  |         LoadTasFiles(); | ||||||
|  |         LOG_DEBUG(Input, "tas_reset done"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!is_running) { | ||||||
|  |         tas_data.fill({}); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     if (current_command < script_length) { | ||||||
|  |         LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length); | ||||||
|  |         size_t frame = current_command++; | ||||||
|  |         for (size_t i = 0; i < commands.size(); i++) { | ||||||
|  |             if (frame < commands[i].size()) { | ||||||
|  |                 TASCommand command = commands[i][frame]; | ||||||
|  |                 tas_data[i].buttons = command.buttons; | ||||||
|  |                 auto [l_axis_x, l_axis_y] = command.l_axis; | ||||||
|  |                 tas_data[i].axis[0] = l_axis_x; | ||||||
|  |                 tas_data[i].axis[1] = l_axis_y; | ||||||
|  |                 auto [r_axis_x, r_axis_y] = command.r_axis; | ||||||
|  |                 tas_data[i].axis[2] = r_axis_x; | ||||||
|  |                 tas_data[i].axis[3] = r_axis_y; | ||||||
|  |             } else { | ||||||
|  |                 tas_data[i] = {}; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         is_running = Settings::values.tas_loop.GetValue(); | ||||||
|  |         current_command = 0; | ||||||
|  |         tas_data.fill({}); | ||||||
|  |         if (!is_running) { | ||||||
|  |             SwapToStoredController(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     LOG_DEBUG(Input, "TAS inputs: {}", DebugInputs(tas_data)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TasAnalog Tas::ReadCommandAxis(const std::string& line) const { | ||||||
|  |     std::stringstream linestream(line); | ||||||
|  |     std::string segment; | ||||||
|  |     std::vector<std::string> seglist; | ||||||
|  | 
 | ||||||
|  |     while (std::getline(linestream, segment, ';')) { | ||||||
|  |         seglist.push_back(segment); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const float x = std::stof(seglist.at(0)) / 32767.0f; | ||||||
|  |     const float y = std::stof(seglist.at(1)) / 32767.0f; | ||||||
|  | 
 | ||||||
|  |     return {x, y}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | u32 Tas::ReadCommandButtons(const std::string& data) const { | ||||||
|  |     std::stringstream button_text(data); | ||||||
|  |     std::string line; | ||||||
|  |     u32 buttons = 0; | ||||||
|  |     while (std::getline(button_text, line, ';')) { | ||||||
|  |         for (auto [text, tas_button] : text_to_tas_button) { | ||||||
|  |             if (text == line) { | ||||||
|  |                 buttons |= static_cast<u32>(tas_button); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return buttons; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string Tas::WriteCommandAxis(TasAnalog data) const { | ||||||
|  |     auto [x, y] = data; | ||||||
|  |     std::string line; | ||||||
|  |     line += std::to_string(static_cast<int>(x * 32767)); | ||||||
|  |     line += ";"; | ||||||
|  |     line += std::to_string(static_cast<int>(y * 32767)); | ||||||
|  |     return line; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::string Tas::WriteCommandButtons(u32 data) const { | ||||||
|  |     if (data == 0) { | ||||||
|  |         return "NONE"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::string line; | ||||||
|  |     u32 index = 0; | ||||||
|  |     while (data > 0) { | ||||||
|  |         if ((data & 1) == 1) { | ||||||
|  |             for (auto [text, tas_button] : text_to_tas_button) { | ||||||
|  |                 if (tas_button == static_cast<TasButton>(1 << index)) { | ||||||
|  |                     if (line.size() > 0) { | ||||||
|  |                         line += ";"; | ||||||
|  |                     } | ||||||
|  |                     line += text; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         index++; | ||||||
|  |         data >>= 1; | ||||||
|  |     } | ||||||
|  |     return line; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::StartStop() { | ||||||
|  |     if (!Settings::values.tas_enable) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     if (is_running) { | ||||||
|  |         Stop(); | ||||||
|  |     } else { | ||||||
|  |         is_running = true; | ||||||
|  |         SwapToTasController(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::Stop() { | ||||||
|  |     is_running = false; | ||||||
|  |     SwapToStoredController(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::SwapToTasController() { | ||||||
|  |     if (!Settings::values.tas_swap_controllers) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     auto& players = Settings::values.players.GetValue(); | ||||||
|  |     for (std::size_t index = 0; index < players.size(); index++) { | ||||||
|  |         auto& player = players[index]; | ||||||
|  |         player_mappings[index] = player; | ||||||
|  | 
 | ||||||
|  |         // Only swap active controllers
 | ||||||
|  |         if (!player.connected) { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Common::ParamPackage tas_param; | ||||||
|  |         tas_param.Set("pad", static_cast<u8>(index)); | ||||||
|  |         auto button_mapping = GetButtonMappingForDevice(tas_param); | ||||||
|  |         auto analog_mapping = GetAnalogMappingForDevice(tas_param); | ||||||
|  |         auto& buttons = player.buttons; | ||||||
|  |         auto& analogs = player.analogs; | ||||||
|  | 
 | ||||||
|  |         for (std::size_t i = 0; i < buttons.size(); ++i) { | ||||||
|  |             buttons[i] = button_mapping[static_cast<Settings::NativeButton::Values>(i)].Serialize(); | ||||||
|  |         } | ||||||
|  |         for (std::size_t i = 0; i < analogs.size(); ++i) { | ||||||
|  |             analogs[i] = analog_mapping[static_cast<Settings::NativeAnalog::Values>(i)].Serialize(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     is_old_input_saved = true; | ||||||
|  |     Settings::values.is_device_reload_pending.store(true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::SwapToStoredController() { | ||||||
|  |     if (!is_old_input_saved) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     auto& players = Settings::values.players.GetValue(); | ||||||
|  |     for (std::size_t index = 0; index < players.size(); index++) { | ||||||
|  |         players[index] = player_mappings[index]; | ||||||
|  |     } | ||||||
|  |     is_old_input_saved = false; | ||||||
|  |     Settings::values.is_device_reload_pending.store(true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::Reset() { | ||||||
|  |     if (!Settings::values.tas_enable) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     needs_reset = true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool Tas::Record() { | ||||||
|  |     if (!Settings::values.tas_enable) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |     is_recording = !is_recording; | ||||||
|  |     return is_recording; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Tas::SaveRecording(bool overwrite_file) { | ||||||
|  |     if (is_recording) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     if (record_commands.empty()) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     WriteTasFile(u8"record.txt"); | ||||||
|  |     if (overwrite_file) { | ||||||
|  |         WriteTasFile(u8"script0-1.txt"); | ||||||
|  |     } | ||||||
|  |     needs_reset = true; | ||||||
|  |     record_commands.clear(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | InputCommon::ButtonMapping Tas::GetButtonMappingForDevice( | ||||||
|  |     const Common::ParamPackage& params) const { | ||||||
|  |     // This list is missing ZL/ZR since those are not considered buttons.
 | ||||||
|  |     // We will add those afterwards
 | ||||||
|  |     // This list also excludes any button that can't be really mapped
 | ||||||
|  |     static constexpr std::array<std::pair<Settings::NativeButton::Values, TasButton>, 20> | ||||||
|  |         switch_to_tas_button = { | ||||||
|  |             std::pair{Settings::NativeButton::A, TasButton::BUTTON_A}, | ||||||
|  |             {Settings::NativeButton::B, TasButton::BUTTON_B}, | ||||||
|  |             {Settings::NativeButton::X, TasButton::BUTTON_X}, | ||||||
|  |             {Settings::NativeButton::Y, TasButton::BUTTON_Y}, | ||||||
|  |             {Settings::NativeButton::LStick, TasButton::STICK_L}, | ||||||
|  |             {Settings::NativeButton::RStick, TasButton::STICK_R}, | ||||||
|  |             {Settings::NativeButton::L, TasButton::TRIGGER_L}, | ||||||
|  |             {Settings::NativeButton::R, TasButton::TRIGGER_R}, | ||||||
|  |             {Settings::NativeButton::Plus, TasButton::BUTTON_PLUS}, | ||||||
|  |             {Settings::NativeButton::Minus, TasButton::BUTTON_MINUS}, | ||||||
|  |             {Settings::NativeButton::DLeft, TasButton::BUTTON_LEFT}, | ||||||
|  |             {Settings::NativeButton::DUp, TasButton::BUTTON_UP}, | ||||||
|  |             {Settings::NativeButton::DRight, TasButton::BUTTON_RIGHT}, | ||||||
|  |             {Settings::NativeButton::DDown, TasButton::BUTTON_DOWN}, | ||||||
|  |             {Settings::NativeButton::SL, TasButton::BUTTON_SL}, | ||||||
|  |             {Settings::NativeButton::SR, TasButton::BUTTON_SR}, | ||||||
|  |             {Settings::NativeButton::Screenshot, TasButton::BUTTON_CAPTURE}, | ||||||
|  |             {Settings::NativeButton::Home, TasButton::BUTTON_HOME}, | ||||||
|  |             {Settings::NativeButton::ZL, TasButton::TRIGGER_ZL}, | ||||||
|  |             {Settings::NativeButton::ZR, TasButton::TRIGGER_ZR}, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |     InputCommon::ButtonMapping mapping{}; | ||||||
|  |     for (const auto& [switch_button, tas_button] : switch_to_tas_button) { | ||||||
|  |         Common::ParamPackage button_params({{"engine", "tas"}}); | ||||||
|  |         button_params.Set("pad", params.Get("pad", 0)); | ||||||
|  |         button_params.Set("button", static_cast<int>(tas_button)); | ||||||
|  |         mapping.insert_or_assign(switch_button, std::move(button_params)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return mapping; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | InputCommon::AnalogMapping Tas::GetAnalogMappingForDevice( | ||||||
|  |     const Common::ParamPackage& params) const { | ||||||
|  | 
 | ||||||
|  |     InputCommon::AnalogMapping mapping = {}; | ||||||
|  |     Common::ParamPackage left_analog_params; | ||||||
|  |     left_analog_params.Set("engine", "tas"); | ||||||
|  |     left_analog_params.Set("pad", params.Get("pad", 0)); | ||||||
|  |     left_analog_params.Set("axis_x", static_cast<int>(TasAxes::StickX)); | ||||||
|  |     left_analog_params.Set("axis_y", static_cast<int>(TasAxes::StickY)); | ||||||
|  |     mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params)); | ||||||
|  |     Common::ParamPackage right_analog_params; | ||||||
|  |     right_analog_params.Set("engine", "tas"); | ||||||
|  |     right_analog_params.Set("pad", params.Get("pad", 0)); | ||||||
|  |     right_analog_params.Set("axis_x", static_cast<int>(TasAxes::SubstickX)); | ||||||
|  |     right_analog_params.Set("axis_y", static_cast<int>(TasAxes::SubstickY)); | ||||||
|  |     mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params)); | ||||||
|  |     return mapping; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const TasData& Tas::GetTasState(std::size_t pad) const { | ||||||
|  |     return tas_data[pad]; | ||||||
|  | } | ||||||
|  | } // namespace TasInput
 | ||||||
							
								
								
									
										237
									
								
								src/input_common/tas/tas_input.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								src/input_common/tas/tas_input.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,237 @@ | ||||||
|  | // Copyright 2020 yuzu Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <array> | ||||||
|  | 
 | ||||||
|  | #include "common/common_types.h" | ||||||
|  | #include "common/settings_input.h" | ||||||
|  | #include "core/frontend/input.h" | ||||||
|  | #include "input_common/main.h" | ||||||
|  | 
 | ||||||
|  | /*
 | ||||||
|  | To play back TAS scripts on Yuzu, select the folder with scripts in the configuration menu below | ||||||
|  | Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt | ||||||
|  | for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players). | ||||||
|  | 
 | ||||||
|  | A script file has the same format as TAS-nx uses, so final files will look like this: | ||||||
|  | 
 | ||||||
|  | 1 KEY_B 0;0 0;0 | ||||||
|  | 6 KEY_ZL 0;0 0;0 | ||||||
|  | 41 KEY_ZL;KEY_Y 0;0 0;0 | ||||||
|  | 43 KEY_X;KEY_A 32767;0 0;0 | ||||||
|  | 44 KEY_A 32767;0 0;0 | ||||||
|  | 45 KEY_A 32767;0 0;0 | ||||||
|  | 46 KEY_A 32767;0 0;0 | ||||||
|  | 47 KEY_A 32767;0 0;0 | ||||||
|  | 
 | ||||||
|  | After placing the file at the correct location, it can be read into Yuzu with the (default) hotkey | ||||||
|  | CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file | ||||||
|  | has. Playback can be started or stopped using CTRL+F5. | ||||||
|  | 
 | ||||||
|  | However, for playback to actually work, the correct input device has to be selected: In the Controls | ||||||
|  | menu, select TAS from the device list for the controller that the script should be played on. | ||||||
|  | 
 | ||||||
|  | Recording a new script file is really simple: Just make sure that the proper device (not TAS) is | ||||||
|  | connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke | ||||||
|  | again (CTRL+F7). The new script will be saved at the location previously selected, as the filename | ||||||
|  | record.txt. | ||||||
|  | 
 | ||||||
|  | For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller | ||||||
|  | P1). | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | namespace TasInput { | ||||||
|  | 
 | ||||||
|  | constexpr size_t PLAYER_NUMBER = 8; | ||||||
|  | 
 | ||||||
|  | using TasAnalog = std::pair<float, float>; | ||||||
|  | 
 | ||||||
|  | enum class TasState { | ||||||
|  |     Running, | ||||||
|  |     Recording, | ||||||
|  |     Stopped, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | enum class TasButton : u32 { | ||||||
|  |     BUTTON_A = 1U << 0, | ||||||
|  |     BUTTON_B = 1U << 1, | ||||||
|  |     BUTTON_X = 1U << 2, | ||||||
|  |     BUTTON_Y = 1U << 3, | ||||||
|  |     STICK_L = 1U << 4, | ||||||
|  |     STICK_R = 1U << 5, | ||||||
|  |     TRIGGER_L = 1U << 6, | ||||||
|  |     TRIGGER_R = 1U << 7, | ||||||
|  |     TRIGGER_ZL = 1U << 8, | ||||||
|  |     TRIGGER_ZR = 1U << 9, | ||||||
|  |     BUTTON_PLUS = 1U << 10, | ||||||
|  |     BUTTON_MINUS = 1U << 11, | ||||||
|  |     BUTTON_LEFT = 1U << 12, | ||||||
|  |     BUTTON_UP = 1U << 13, | ||||||
|  |     BUTTON_RIGHT = 1U << 14, | ||||||
|  |     BUTTON_DOWN = 1U << 15, | ||||||
|  |     BUTTON_SL = 1U << 16, | ||||||
|  |     BUTTON_SR = 1U << 17, | ||||||
|  |     BUTTON_HOME = 1U << 18, | ||||||
|  |     BUTTON_CAPTURE = 1U << 19, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | enum class TasAxes : u8 { | ||||||
|  |     StickX, | ||||||
|  |     StickY, | ||||||
|  |     SubstickX, | ||||||
|  |     SubstickY, | ||||||
|  |     Undefined, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct TasData { | ||||||
|  |     u32 buttons{}; | ||||||
|  |     std::array<float, 4> axis{}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | class Tas { | ||||||
|  | public: | ||||||
|  |     Tas(); | ||||||
|  |     ~Tas(); | ||||||
|  | 
 | ||||||
|  |     // Changes the input status that will be stored in each frame
 | ||||||
|  |     void RecordInput(u32 buttons, const std::array<std::pair<float, float>, 2>& axes); | ||||||
|  | 
 | ||||||
|  |     // Main loop that records or executes input
 | ||||||
|  |     void UpdateThread(); | ||||||
|  | 
 | ||||||
|  |     //  Sets the flag to start or stop the TAS command excecution and swaps controllers profiles
 | ||||||
|  |     void StartStop(); | ||||||
|  | 
 | ||||||
|  |     //  Stop the TAS and reverts any controller profile
 | ||||||
|  |     void Stop(); | ||||||
|  | 
 | ||||||
|  |     // Sets the flag to reload the file and start from the begining in the next update
 | ||||||
|  |     void Reset(); | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Sets the flag to enable or disable recording of inputs | ||||||
|  |      * @return Returns true if the current recording status is enabled | ||||||
|  |      */ | ||||||
|  |     bool Record(); | ||||||
|  | 
 | ||||||
|  |     // Saves contents of record_commands on a file if overwrite is enabled player 1 will be
 | ||||||
|  |     // overwritten with the recorded commands
 | ||||||
|  |     void SaveRecording(bool overwrite_file); | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Returns the current status values of TAS playback/recording | ||||||
|  |      * @return Tuple of | ||||||
|  |      * TasState indicating the current state out of Running, Recording or Stopped ; | ||||||
|  |      * Current playback progress or amount of frames (so far) for Recording ; | ||||||
|  |      * Total length of script file currently loaded or amount of frames (so far) for Recording | ||||||
|  |      */ | ||||||
|  |     std::tuple<TasState, size_t, size_t> GetStatus() const; | ||||||
|  | 
 | ||||||
|  |     // Retuns an array of the default button mappings
 | ||||||
|  |     InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) const; | ||||||
|  | 
 | ||||||
|  |     // Retuns an array of the default analog mappings
 | ||||||
|  |     InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) const; | ||||||
|  |     [[nodiscard]] const TasData& GetTasState(std::size_t pad) const; | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     struct TASCommand { | ||||||
|  |         u32 buttons{}; | ||||||
|  |         TasAnalog l_axis{}; | ||||||
|  |         TasAnalog r_axis{}; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Loads TAS files from all players
 | ||||||
|  |     void LoadTasFiles(); | ||||||
|  | 
 | ||||||
|  |     // Loads TAS file from the specified player
 | ||||||
|  |     void LoadTasFile(size_t player_index); | ||||||
|  | 
 | ||||||
|  |     // Writes a TAS file from the recorded commands
 | ||||||
|  |     void WriteTasFile(std::u8string file_name); | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Parses a string containing the axis values with the following format "x;y" | ||||||
|  |      * X and Y have a range from -32767 to 32767 | ||||||
|  |      * @return Returns a TAS analog object with axis values with range from -1.0 to 1.0 | ||||||
|  |      */ | ||||||
|  |     TasAnalog ReadCommandAxis(const std::string& line) const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Parses a string containing the button values with the following format "a;b;c;d..." | ||||||
|  |      * Each button is represented by it's text format specified in text_to_tas_button array | ||||||
|  |      * @return Returns a u32 with each bit representing the status of a button | ||||||
|  |      */ | ||||||
|  |     u32 ReadCommandButtons(const std::string& line) const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Converts an u32 containing the button status into the text equivalent | ||||||
|  |      * @return Returns a string with the name of the buttons to be written to the file | ||||||
|  |      */ | ||||||
|  |     std::string WriteCommandButtons(u32 data) const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Converts an TAS analog object containing the axis status into the text equivalent | ||||||
|  |      * @return Returns a string with the value of the axis to be written to the file | ||||||
|  |      */ | ||||||
|  |     std::string WriteCommandAxis(TasAnalog data) const; | ||||||
|  | 
 | ||||||
|  |     // Inverts the Y axis polarity
 | ||||||
|  |     std::pair<float, float> FlipAxisY(std::pair<float, float> old); | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Converts an u32 containing the button status into the text equivalent | ||||||
|  |      * @return Returns a string with the name of the buttons to be printed on console | ||||||
|  |      */ | ||||||
|  |     std::string DebugButtons(u32 buttons) const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Converts an TAS analog object containing the axis status into the text equivalent | ||||||
|  |      * @return Returns a string with the value of the axis to be printed on console | ||||||
|  |      */ | ||||||
|  |     std::string DebugJoystick(float x, float y) const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Converts the given TAS status into the text equivalent | ||||||
|  |      * @return Returns a string with the value of the TAS status to be printed on console | ||||||
|  |      */ | ||||||
|  |     std::string DebugInput(const TasData& data) const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Converts the given TAS status of multiple players into the text equivalent | ||||||
|  |      * @return Returns a string with the value of the status of all TAS players to be printed on | ||||||
|  |      * console | ||||||
|  |      */ | ||||||
|  |     std::string DebugInputs(const std::array<TasData, PLAYER_NUMBER>& arr) const; | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Converts an u32 containing the button status into the text equivalent | ||||||
|  |      * @return Returns a string with the name of the buttons | ||||||
|  |      */ | ||||||
|  |     std::string ButtonsToString(u32 button) const; | ||||||
|  | 
 | ||||||
|  |     // Stores current controller configuration and sets a TAS controller for every active controller
 | ||||||
|  |     // to the current config
 | ||||||
|  |     void SwapToTasController(); | ||||||
|  | 
 | ||||||
|  |     // Sets the stored controller configuration to the current config
 | ||||||
|  |     void SwapToStoredController(); | ||||||
|  | 
 | ||||||
|  |     size_t script_length{0}; | ||||||
|  |     std::array<TasData, PLAYER_NUMBER> tas_data; | ||||||
|  |     bool is_old_input_saved{false}; | ||||||
|  |     bool is_recording{false}; | ||||||
|  |     bool is_running{false}; | ||||||
|  |     bool needs_reset{false}; | ||||||
|  |     std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{}; | ||||||
|  |     std::vector<TASCommand> record_commands{}; | ||||||
|  |     size_t current_command{0}; | ||||||
|  |     TASCommand last_input{}; // only used for recording
 | ||||||
|  | 
 | ||||||
|  |     // Old settings for swapping controllers
 | ||||||
|  |     std::array<Settings::PlayerInput, 10> player_mappings; | ||||||
|  | }; | ||||||
|  | } // namespace TasInput
 | ||||||
							
								
								
									
										101
									
								
								src/input_common/tas/tas_poller.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/input_common/tas/tas_poller.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,101 @@ | ||||||
|  | // Copyright 2021 yuzu Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <mutex> | ||||||
|  | #include <utility> | ||||||
|  | 
 | ||||||
|  | #include "common/settings.h" | ||||||
|  | #include "common/threadsafe_queue.h" | ||||||
|  | #include "input_common/tas/tas_input.h" | ||||||
|  | #include "input_common/tas/tas_poller.h" | ||||||
|  | 
 | ||||||
|  | namespace InputCommon { | ||||||
|  | 
 | ||||||
|  | class TasButton final : public Input::ButtonDevice { | ||||||
|  | public: | ||||||
|  |     explicit TasButton(u32 button_, u32 pad_, const TasInput::Tas* tas_input_) | ||||||
|  |         : button(button_), pad(pad_), tas_input(tas_input_) {} | ||||||
|  | 
 | ||||||
|  |     bool GetStatus() const override { | ||||||
|  |         return (tas_input->GetTasState(pad).buttons & button) != 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     const u32 button; | ||||||
|  |     const u32 pad; | ||||||
|  |     const TasInput::Tas* tas_input; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | TasButtonFactory::TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_) | ||||||
|  |     : tas_input(std::move(tas_input_)) {} | ||||||
|  | 
 | ||||||
|  | std::unique_ptr<Input::ButtonDevice> TasButtonFactory::Create(const Common::ParamPackage& params) { | ||||||
|  |     const auto button_id = params.Get("button", 0); | ||||||
|  |     const auto pad = params.Get("pad", 0); | ||||||
|  | 
 | ||||||
|  |     return std::make_unique<TasButton>(button_id, pad, tas_input.get()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class TasAnalog final : public Input::AnalogDevice { | ||||||
|  | public: | ||||||
|  |     explicit TasAnalog(u32 pad_, u32 axis_x_, u32 axis_y_, const TasInput::Tas* tas_input_) | ||||||
|  |         : pad(pad_), axis_x(axis_x_), axis_y(axis_y_), tas_input(tas_input_) {} | ||||||
|  | 
 | ||||||
|  |     float GetAxis(u32 axis) const { | ||||||
|  |         std::lock_guard lock{mutex}; | ||||||
|  |         return tas_input->GetTasState(pad).axis.at(axis); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::pair<float, float> GetAnalog(u32 analog_axis_x, u32 analog_axis_y) const { | ||||||
|  |         float x = GetAxis(analog_axis_x); | ||||||
|  |         float y = GetAxis(analog_axis_y); | ||||||
|  | 
 | ||||||
|  |         // Make sure the coordinates are in the unit circle,
 | ||||||
|  |         // otherwise normalize it.
 | ||||||
|  |         float r = x * x + y * y; | ||||||
|  |         if (r > 1.0f) { | ||||||
|  |             r = std::sqrt(r); | ||||||
|  |             x /= r; | ||||||
|  |             y /= r; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return {x, y}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     std::tuple<float, float> GetStatus() const override { | ||||||
|  |         return GetAnalog(axis_x, axis_y); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Input::AnalogProperties GetAnalogProperties() const override { | ||||||
|  |         return {0.0f, 1.0f, 0.5f}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     const u32 pad; | ||||||
|  |     const u32 axis_x; | ||||||
|  |     const u32 axis_y; | ||||||
|  |     const TasInput::Tas* tas_input; | ||||||
|  |     mutable std::mutex mutex; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /// An analog device factory that creates analog devices from GC Adapter
 | ||||||
|  | TasAnalogFactory::TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_) | ||||||
|  |     : tas_input(std::move(tas_input_)) {} | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Creates analog device from joystick axes | ||||||
|  |  * @param params contains parameters for creating the device: | ||||||
|  |  *     - "port": the nth gcpad on the adapter | ||||||
|  |  *     - "axis_x": the index of the axis to be bind as x-axis | ||||||
|  |  *     - "axis_y": the index of the axis to be bind as y-axis | ||||||
|  |  */ | ||||||
|  | std::unique_ptr<Input::AnalogDevice> TasAnalogFactory::Create(const Common::ParamPackage& params) { | ||||||
|  |     const auto pad = static_cast<u32>(params.Get("pad", 0)); | ||||||
|  |     const auto axis_x = static_cast<u32>(params.Get("axis_x", 0)); | ||||||
|  |     const auto axis_y = static_cast<u32>(params.Get("axis_y", 1)); | ||||||
|  | 
 | ||||||
|  |     return std::make_unique<TasAnalog>(pad, axis_x, axis_y, tas_input.get()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace InputCommon
 | ||||||
							
								
								
									
										43
									
								
								src/input_common/tas/tas_poller.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/input_common/tas/tas_poller.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | ||||||
|  | // Copyright 2021 yuzu Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <memory> | ||||||
|  | #include "core/frontend/input.h" | ||||||
|  | #include "input_common/tas/tas_input.h" | ||||||
|  | 
 | ||||||
|  | namespace InputCommon { | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * A button device factory representing a tas bot. It receives tas events and forward them | ||||||
|  |  * to all button devices it created. | ||||||
|  |  */ | ||||||
|  | class TasButtonFactory final : public Input::Factory<Input::ButtonDevice> { | ||||||
|  | public: | ||||||
|  |     explicit TasButtonFactory(std::shared_ptr<TasInput::Tas> tas_input_); | ||||||
|  | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Creates a button device from a button press | ||||||
|  |      * @param params contains parameters for creating the device: | ||||||
|  |      *     - "code": the code of the key to bind with the button | ||||||
|  |      */ | ||||||
|  |     std::unique_ptr<Input::ButtonDevice> Create(const Common::ParamPackage& params) override; | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     std::shared_ptr<TasInput::Tas> tas_input; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /// An analog device factory that creates analog devices from tas
 | ||||||
|  | class TasAnalogFactory final : public Input::Factory<Input::AnalogDevice> { | ||||||
|  | public: | ||||||
|  |     explicit TasAnalogFactory(std::shared_ptr<TasInput::Tas> tas_input_); | ||||||
|  | 
 | ||||||
|  |     std::unique_ptr<Input::AnalogDevice> Create(const Common::ParamPackage& params) override; | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     std::shared_ptr<TasInput::Tas> tas_input; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | } // namespace InputCommon
 | ||||||
|  | @ -108,6 +108,9 @@ add_executable(yuzu | ||||||
|     configuration/configure_system.cpp |     configuration/configure_system.cpp | ||||||
|     configuration/configure_system.h |     configuration/configure_system.h | ||||||
|     configuration/configure_system.ui |     configuration/configure_system.ui | ||||||
|  |     configuration/configure_tas.cpp | ||||||
|  |     configuration/configure_tas.h | ||||||
|  |     configuration/configure_tas.ui | ||||||
|     configuration/configure_touch_from_button.cpp |     configuration/configure_touch_from_button.cpp | ||||||
|     configuration/configure_touch_from_button.h |     configuration/configure_touch_from_button.h | ||||||
|     configuration/configure_touch_from_button.ui |     configuration/configure_touch_from_button.ui | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ | ||||||
| #include "input_common/keyboard.h" | #include "input_common/keyboard.h" | ||||||
| #include "input_common/main.h" | #include "input_common/main.h" | ||||||
| #include "input_common/mouse/mouse_input.h" | #include "input_common/mouse/mouse_input.h" | ||||||
|  | #include "input_common/tas/tas_input.h" | ||||||
| #include "video_core/renderer_base.h" | #include "video_core/renderer_base.h" | ||||||
| #include "video_core/video_core.h" | #include "video_core/video_core.h" | ||||||
| #include "yuzu/bootmanager.h" | #include "yuzu/bootmanager.h" | ||||||
|  | @ -312,6 +313,7 @@ GRenderWindow::~GRenderWindow() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GRenderWindow::OnFrameDisplayed() { | void GRenderWindow::OnFrameDisplayed() { | ||||||
|  |     input_subsystem->GetTas()->UpdateThread(); | ||||||
|     if (!first_frame) { |     if (!first_frame) { | ||||||
|         first_frame = true; |         first_frame = true; | ||||||
|         emit FirstFrameDisplayed(); |         emit FirstFrameDisplayed(); | ||||||
|  |  | ||||||
|  | @ -221,7 +221,7 @@ const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> Config::default | ||||||
| // This must be in alphabetical order according to action name as it must have the same order as
 | // This must be in alphabetical order according to action name as it must have the same order as
 | ||||||
| // UISetting::values.shortcuts, which is alphabetically ordered.
 | // UISetting::values.shortcuts, which is alphabetically ordered.
 | ||||||
| // clang-format off
 | // clang-format off
 | ||||||
| const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{ | const std::array<UISettings::Shortcut, 21> Config::default_hotkeys{{ | ||||||
|     {QStringLiteral("Capture Screenshot"),       QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}}, |     {QStringLiteral("Capture Screenshot"),       QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}}, | ||||||
|     {QStringLiteral("Change Docked Mode"),       QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::ApplicationShortcut}}, |     {QStringLiteral("Change Docked Mode"),       QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::ApplicationShortcut}}, | ||||||
|     {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, |     {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, | ||||||
|  | @ -235,6 +235,9 @@ const std::array<UISettings::Shortcut, 18> Config::default_hotkeys{{ | ||||||
|     {QStringLiteral("Mute Audio"),               QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}}, |     {QStringLiteral("Mute Audio"),               QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}}, | ||||||
|     {QStringLiteral("Restart Emulation"),        QStringLiteral("Main Window"), {QStringLiteral("F6"), Qt::WindowShortcut}}, |     {QStringLiteral("Restart Emulation"),        QStringLiteral("Main Window"), {QStringLiteral("F6"), Qt::WindowShortcut}}, | ||||||
|     {QStringLiteral("Stop Emulation"),           QStringLiteral("Main Window"), {QStringLiteral("F5"), Qt::WindowShortcut}}, |     {QStringLiteral("Stop Emulation"),           QStringLiteral("Main Window"), {QStringLiteral("F5"), Qt::WindowShortcut}}, | ||||||
|  |     {QStringLiteral("TAS Start/Stop"),           QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F5"), Qt::ApplicationShortcut}}, | ||||||
|  |     {QStringLiteral("TAS Reset"),                QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F6"), Qt::ApplicationShortcut}}, | ||||||
|  |     {QStringLiteral("TAS Record"),               QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F7"), Qt::ApplicationShortcut}}, | ||||||
|     {QStringLiteral("Toggle Filter Bar"),        QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F"), Qt::WindowShortcut}}, |     {QStringLiteral("Toggle Filter Bar"),        QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F"), Qt::WindowShortcut}}, | ||||||
|     {QStringLiteral("Toggle Framerate Limit"),   QStringLiteral("Main Window"), {QStringLiteral("Ctrl+U"), Qt::ApplicationShortcut}}, |     {QStringLiteral("Toggle Framerate Limit"),   QStringLiteral("Main Window"), {QStringLiteral("Ctrl+U"), Qt::ApplicationShortcut}}, | ||||||
|     {QStringLiteral("Toggle Mouse Panning"),     QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F9"), Qt::ApplicationShortcut}}, |     {QStringLiteral("Toggle Mouse Panning"),     QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F9"), Qt::ApplicationShortcut}}, | ||||||
|  | @ -564,6 +567,11 @@ void Config::ReadControlValues() { | ||||||
|     Settings::values.mouse_panning = false; |     Settings::values.mouse_panning = false; | ||||||
|     ReadBasicSetting(Settings::values.mouse_panning_sensitivity); |     ReadBasicSetting(Settings::values.mouse_panning_sensitivity); | ||||||
| 
 | 
 | ||||||
|  |     ReadBasicSetting(Settings::values.tas_enable); | ||||||
|  |     ReadBasicSetting(Settings::values.tas_loop); | ||||||
|  |     ReadBasicSetting(Settings::values.tas_swap_controllers); | ||||||
|  |     ReadBasicSetting(Settings::values.pause_tas_on_load); | ||||||
|  | 
 | ||||||
|     ReadGlobalSetting(Settings::values.use_docked_mode); |     ReadGlobalSetting(Settings::values.use_docked_mode); | ||||||
| 
 | 
 | ||||||
|     // Disable docked mode if handheld is selected
 |     // Disable docked mode if handheld is selected
 | ||||||
|  | @ -661,6 +669,13 @@ void Config::ReadDataStorageValues() { | ||||||
|                     QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))) |                     QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))) | ||||||
|             .toString() |             .toString() | ||||||
|             .toStdString()); |             .toStdString()); | ||||||
|  |     FS::SetYuzuPath(FS::YuzuPath::TASDir, | ||||||
|  |                     qt_config | ||||||
|  |                         ->value(QStringLiteral("tas_directory"), | ||||||
|  |                                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir))) | ||||||
|  |                         .toString() | ||||||
|  |                         .toStdString()); | ||||||
|  | 
 | ||||||
|     ReadBasicSetting(Settings::values.gamecard_inserted); |     ReadBasicSetting(Settings::values.gamecard_inserted); | ||||||
|     ReadBasicSetting(Settings::values.gamecard_current_game); |     ReadBasicSetting(Settings::values.gamecard_current_game); | ||||||
|     ReadBasicSetting(Settings::values.gamecard_path); |     ReadBasicSetting(Settings::values.gamecard_path); | ||||||
|  | @ -1188,6 +1203,11 @@ void Config::SaveControlValues() { | ||||||
|     WriteBasicSetting(Settings::values.emulate_analog_keyboard); |     WriteBasicSetting(Settings::values.emulate_analog_keyboard); | ||||||
|     WriteBasicSetting(Settings::values.mouse_panning_sensitivity); |     WriteBasicSetting(Settings::values.mouse_panning_sensitivity); | ||||||
| 
 | 
 | ||||||
|  |     WriteBasicSetting(Settings::values.tas_enable); | ||||||
|  |     WriteBasicSetting(Settings::values.tas_loop); | ||||||
|  |     WriteBasicSetting(Settings::values.tas_swap_controllers); | ||||||
|  |     WriteBasicSetting(Settings::values.pause_tas_on_load); | ||||||
|  | 
 | ||||||
|     qt_config->endGroup(); |     qt_config->endGroup(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1215,6 +1235,10 @@ void Config::SaveDataStorageValues() { | ||||||
|     WriteSetting(QStringLiteral("dump_directory"), |     WriteSetting(QStringLiteral("dump_directory"), | ||||||
|                  QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)), |                  QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)), | ||||||
|                  QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))); |                  QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir))); | ||||||
|  |     WriteSetting(QStringLiteral("tas_directory"), | ||||||
|  |                  QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir)), | ||||||
|  |                  QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::TASDir))); | ||||||
|  | 
 | ||||||
|     WriteBasicSetting(Settings::values.gamecard_inserted); |     WriteBasicSetting(Settings::values.gamecard_inserted); | ||||||
|     WriteBasicSetting(Settings::values.gamecard_current_game); |     WriteBasicSetting(Settings::values.gamecard_current_game); | ||||||
|     WriteBasicSetting(Settings::values.gamecard_path); |     WriteBasicSetting(Settings::values.gamecard_path); | ||||||
|  |  | ||||||
|  | @ -42,7 +42,7 @@ public: | ||||||
|         default_mouse_buttons; |         default_mouse_buttons; | ||||||
|     static const std::array<int, Settings::NativeKeyboard::NumKeyboardKeys> default_keyboard_keys; |     static const std::array<int, Settings::NativeKeyboard::NumKeyboardKeys> default_keyboard_keys; | ||||||
|     static const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> default_keyboard_mods; |     static const std::array<int, Settings::NativeKeyboard::NumKeyboardMods> default_keyboard_mods; | ||||||
|     static const std::array<UISettings::Shortcut, 18> default_hotkeys; |     static const std::array<UISettings::Shortcut, 21> default_hotkeys; | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|     void Initialize(const std::string& config_name); |     void Initialize(const std::string& config_name); | ||||||
|  |  | ||||||
|  | @ -124,6 +124,19 @@ QString ButtonToText(const Common::ParamPackage& param) { | ||||||
|         return GetKeyName(param.Get("code", 0)); |         return GetKeyName(param.Get("code", 0)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (param.Get("engine", "") == "tas") { | ||||||
|  |         if (param.Has("axis")) { | ||||||
|  |             const QString axis_str = QString::fromStdString(param.Get("axis", "")); | ||||||
|  | 
 | ||||||
|  |             return QObject::tr("TAS Axis %1").arg(axis_str); | ||||||
|  |         } | ||||||
|  |         if (param.Has("button")) { | ||||||
|  |             const QString button_str = QString::number(int(std::log2(param.Get("button", 0)))); | ||||||
|  |             return QObject::tr("TAS Btn %1").arg(button_str); | ||||||
|  |         } | ||||||
|  |         return GetKeyName(param.Get("code", 0)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (param.Get("engine", "") == "cemuhookudp") { |     if (param.Get("engine", "") == "cemuhookudp") { | ||||||
|         if (param.Has("pad_index")) { |         if (param.Has("pad_index")) { | ||||||
|             const QString motion_str = QString::fromStdString(param.Get("pad_index", "")); |             const QString motion_str = QString::fromStdString(param.Get("pad_index", "")); | ||||||
|  | @ -187,7 +200,8 @@ QString AnalogToText(const Common::ParamPackage& param, const std::string& dir) | ||||||
|     const QString axis_y_str = QString::fromStdString(param.Get("axis_y", "")); |     const QString axis_y_str = QString::fromStdString(param.Get("axis_y", "")); | ||||||
|     const bool invert_x = param.Get("invert_x", "+") == "-"; |     const bool invert_x = param.Get("invert_x", "+") == "-"; | ||||||
|     const bool invert_y = param.Get("invert_y", "+") == "-"; |     const bool invert_y = param.Get("invert_y", "+") == "-"; | ||||||
|     if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse") { |     if (engine_str == "sdl" || engine_str == "gcpad" || engine_str == "mouse" || | ||||||
|  |         engine_str == "tas") { | ||||||
|         if (dir == "modifier") { |         if (dir == "modifier") { | ||||||
|             return QObject::tr("[unused]"); |             return QObject::tr("[unused]"); | ||||||
|         } |         } | ||||||
|  | @ -926,9 +940,9 @@ void ConfigureInputPlayer::UpdateUI() { | ||||||
| 
 | 
 | ||||||
|         int slider_value; |         int slider_value; | ||||||
|         auto& param = analogs_param[analog_id]; |         auto& param = analogs_param[analog_id]; | ||||||
|         const bool is_controller = param.Get("engine", "") == "sdl" || |         const bool is_controller = | ||||||
|                                    param.Get("engine", "") == "gcpad" || |             param.Get("engine", "") == "sdl" || param.Get("engine", "") == "gcpad" || | ||||||
|                                    param.Get("engine", "") == "mouse"; |             param.Get("engine", "") == "mouse" || param.Get("engine", "") == "tas"; | ||||||
| 
 | 
 | ||||||
|         if (is_controller) { |         if (is_controller) { | ||||||
|             if (!param.Has("deadzone")) { |             if (!param.Has("deadzone")) { | ||||||
|  | @ -1045,8 +1059,12 @@ int ConfigureInputPlayer::GetIndexFromControllerType(Settings::ControllerType ty | ||||||
| void ConfigureInputPlayer::UpdateInputDevices() { | void ConfigureInputPlayer::UpdateInputDevices() { | ||||||
|     input_devices = input_subsystem->GetInputDevices(); |     input_devices = input_subsystem->GetInputDevices(); | ||||||
|     ui->comboDevices->clear(); |     ui->comboDevices->clear(); | ||||||
|     for (auto device : input_devices) { |     for (auto& device : input_devices) { | ||||||
|         ui->comboDevices->addItem(QString::fromStdString(device.Get("display", "Unknown")), {}); |         const std::string display = device.Get("display", "Unknown"); | ||||||
|  |         ui->comboDevices->addItem(QString::fromStdString(display), {}); | ||||||
|  |         if (display == "TAS") { | ||||||
|  |             device.Set("pad", static_cast<u8>(player_index)); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -175,7 +175,7 @@ void PlayerControlPreview::ResetInputs() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void PlayerControlPreview::UpdateInput() { | void PlayerControlPreview::UpdateInput() { | ||||||
|     if (!is_enabled && !mapping_active) { |     if (!is_enabled && !mapping_active && !Settings::values.tas_enable) { | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     bool input_changed = false; |     bool input_changed = false; | ||||||
|  | @ -222,6 +222,19 @@ void PlayerControlPreview::UpdateInput() { | ||||||
| 
 | 
 | ||||||
|     if (input_changed) { |     if (input_changed) { | ||||||
|         update(); |         update(); | ||||||
|  |         if (controller_callback.input != nullptr) { | ||||||
|  |             ControllerInput input{ | ||||||
|  |                 .axis_values = {std::pair<float, float>{ | ||||||
|  |                                     axis_values[Settings::NativeAnalog::LStick].value.x(), | ||||||
|  |                                     axis_values[Settings::NativeAnalog::LStick].value.y()}, | ||||||
|  |                                 std::pair<float, float>{ | ||||||
|  |                                     axis_values[Settings::NativeAnalog::RStick].value.x(), | ||||||
|  |                                     axis_values[Settings::NativeAnalog::RStick].value.y()}}, | ||||||
|  |                 .button_values = button_values, | ||||||
|  |                 .changed = true, | ||||||
|  |             }; | ||||||
|  |             controller_callback.input(std::move(input)); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (mapping_active) { |     if (mapping_active) { | ||||||
|  | @ -229,6 +242,10 @@ void PlayerControlPreview::UpdateInput() { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void PlayerControlPreview::SetCallBack(ControllerCallback callback_) { | ||||||
|  |     controller_callback = std::move(callback_); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void PlayerControlPreview::paintEvent(QPaintEvent* event) { | void PlayerControlPreview::paintEvent(QPaintEvent* event) { | ||||||
|     QFrame::paintEvent(event); |     QFrame::paintEvent(event); | ||||||
|     QPainter p(this); |     QPainter p(this); | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ | ||||||
| #include <QPointer> | #include <QPointer> | ||||||
| #include "common/settings.h" | #include "common/settings.h" | ||||||
| #include "core/frontend/input.h" | #include "core/frontend/input.h" | ||||||
|  | #include "yuzu/debugger/controller.h" | ||||||
| 
 | 
 | ||||||
| class QLabel; | class QLabel; | ||||||
| 
 | 
 | ||||||
|  | @ -33,6 +34,7 @@ public: | ||||||
|     void BeginMappingAnalog(std::size_t button_id); |     void BeginMappingAnalog(std::size_t button_id); | ||||||
|     void EndMapping(); |     void EndMapping(); | ||||||
|     void UpdateInput(); |     void UpdateInput(); | ||||||
|  |     void SetCallBack(ControllerCallback callback_); | ||||||
| 
 | 
 | ||||||
| protected: | protected: | ||||||
|     void paintEvent(QPaintEvent* event) override; |     void paintEvent(QPaintEvent* event) override; | ||||||
|  | @ -181,6 +183,7 @@ private: | ||||||
|     using StickArray = |     using StickArray = | ||||||
|         std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID>; |         std::array<std::unique_ptr<Input::AnalogDevice>, Settings::NativeAnalog::NUM_STICKS_HID>; | ||||||
| 
 | 
 | ||||||
|  |     ControllerCallback controller_callback; | ||||||
|     bool is_enabled{}; |     bool is_enabled{}; | ||||||
|     bool mapping_active{}; |     bool mapping_active{}; | ||||||
|     int blink_counter{}; |     int blink_counter{}; | ||||||
|  |  | ||||||
							
								
								
									
										84
									
								
								src/yuzu/configuration/configure_tas.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/yuzu/configuration/configure_tas.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,84 @@ | ||||||
|  | // Copyright 2021 yuzu Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #include <QFileDialog> | ||||||
|  | #include <QMessageBox> | ||||||
|  | #include "common/fs/fs.h" | ||||||
|  | #include "common/fs/path_util.h" | ||||||
|  | #include "common/settings.h" | ||||||
|  | #include "ui_configure_tas.h" | ||||||
|  | #include "yuzu/configuration/configure_tas.h" | ||||||
|  | #include "yuzu/uisettings.h" | ||||||
|  | 
 | ||||||
|  | ConfigureTasDialog::ConfigureTasDialog(QWidget* parent) | ||||||
|  |     : QDialog(parent), ui(std::make_unique<Ui::ConfigureTas>()) { | ||||||
|  | 
 | ||||||
|  |     ui->setupUi(this); | ||||||
|  | 
 | ||||||
|  |     setFocusPolicy(Qt::ClickFocus); | ||||||
|  |     setWindowTitle(tr("TAS Configuration")); | ||||||
|  | 
 | ||||||
|  |     connect(ui->tas_path_button, &QToolButton::pressed, this, | ||||||
|  |             [this] { SetDirectory(DirectoryTarget::TAS, ui->tas_path_edit); }); | ||||||
|  | 
 | ||||||
|  |     LoadConfiguration(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ConfigureTasDialog::~ConfigureTasDialog() = default; | ||||||
|  | 
 | ||||||
|  | void ConfigureTasDialog::LoadConfiguration() { | ||||||
|  |     ui->tas_path_edit->setText( | ||||||
|  |         QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::TASDir))); | ||||||
|  |     ui->tas_enable->setChecked(Settings::values.tas_enable.GetValue()); | ||||||
|  |     ui->tas_control_swap->setChecked(Settings::values.tas_swap_controllers.GetValue()); | ||||||
|  |     ui->tas_loop_script->setChecked(Settings::values.tas_loop.GetValue()); | ||||||
|  |     ui->tas_pause_on_load->setChecked(Settings::values.pause_tas_on_load.GetValue()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConfigureTasDialog::ApplyConfiguration() { | ||||||
|  |     Common::FS::SetYuzuPath(Common::FS::YuzuPath::TASDir, ui->tas_path_edit->text().toStdString()); | ||||||
|  |     Settings::values.tas_enable.SetValue(ui->tas_enable->isChecked()); | ||||||
|  |     Settings::values.tas_swap_controllers.SetValue(ui->tas_control_swap->isChecked()); | ||||||
|  |     Settings::values.tas_loop.SetValue(ui->tas_loop_script->isChecked()); | ||||||
|  |     Settings::values.pause_tas_on_load.SetValue(ui->tas_pause_on_load->isChecked()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConfigureTasDialog::SetDirectory(DirectoryTarget target, QLineEdit* edit) { | ||||||
|  |     QString caption; | ||||||
|  | 
 | ||||||
|  |     switch (target) { | ||||||
|  |     case DirectoryTarget::TAS: | ||||||
|  |         caption = tr("Select TAS Load Directory..."); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     QString str = QFileDialog::getExistingDirectory(this, caption, edit->text()); | ||||||
|  | 
 | ||||||
|  |     if (str.isEmpty()) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (str.back() != QChar::fromLatin1('/')) { | ||||||
|  |         str.append(QChar::fromLatin1('/')); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     edit->setText(str); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConfigureTasDialog::changeEvent(QEvent* event) { | ||||||
|  |     if (event->type() == QEvent::LanguageChange) { | ||||||
|  |         RetranslateUI(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     QDialog::changeEvent(event); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConfigureTasDialog::RetranslateUI() { | ||||||
|  |     ui->retranslateUi(this); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConfigureTasDialog::HandleApplyButtonClicked() { | ||||||
|  |     UISettings::values.configuration_applied = true; | ||||||
|  |     ApplyConfiguration(); | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								src/yuzu/configuration/configure_tas.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/yuzu/configuration/configure_tas.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,38 @@ | ||||||
|  | // Copyright 2021 yuzu Emulator Project
 | ||||||
|  | // Licensed under GPLv2 or any later version
 | ||||||
|  | // Refer to the license.txt file included.
 | ||||||
|  | 
 | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <QDialog> | ||||||
|  | 
 | ||||||
|  | namespace Ui { | ||||||
|  | class ConfigureTas; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class ConfigureTasDialog : public QDialog { | ||||||
|  |     Q_OBJECT | ||||||
|  | 
 | ||||||
|  | public: | ||||||
|  |     explicit ConfigureTasDialog(QWidget* parent); | ||||||
|  |     ~ConfigureTasDialog() override; | ||||||
|  | 
 | ||||||
|  |     /// Save all button configurations to settings file
 | ||||||
|  |     void ApplyConfiguration(); | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |     enum class DirectoryTarget { | ||||||
|  |         TAS, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     void LoadConfiguration(); | ||||||
|  | 
 | ||||||
|  |     void SetDirectory(DirectoryTarget target, QLineEdit* edit); | ||||||
|  | 
 | ||||||
|  |     void changeEvent(QEvent* event) override; | ||||||
|  |     void RetranslateUI(); | ||||||
|  | 
 | ||||||
|  |     void HandleApplyButtonClicked(); | ||||||
|  | 
 | ||||||
|  |     std::unique_ptr<Ui::ConfigureTas> ui; | ||||||
|  | }; | ||||||
							
								
								
									
										183
									
								
								src/yuzu/configuration/configure_tas.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/yuzu/configuration/configure_tas.ui
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,183 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <ui version="4.0"> | ||||||
|  |   <class>ConfigureTas</class> | ||||||
|  |   <widget class="QDialog" name="ConfigureTas"> | ||||||
|  |     <property name="geometry"> | ||||||
|  |       <rect> | ||||||
|  |         <x>0</x> | ||||||
|  |         <y>0</y> | ||||||
|  |         <width>800</width> | ||||||
|  |         <height>300</height> | ||||||
|  |       </rect> | ||||||
|  |     </property> | ||||||
|  |     <property name="windowTitle"> | ||||||
|  |       <string>Dialog</string> | ||||||
|  |     </property> | ||||||
|  |     <layout class="QVBoxLayout" name="verticalLayout_1"> | ||||||
|  |       <item> | ||||||
|  |         <layout class="QHBoxLayout" name="horizontalLayout"> | ||||||
|  |           <item> | ||||||
|  |             <widget class="QGroupBox" name="groupBox"> | ||||||
|  |               <property name="title"> | ||||||
|  |                 <string>TAS</string> | ||||||
|  |               </property> | ||||||
|  |               <layout class="QGridLayout" name="gridLayout"> | ||||||
|  |                 <item row="0" column="0" colspan="1"> | ||||||
|  |                   <widget class="QLabel" name="label_1"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>Reads controller input from scripts in the same format as TAS-nx scripts. For a more detailed explanation please consult the FAQ on the yuzu website.</string> | ||||||
|  |                     </property> | ||||||
|  |                     <property name="wordWrap"> | ||||||
|  |                       <bool>true</bool> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="1" column="0" colspan="1"> | ||||||
|  |                   <widget class="QLabel" name="label_2"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>To check which hotkeys control the playback/recording, please refer to the Hotkey settings (General -> Hotkeys).</string> | ||||||
|  |                     </property> | ||||||
|  |                     <property name="wordWrap"> | ||||||
|  |                       <bool>true</bool> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="2" column="0" colspan="1"> | ||||||
|  |                   <widget class="QLabel" name="label_2"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>WARNING: This is an experimental feature. It will not play back scripts frame perfectly with the current, imperfect syncing method.</string> | ||||||
|  |                     </property> | ||||||
|  |                     <property name="wordWrap"> | ||||||
|  |                       <bool>true</bool> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |               </layout> | ||||||
|  |             </widget> | ||||||
|  |           </item> | ||||||
|  |         </layout> | ||||||
|  |       </item> | ||||||
|  |       <item> | ||||||
|  |         <layout class="QHBoxLayout" name="horizontalLayout"> | ||||||
|  |           <item> | ||||||
|  |             <widget class="QGroupBox" name="groupBox"> | ||||||
|  |               <property name="title"> | ||||||
|  |                 <string>Settings</string> | ||||||
|  |               </property> | ||||||
|  |               <layout class="QGridLayout" name="gridLayout"> | ||||||
|  |                 <item row="0" column="0" colspan="4"> | ||||||
|  |                   <widget class="QCheckBox" name="tas_enable"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>Enable TAS features</string> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="1" column="0" colspan="4"> | ||||||
|  |                   <widget class="QCheckBox" name="tas_control_swap"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>Automatic controller profile swapping</string> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="2" column="0" colspan="4"> | ||||||
|  |                   <widget class="QCheckBox" name="tas_loop_script"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>Loop script</string> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="3" column="0" colspan="4"> | ||||||
|  |                   <widget class="QCheckBox" name="tas_pause_on_load"> | ||||||
|  |                     <property name="enabled"> | ||||||
|  |                       <bool>false</bool> | ||||||
|  |                     </property> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>Pause execution during loads</string> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |               </layout> | ||||||
|  |             </widget> | ||||||
|  |           </item> | ||||||
|  |         </layout> | ||||||
|  |       </item> | ||||||
|  |       <item> | ||||||
|  |         <layout class="QHBoxLayout" name="horizontalLayout"> | ||||||
|  |           <item> | ||||||
|  |             <widget class="QGroupBox" name="groupBox"> | ||||||
|  |               <property name="title"> | ||||||
|  |                 <string>Script Directory</string> | ||||||
|  |               </property> | ||||||
|  |               <layout class="QGridLayout" name="gridLayout"> | ||||||
|  |                 <item row="0" column="0"> | ||||||
|  |                   <widget class="QLabel" name="label"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>Path</string> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="0" column="3"> | ||||||
|  |                   <widget class="QToolButton" name="tas_path_button"> | ||||||
|  |                     <property name="text"> | ||||||
|  |                       <string>...</string> | ||||||
|  |                     </property> | ||||||
|  |                   </widget> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="0" column="2"> | ||||||
|  |                   <widget class="QLineEdit" name="tas_path_edit"/> | ||||||
|  |                 </item> | ||||||
|  |                 <item row="0" column="1"> | ||||||
|  |                   <spacer name="horizontalSpacer"> | ||||||
|  |                     <property name="orientation"> | ||||||
|  |                       <enum>Qt::Horizontal</enum> | ||||||
|  |                     </property> | ||||||
|  |                     <property name="sizeType"> | ||||||
|  |                       <enum>QSizePolicy::Maximum</enum> | ||||||
|  |                     </property> | ||||||
|  |                     <property name="sizeHint" stdset="0"> | ||||||
|  |                       <size> | ||||||
|  |                         <width>60</width> | ||||||
|  |                         <height>20</height> | ||||||
|  |                       </size> | ||||||
|  |                     </property> | ||||||
|  |                   </spacer> | ||||||
|  |                 </item> | ||||||
|  |               </layout> | ||||||
|  |             </widget> | ||||||
|  |           </item> | ||||||
|  |         </layout> | ||||||
|  |       </item> | ||||||
|  |       <item> | ||||||
|  |         <widget class="QDialogButtonBox" name="buttonBox"> | ||||||
|  |           <property name="sizePolicy"> | ||||||
|  |             <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> | ||||||
|  |               <horstretch>0</horstretch> | ||||||
|  |               <verstretch>0</verstretch> | ||||||
|  |             </sizepolicy> | ||||||
|  |           </property> | ||||||
|  |           <property name="orientation"> | ||||||
|  |             <enum>Qt::Horizontal</enum> | ||||||
|  |           </property> | ||||||
|  |           <property name="standardButtons"> | ||||||
|  |             <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> | ||||||
|  |           </property> | ||||||
|  |         </widget> | ||||||
|  |       </item> | ||||||
|  |     </layout> | ||||||
|  |   </widget> | ||||||
|  |   <resources/> | ||||||
|  |   <connections> | ||||||
|  |     <connection> | ||||||
|  |       <sender>buttonBox</sender> | ||||||
|  |       <signal>accepted()</signal> | ||||||
|  |       <receiver>ConfigureTas</receiver> | ||||||
|  |       <slot>accept()</slot> | ||||||
|  |     </connection> | ||||||
|  |     <connection> | ||||||
|  |       <sender>buttonBox</sender> | ||||||
|  |       <signal>rejected()</signal> | ||||||
|  |       <receiver>ConfigureTas</receiver> | ||||||
|  |       <slot>reject()</slot> | ||||||
|  |     </connection> | ||||||
|  |   </connections> | ||||||
|  | </ui> | ||||||
|  | @ -99,7 +99,7 @@ void ConfigureVibration::SetVibrationDevices(std::size_t player_index) { | ||||||
|         const auto guid = param.Get("guid", ""); |         const auto guid = param.Get("guid", ""); | ||||||
|         const auto port = param.Get("port", ""); |         const auto port = param.Get("port", ""); | ||||||
| 
 | 
 | ||||||
|         if (engine.empty() || engine == "keyboard" || engine == "mouse") { |         if (engine.empty() || engine == "keyboard" || engine == "mouse" || engine == "tas") { | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,10 +6,13 @@ | ||||||
| #include <QLayout> | #include <QLayout> | ||||||
| #include <QString> | #include <QString> | ||||||
| #include "common/settings.h" | #include "common/settings.h" | ||||||
|  | #include "input_common/main.h" | ||||||
|  | #include "input_common/tas/tas_input.h" | ||||||
| #include "yuzu/configuration/configure_input_player_widget.h" | #include "yuzu/configuration/configure_input_player_widget.h" | ||||||
| #include "yuzu/debugger/controller.h" | #include "yuzu/debugger/controller.h" | ||||||
| 
 | 
 | ||||||
| ControllerDialog::ControllerDialog(QWidget* parent) : QWidget(parent, Qt::Dialog) { | ControllerDialog::ControllerDialog(QWidget* parent, InputCommon::InputSubsystem* input_subsystem_) | ||||||
|  |     : QWidget(parent, Qt::Dialog), input_subsystem{input_subsystem_} { | ||||||
|     setObjectName(QStringLiteral("Controller")); |     setObjectName(QStringLiteral("Controller")); | ||||||
|     setWindowTitle(tr("Controller P1")); |     setWindowTitle(tr("Controller P1")); | ||||||
|     resize(500, 350); |     resize(500, 350); | ||||||
|  | @ -38,6 +41,9 @@ void ControllerDialog::refreshConfiguration() { | ||||||
|     constexpr std::size_t player = 0; |     constexpr std::size_t player = 0; | ||||||
|     widget->SetPlayerInputRaw(player, players[player].buttons, players[player].analogs); |     widget->SetPlayerInputRaw(player, players[player].buttons, players[player].analogs); | ||||||
|     widget->SetControllerType(players[player].controller_type); |     widget->SetControllerType(players[player].controller_type); | ||||||
|  |     ControllerCallback callback{[this](ControllerInput input) { InputController(input); }}; | ||||||
|  |     widget->SetCallBack(callback); | ||||||
|  |     widget->repaint(); | ||||||
|     widget->SetConnectedStatus(players[player].connected); |     widget->SetConnectedStatus(players[player].connected); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -67,3 +73,13 @@ void ControllerDialog::hideEvent(QHideEvent* ev) { | ||||||
|     widget->SetConnectedStatus(false); |     widget->SetConnectedStatus(false); | ||||||
|     QWidget::hideEvent(ev); |     QWidget::hideEvent(ev); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | void ControllerDialog::InputController(ControllerInput input) { | ||||||
|  |     u32 buttons = 0; | ||||||
|  |     int index = 0; | ||||||
|  |     for (bool btn : input.button_values) { | ||||||
|  |         buttons |= (btn ? 1U : 0U) << index; | ||||||
|  |         index++; | ||||||
|  |     } | ||||||
|  |     input_subsystem->GetTas()->RecordInput(buttons, input.axis_values); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -4,18 +4,35 @@ | ||||||
| 
 | 
 | ||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
|  | #include <QFileSystemWatcher> | ||||||
| #include <QWidget> | #include <QWidget> | ||||||
|  | #include "common/settings.h" | ||||||
| 
 | 
 | ||||||
| class QAction; | class QAction; | ||||||
| class QHideEvent; | class QHideEvent; | ||||||
| class QShowEvent; | class QShowEvent; | ||||||
| class PlayerControlPreview; | class PlayerControlPreview; | ||||||
| 
 | 
 | ||||||
|  | namespace InputCommon { | ||||||
|  | class InputSubsystem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | struct ControllerInput { | ||||||
|  |     std::array<std::pair<float, float>, Settings::NativeAnalog::NUM_STICKS_HID> axis_values{}; | ||||||
|  |     std::array<bool, Settings::NativeButton::NumButtons> button_values{}; | ||||||
|  |     bool changed{}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct ControllerCallback { | ||||||
|  |     std::function<void(ControllerInput)> input; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| class ControllerDialog : public QWidget { | class ControllerDialog : public QWidget { | ||||||
|     Q_OBJECT |     Q_OBJECT | ||||||
| 
 | 
 | ||||||
| public: | public: | ||||||
|     explicit ControllerDialog(QWidget* parent = nullptr); |     explicit ControllerDialog(QWidget* parent = nullptr, | ||||||
|  |                               InputCommon::InputSubsystem* input_subsystem_ = nullptr); | ||||||
| 
 | 
 | ||||||
|     /// Returns a QAction that can be used to toggle visibility of this dialog.
 |     /// Returns a QAction that can be used to toggle visibility of this dialog.
 | ||||||
|     QAction* toggleViewAction(); |     QAction* toggleViewAction(); | ||||||
|  | @ -26,6 +43,9 @@ protected: | ||||||
|     void hideEvent(QHideEvent* ev) override; |     void hideEvent(QHideEvent* ev) override; | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|  |     void InputController(ControllerInput input); | ||||||
|     QAction* toggle_view_action = nullptr; |     QAction* toggle_view_action = nullptr; | ||||||
|  |     QFileSystemWatcher* watcher = nullptr; | ||||||
|     PlayerControlPreview* widget; |     PlayerControlPreview* widget; | ||||||
|  |     InputCommon::InputSubsystem* input_subsystem; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ | ||||||
| #include "common/nvidia_flags.h" | #include "common/nvidia_flags.h" | ||||||
| #include "configuration/configure_input.h" | #include "configuration/configure_input.h" | ||||||
| #include "configuration/configure_per_game.h" | #include "configuration/configure_per_game.h" | ||||||
|  | #include "configuration/configure_tas.h" | ||||||
| #include "configuration/configure_vibration.h" | #include "configuration/configure_vibration.h" | ||||||
| #include "core/file_sys/vfs.h" | #include "core/file_sys/vfs.h" | ||||||
| #include "core/file_sys/vfs_real.h" | #include "core/file_sys/vfs_real.h" | ||||||
|  | @ -102,6 +103,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | ||||||
| #include "core/perf_stats.h" | #include "core/perf_stats.h" | ||||||
| #include "core/telemetry_session.h" | #include "core/telemetry_session.h" | ||||||
| #include "input_common/main.h" | #include "input_common/main.h" | ||||||
|  | #include "input_common/tas/tas_input.h" | ||||||
| #include "util/overlay_dialog.h" | #include "util/overlay_dialog.h" | ||||||
| #include "video_core/gpu.h" | #include "video_core/gpu.h" | ||||||
| #include "video_core/renderer_base.h" | #include "video_core/renderer_base.h" | ||||||
|  | @ -747,6 +749,11 @@ void GMainWindow::InitializeWidgets() { | ||||||
|         statusBar()->addPermanentWidget(label); |         statusBar()->addPermanentWidget(label); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     tas_label = new QLabel(); | ||||||
|  |     tas_label->setObjectName(QStringLiteral("TASlabel")); | ||||||
|  |     tas_label->setFocusPolicy(Qt::NoFocus); | ||||||
|  |     statusBar()->insertPermanentWidget(0, tas_label); | ||||||
|  | 
 | ||||||
|     // Setup Dock button
 |     // Setup Dock button
 | ||||||
|     dock_status_button = new QPushButton(); |     dock_status_button = new QPushButton(); | ||||||
|     dock_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); |     dock_status_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); | ||||||
|  | @ -841,7 +848,7 @@ void GMainWindow::InitializeDebugWidgets() { | ||||||
|     waitTreeWidget->hide(); |     waitTreeWidget->hide(); | ||||||
|     debug_menu->addAction(waitTreeWidget->toggleViewAction()); |     debug_menu->addAction(waitTreeWidget->toggleViewAction()); | ||||||
| 
 | 
 | ||||||
|     controller_dialog = new ControllerDialog(this); |     controller_dialog = new ControllerDialog(this, input_subsystem.get()); | ||||||
|     controller_dialog->hide(); |     controller_dialog->hide(); | ||||||
|     debug_menu->addAction(controller_dialog->toggleViewAction()); |     debug_menu->addAction(controller_dialog->toggleViewAction()); | ||||||
| 
 | 
 | ||||||
|  | @ -1014,6 +1021,28 @@ void GMainWindow::InitializeHotkeys() { | ||||||
|                     render_window->setAttribute(Qt::WA_Hover, true); |                     render_window->setAttribute(Qt::WA_Hover, true); | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|  |     connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Start/Stop"), this), | ||||||
|  |             &QShortcut::activated, this, [&] { | ||||||
|  |                 if (!emulation_running) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 input_subsystem->GetTas()->StartStop(); | ||||||
|  |             }); | ||||||
|  |     connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Reset"), this), | ||||||
|  |             &QShortcut::activated, this, [&] { input_subsystem->GetTas()->Reset(); }); | ||||||
|  |     connect(hotkey_registry.GetHotkey(main_window, QStringLiteral("TAS Record"), this), | ||||||
|  |             &QShortcut::activated, this, [&] { | ||||||
|  |                 if (!emulation_running) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 bool is_recording = input_subsystem->GetTas()->Record(); | ||||||
|  |                 if (!is_recording) { | ||||||
|  |                     const auto res = QMessageBox::question(this, tr("TAS Recording"), | ||||||
|  |                                                            tr("Overwrite file of player 1?"), | ||||||
|  |                                                            QMessageBox::Yes | QMessageBox::No); | ||||||
|  |                     input_subsystem->GetTas()->SaveRecording(res == QMessageBox::Yes); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::SetDefaultUIGeometry() { | void GMainWindow::SetDefaultUIGeometry() { | ||||||
|  | @ -1132,6 +1161,7 @@ void GMainWindow::ConnectMenuEvents() { | ||||||
|     connect(ui.action_Open_FAQ, &QAction::triggered, this, &GMainWindow::OnOpenFAQ); |     connect(ui.action_Open_FAQ, &QAction::triggered, this, &GMainWindow::OnOpenFAQ); | ||||||
|     connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); }); |     connect(ui.action_Restart, &QAction::triggered, this, [this] { BootGame(QString(game_path)); }); | ||||||
|     connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure); |     connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure); | ||||||
|  |     connect(ui.action_Configure_Tas, &QAction::triggered, this, &GMainWindow::OnConfigureTas); | ||||||
|     connect(ui.action_Configure_Current_Game, &QAction::triggered, this, |     connect(ui.action_Configure_Current_Game, &QAction::triggered, this, | ||||||
|             &GMainWindow::OnConfigurePerGame); |             &GMainWindow::OnConfigurePerGame); | ||||||
| 
 | 
 | ||||||
|  | @ -1464,6 +1494,8 @@ void GMainWindow::ShutdownGame() { | ||||||
|         game_list->show(); |         game_list->show(); | ||||||
|     } |     } | ||||||
|     game_list->SetFilterFocus(); |     game_list->SetFilterFocus(); | ||||||
|  |     tas_label->clear(); | ||||||
|  |     input_subsystem->GetTas()->Stop(); | ||||||
| 
 | 
 | ||||||
|     render_window->removeEventFilter(render_window); |     render_window->removeEventFilter(render_window); | ||||||
|     render_window->setAttribute(Qt::WA_Hover, false); |     render_window->setAttribute(Qt::WA_Hover, false); | ||||||
|  | @ -2698,6 +2730,19 @@ void GMainWindow::OnConfigure() { | ||||||
|     UpdateStatusButtons(); |     UpdateStatusButtons(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void GMainWindow::OnConfigureTas() { | ||||||
|  |     const auto& system = Core::System::GetInstance(); | ||||||
|  |     ConfigureTasDialog dialog(this); | ||||||
|  |     const auto result = dialog.exec(); | ||||||
|  | 
 | ||||||
|  |     if (result != QDialog::Accepted && !UISettings::values.configuration_applied) { | ||||||
|  |         Settings::RestoreGlobalState(system.IsPoweredOn()); | ||||||
|  |         return; | ||||||
|  |     } else if (result == QDialog::Accepted) { | ||||||
|  |         dialog.ApplyConfiguration(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void GMainWindow::OnConfigurePerGame() { | void GMainWindow::OnConfigurePerGame() { | ||||||
|     const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID(); |     const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID(); | ||||||
|     OpenPerGameConfiguration(title_id, game_path.toStdString()); |     OpenPerGameConfiguration(title_id, game_path.toStdString()); | ||||||
|  | @ -2874,12 +2919,32 @@ void GMainWindow::UpdateWindowTitle(std::string_view title_name, std::string_vie | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | QString GMainWindow::GetTasStateDescription() const { | ||||||
|  |     auto [tas_status, current_tas_frame, total_tas_frames] = input_subsystem->GetTas()->GetStatus(); | ||||||
|  |     switch (tas_status) { | ||||||
|  |     case TasInput::TasState::Running: | ||||||
|  |         return tr("TAS state: Running %1/%2").arg(current_tas_frame).arg(total_tas_frames); | ||||||
|  |     case TasInput::TasState::Recording: | ||||||
|  |         return tr("TAS state: Recording %1").arg(total_tas_frames); | ||||||
|  |     case TasInput::TasState::Stopped: | ||||||
|  |         return tr("TAS state: Idle %1/%2").arg(current_tas_frame).arg(total_tas_frames); | ||||||
|  |     default: | ||||||
|  |         return tr("TAS State: Invalid"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void GMainWindow::UpdateStatusBar() { | void GMainWindow::UpdateStatusBar() { | ||||||
|     if (emu_thread == nullptr) { |     if (emu_thread == nullptr) { | ||||||
|         status_bar_update_timer.stop(); |         status_bar_update_timer.stop(); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (Settings::values.tas_enable) { | ||||||
|  |         tas_label->setText(GetTasStateDescription()); | ||||||
|  |     } else { | ||||||
|  |         tas_label->clear(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     auto& system = Core::System::GetInstance(); |     auto& system = Core::System::GetInstance(); | ||||||
|     auto results = system.GetAndResetPerfStats(); |     auto results = system.GetAndResetPerfStats(); | ||||||
|     auto& shader_notify = system.GPU().ShaderNotify(); |     auto& shader_notify = system.GPU().ShaderNotify(); | ||||||
|  |  | ||||||
|  | @ -259,6 +259,7 @@ private slots: | ||||||
|     void OnMenuInstallToNAND(); |     void OnMenuInstallToNAND(); | ||||||
|     void OnMenuRecentFile(); |     void OnMenuRecentFile(); | ||||||
|     void OnConfigure(); |     void OnConfigure(); | ||||||
|  |     void OnConfigureTas(); | ||||||
|     void OnConfigurePerGame(); |     void OnConfigurePerGame(); | ||||||
|     void OnLoadAmiibo(); |     void OnLoadAmiibo(); | ||||||
|     void OnOpenYuzuFolder(); |     void OnOpenYuzuFolder(); | ||||||
|  | @ -300,6 +301,7 @@ private: | ||||||
|     void OpenURL(const QUrl& url); |     void OpenURL(const QUrl& url); | ||||||
|     void LoadTranslation(); |     void LoadTranslation(); | ||||||
|     void OpenPerGameConfiguration(u64 title_id, const std::string& file_name); |     void OpenPerGameConfiguration(u64 title_id, const std::string& file_name); | ||||||
|  |     QString GetTasStateDescription() const; | ||||||
| 
 | 
 | ||||||
|     Ui::MainWindow ui; |     Ui::MainWindow ui; | ||||||
| 
 | 
 | ||||||
|  | @ -318,6 +320,7 @@ private: | ||||||
|     QLabel* emu_speed_label = nullptr; |     QLabel* emu_speed_label = nullptr; | ||||||
|     QLabel* game_fps_label = nullptr; |     QLabel* game_fps_label = nullptr; | ||||||
|     QLabel* emu_frametime_label = nullptr; |     QLabel* emu_frametime_label = nullptr; | ||||||
|  |     QLabel* tas_label = nullptr; | ||||||
|     QPushButton* gpu_accuracy_button = nullptr; |     QPushButton* gpu_accuracy_button = nullptr; | ||||||
|     QPushButton* renderer_status_button = nullptr; |     QPushButton* renderer_status_button = nullptr; | ||||||
|     QPushButton* dock_status_button = nullptr; |     QPushButton* dock_status_button = nullptr; | ||||||
|  |  | ||||||
|  | @ -100,6 +100,7 @@ | ||||||
|     <addaction name="action_Rederive"/> |     <addaction name="action_Rederive"/> | ||||||
|     <addaction name="separator"/> |     <addaction name="separator"/> | ||||||
|     <addaction name="action_Capture_Screenshot"/> |     <addaction name="action_Capture_Screenshot"/> | ||||||
|  |     <addaction name="action_Configure_Tas"/> | ||||||
|    </widget> |    </widget> | ||||||
|    <widget class="QMenu" name="menu_Help"> |    <widget class="QMenu" name="menu_Help"> | ||||||
|     <property name="title"> |     <property name="title"> | ||||||
|  | @ -294,6 +295,11 @@ | ||||||
|     <string>&Capture Screenshot</string> |     <string>&Capture Screenshot</string> | ||||||
|    </property> |    </property> | ||||||
|   </action> |   </action> | ||||||
|  |   <action name="action_Configure_Tas"> | ||||||
|  |    <property name="text"> | ||||||
|  |     <string>Configure &TAS...</string> | ||||||
|  |    </property> | ||||||
|  |   </action> | ||||||
|   <action name="action_Configure_Current_Game"> |   <action name="action_Configure_Current_Game"> | ||||||
|    <property name="enabled"> |    <property name="enabled"> | ||||||
|     <bool>false</bool> |     <bool>false</bool> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 bunnei
						bunnei