diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 69247b166..4af157cde 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -152,6 +152,9 @@ void Config::ReadValues() { Settings::values.gdbstub_port = static_cast(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689)); + // Scripted Input + Settings::values.script_name = sdl2_config->Get("ScriptedInput", "script_name", ""); + // Web Service Settings::values.telemetry_endpoint_url = sdl2_config->Get( "WebService", "telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry"); diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h index a12498e0f..e9a75d86f 100644 --- a/src/citra/default_ini.h +++ b/src/citra/default_ini.h @@ -169,6 +169,10 @@ log_filter = *:Info use_gdbstub=false gdbstub_port=24689 +[ScriptedInput] +# Script file to load for simulated input +script_name= + [WebService] # Endpoint URL for submitting telemetry data telemetry_endpoint_url = diff --git a/src/core/settings.cpp b/src/core/settings.cpp index d4f0429d1..558969851 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -7,6 +7,7 @@ #include "core/hle/service/hid/hid.h" #include "core/hle/service/ir/ir.h" #include "core/settings.h" +#include "scripted_input/scripted_input.h" #include "video_core/video_core.h" #include "core/frontend/emu_window.h" @@ -34,6 +35,8 @@ void Apply() { Service::HID::ReloadInputDevices(); Service::IR::ReloadInputDevices(); + + ScriptedInput::LoadScript(values.script_name); } } // namespace diff --git a/src/core/settings.h b/src/core/settings.h index ee16bb90a..678ac0792 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -127,6 +127,9 @@ struct Values { bool use_gdbstub; u16 gdbstub_port; + // ScriptedInput + std::string script_name; + // WebService std::string telemetry_endpoint_url; } extern values; diff --git a/src/scripted_input/CMakeLists.txt b/src/scripted_input/CMakeLists.txt index ced237e24..b5526a605 100644 --- a/src/scripted_input/CMakeLists.txt +++ b/src/scripted_input/CMakeLists.txt @@ -1,14 +1,17 @@ set(SRCS scripted_input.cpp scripted_buttons.cpp + script_runner.cpp ) set(HEADERS scripted_input.h scripted_buttons.h + script_runner.h ) create_directory_groups(${SRCS} ${HEADERS}) add_library(scripted_input STATIC ${SRCS} ${HEADERS}) target_link_libraries(scripted_input PUBLIC common core) +target_link_libraries(scripted_input PRIVATE glad) \ No newline at end of file diff --git a/src/scripted_input/script_runner.cpp b/src/scripted_input/script_runner.cpp new file mode 100644 index 000000000..94782afeb --- /dev/null +++ b/src/scripted_input/script_runner.cpp @@ -0,0 +1,133 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "scripted_input/script_runner.h" +#include "scripted_input/scripted_buttons.h" + +#define SCRIPT_MAX_LINE 200 + +namespace ScriptedInput { + +void ScriptRunner::SetButtons(std::shared_ptr buttons) { + scripted_buttons = buttons; +} + +void ScriptRunner::LoadScript(std::string script_name) { + //TODO: Load from a file specified in config + FILE* file = fopen(script_name.c_str(), "r"); + if (!file) { + //TODO: Log error + return; + } + + char line[SCRIPT_MAX_LINE]; + + int lineno = 0; + while (fgets(line, SCRIPT_MAX_LINE, file) != NULL) { + lineno++; + + //Remove end of line bytes and comments + char terminal_chars[] = "\r\n#"; + for (int i = 0; i < strlen(terminal_chars); i++) { + char* index = strchr(line, terminal_chars[i]); + if (index != NULL) { + *index = '\0'; + } + } + + ScriptItem item; + + char* pch = strtok(line, " "); + while (pch != NULL) { + if (item.type == ScriptItemType::Undefined) { + //The first token is a number or "screenshot" + if (strcmp("screenshot", pch) == 0) { + item.type = ScriptItemType::Screenshot; + } + else { + int res = sscanf(pch, "%d", &item.frames); + if (res == 1 && item.frames > 0) { + item.type = ScriptItemType::Run; + } + else { + //TODO: Parse error + } + } + } + else { + //The following should be buttons + int index = IndexOfButton(pch); + if (index != -1) { + item.buttons_active.push_back(index); + } + else { + //TODO: Parse error + } + } + pch = strtok(NULL, " ,.-"); + } + + if (item.type != ScriptItemType::Undefined) { + script.push_back(item); + } + } + + fclose(file); +} + +void ScriptRunner::NotifyFrameFinished() { + frame_number++; + + //Script already finished + if (script_index >= script.size()) { + //printf("already done\n"); + return; + } + + //If we're done with the current item, move to the next + script_frame++; + if (script_frame == script[script_index].frames) { + printf("At frame %i, progressing to index %i\n", frame_number, script_index + 1); + script_frame = 0; + script_index++; + + //take screenshots if we hit a screenshot item + while (script_index < script.size() && script[script_index].type == ScriptItemType::Screenshot) { + //TODO: Screenshot + SaveScreenshot(); + script_index++; + } + + //Now we'll be at the next run item, so set the buttons up + if (script_index < script.size()) { + printf("changing button\n"); + scripted_buttons.get()->SetActiveButtons(script[script_index].buttons_active); + } + } +} + +void ScriptRunner::SaveScreenshot() { + const int w = 400;//render_window->GetActiveConfig().min_client_area_size.first; + const int h = 480;//render_window->GetActiveConfig().min_client_area_size.second; + unsigned char* pixels = new unsigned char[w * h * 3]; + glReadPixels(0, 0, w, h, GL_BGR, GL_UNSIGNED_BYTE, pixels); + + //BMP + //ref https://web.archive.org/web/20080912171714/http://www.fortunecity.com/skyscraper/windows/364/bmpffrmt.html + int image_bytes = (w * h * 3); + int size = 14 + 40 + image_bytes; + unsigned char file_header[14] = { 'B', 'M', size, size >> 8, size >> 16, size >> 24, 0, 0, 0, 0, 14 + 40, 0, 0, 0 }; + unsigned char info_header[40] = { 40, 0, 0, 0, w, w >> 8, w >> 16, w >> 24, h, h >> 8, h >> 16, h >> 24, 1, 0, 24, 0, 0, 0, 0, 0, image_bytes, image_bytes >> 8, image_bytes >> 16, image_bytes >> 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + FILE *fp = fopen("out.bmp", "wb"); + fwrite(file_header, sizeof(file_header), 1, fp); + fwrite(info_header, sizeof(info_header), 1, fp); + fwrite(pixels, w * h * 3, 1, fp); + fclose(fp); + + + delete[] pixels; +} +} \ No newline at end of file diff --git a/src/scripted_input/script_runner.h b/src/scripted_input/script_runner.h new file mode 100644 index 000000000..cbdf2fa5c --- /dev/null +++ b/src/scripted_input/script_runner.h @@ -0,0 +1,47 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/input.h" +#include "scripted_input/scripted_buttons.h" + +namespace ScriptedInput { + +enum class ScriptItemType { + Undefined, + Run, + Screenshot +}; + +class ScriptItem { +public: + static ScriptItem Screenshot; + + ScriptItemType type{ ScriptItemType::Undefined }; + int frames {0}; + std::vector buttons_active; +}; + +/** +* A button device factory that returns inputs from a script file +*/ +class ScriptRunner final { +public: + void SetButtons(std::shared_ptr buttons); + void LoadScript(std::string script_name); + + void NotifyFrameFinished(); +private: + std::vector script; + int frame_number {0}; + int script_index {0}; + int script_frame {0}; + + std::shared_ptr scripted_buttons; + + void SaveScreenshot(); +}; + +} // namespace InputCommon diff --git a/src/scripted_input/scripted_buttons.cpp b/src/scripted_input/scripted_buttons.cpp index 4c7dfef2a..1934a84e6 100644 --- a/src/scripted_input/scripted_buttons.cpp +++ b/src/scripted_input/scripted_buttons.cpp @@ -2,8 +2,11 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include "scripted_input/scripted_buttons.h" +const int button_count = 15; + std::string button_name_to_index[] = { "a", "b", @@ -22,8 +25,10 @@ std::string button_name_to_index[] = { "home" }; +namespace ScriptedInput { + int IndexOfButton(const std::string& button) { - for (int i = 0; i < 15; i++) { + for (int i = 0; i < button_count; i++) { if (button_name_to_index[i] == button) { return i; } @@ -33,10 +38,6 @@ int IndexOfButton(const std::string& button) { return -1; //home } -int frame = 0; - -namespace ScriptedInput { - class ScriptedButton final : public Input::ButtonDevice { bool GetStatus() const override { return status.load(); @@ -50,7 +51,7 @@ private: class ScriptedButtonList { //TODO: Do i need to memset(0) buttons? public: - ScriptedButton* buttons[15]; + ScriptedButton* buttons[button_count]; }; @@ -73,14 +74,23 @@ bool ScriptedButtons::IsInUse() { return is_in_use; } -void ScriptedButtons::NotifyFrameFinished() { - frame++; - if (frame >= 10) { - printf("TOGGLING\r\n"); - frame = 0; +void ScriptedButtons::SetActiveButtons(const std::vector& buttons_active) { - auto button = scripted_button_list.get()->buttons[0]; - button->status.store(!button->GetStatus()); + for (int i = 0; i < button_count; i++) { + auto button = scripted_button_list.get()->buttons[i]; + if (button) { + button->status.store(false); + } + } + + for (int i = 0; i < buttons_active.size(); i++) { + auto button = scripted_button_list.get()->buttons[buttons_active[i]]; + if (button) { + button->status.store(true); + } + else { + //TODO: Warning that button isn't mapped to scripting + } } } } \ No newline at end of file diff --git a/src/scripted_input/scripted_buttons.h b/src/scripted_input/scripted_buttons.h index e3cdc02ed..5201b1434 100644 --- a/src/scripted_input/scripted_buttons.h +++ b/src/scripted_input/scripted_buttons.h @@ -4,12 +4,13 @@ #pragma once -#include #include #include "core/frontend/input.h" namespace ScriptedInput { +int IndexOfButton(const std::string& button); + class ScriptedButtonList; /** @@ -30,7 +31,7 @@ public: */ bool IsInUse(); - void NotifyFrameFinished(); + void SetActiveButtons(const std::vector& buttons_active); private: std::shared_ptr scripted_button_list; bool is_in_use {false}; diff --git a/src/scripted_input/scripted_input.cpp b/src/scripted_input/scripted_input.cpp index 23417f974..4eef1542a 100644 --- a/src/scripted_input/scripted_input.cpp +++ b/src/scripted_input/scripted_input.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include "scripted_input/script_runner.h" #include "scripted_input/scripted_buttons.h" #include "scripted_input/scripted_input.h" @@ -11,10 +12,18 @@ namespace ScriptedInput { static std::shared_ptr scripted_buttons; //TODO: static std::shared_ptr scripted_analog; +static ScriptRunner script_runner; void Init() { scripted_buttons = std::make_shared(); Input::RegisterFactory("scripted", scripted_buttons); + script_runner.SetButtons(scripted_buttons); +} + +void LoadScript(std::string script_name) { + if (script_name.length() > 0) { + script_runner.LoadScript(script_name); + } } void Shutdown() { @@ -22,11 +31,13 @@ void Shutdown() { } bool IsInUse() { + //TODO return script_runner.HasScript(); return scripted_buttons.get()->IsInUse(); } void NotifyFrameFinished() { - scripted_buttons.get()->NotifyFrameFinished(); + script_runner.NotifyFrameFinished(); + //TODO? scripted_buttons.get()->NotifyFrameFinished(); } } // namespace ScriptedInput diff --git a/src/scripted_input/scripted_input.h b/src/scripted_input/scripted_input.h index f6a5417e5..c9e00a55d 100644 --- a/src/scripted_input/scripted_input.h +++ b/src/scripted_input/scripted_input.h @@ -11,6 +11,8 @@ namespace ScriptedInput { /// Initializes and registers all built-in input device factories. void Init(); +void LoadScript(std::string script_name); + /// Unresisters all build-in input device factories and shut them down. void Shutdown();