diff --git a/.pvs-suppress b/.pvs-suppress index 4a3c8c05..cbcbd752 100644 --- a/.pvs-suppress +++ b/.pvs-suppress @@ -1,6 +1,14 @@ { "version": 1, "warnings": [ + { + "CodeCurrent": 432032057, + "CodeNext": 36027430, + "CodePrev": 0, + "ErrorCode": "V801", + "FileName": "sdl_mapper.cpp", + "Message": "Decreased performance. It is better to redefine the first function argument as a reference. Consider replacing 'const .. sequence' with 'const .. &sequence'." + }, { "CodeCurrent": 4109900279, "CodeNext": 355817, diff --git a/README b/README index 65b25a72..cb73f2ab 100644 --- a/README +++ b/README @@ -1012,6 +1012,34 @@ KEYB [keyboardlayoutcode [codepage [codepagefile]]] keyb +AUTOTYPE [-list] [-w WAIT] [-p PACE] button_1 [button_2 [...]] + + Types the button sequence on your behalf, as if entered manually. + + It can be used to reliably skip intros, answer Q&A style questions that + some games ask on startup, or to script a simple demo. + + Typing is initially delayed by the WAIT time, which defaults to 2 seconds. + The delay between keystrokes is defined by the PACE time, which defaults + to 0.5 seconds. + + The comma character "," adds an extra delay similar to modern phone numbers. + + -list: prints all available button names. + + Examples: + autotype -w 3 -p 0.7 up enter , right enter + autotype -w 1.3 esc esc esc enter p l a y e r enter + autotype -p 1 e x i t enter + + Sample batch file for Microprose F-19 Steath Fighter: + + autotype n 1 + f19.com + + It types 'n' when asked if you have a joystick and '1' to select VGA mode. + The game then proceeds to load with these settings applied. + For more information use the /? command line switch with the programs. diff --git a/include/mapper.h b/include/mapper.h index f097a8e1..674a9363 100644 --- a/include/mapper.h +++ b/include/mapper.h @@ -19,6 +19,11 @@ #ifndef DOSBOX_MAPPER_H #define DOSBOX_MAPPER_H +#include +#include +#include "setup.h" +#include "types.h" + enum MapKeys { MK_f1,MK_f2,MK_f3,MK_f4,MK_f5,MK_f6,MK_f7,MK_f8,MK_f9,MK_f10,MK_f11,MK_f12, MK_return,MK_kpminus,MK_scrolllock,MK_printscreen,MK_pause,MK_home @@ -32,7 +37,10 @@ void MAPPER_StartUp(Section * sec); void MAPPER_Run(bool pressed); void MAPPER_DisplayUI(); void MAPPER_LosingFocus(void); - +std::vector MAPPER_GetEventNames(const std::string &prefix); +void MAPPER_AutoType(std::vector &sequence, + const uint32_t wait_ms, + const uint32_t pacing_ms); #define MMOD1 0x1 #define MMOD2 0x2 diff --git a/include/support.h b/include/support.h index 3a03bf7e..a55fe9ff 100644 --- a/include/support.h +++ b/include/support.h @@ -35,6 +35,9 @@ #define strncasecmp(a, b, n) _strnicmp(a, b, n) #endif +// Convert a string to double, returning true or false depending on susccess +bool str_to_double(const std::string& input, double &value); + // Returns the filename with the prior path stripped. // Works with both \ and / directory delimeters. std::string get_basename(const std::string& filename); diff --git a/src/dos/Makefile.am b/src/dos/Makefile.am index 15008a66..1560fa03 100644 --- a/src/dos/Makefile.am +++ b/src/dos/Makefile.am @@ -3,7 +3,7 @@ AM_CPPFLAGS = -I$(top_srcdir)/include noinst_LIBRARIES = libdos.a EXTRA_DIST = dos_codepages.h dos_keyboard_layout_data.h libdos_a_SOURCES = dos.cpp dos_devices.cpp dos_execute.cpp dos_files.cpp dos_ioctl.cpp dos_memory.cpp \ - dos_misc.cpp dos_classes.cpp dos_programs.cpp dos_tables.cpp \ + dos_misc.cpp dos_classes.cpp program_autotype.cpp dos_programs.cpp dos_tables.cpp \ drives.cpp drive_virtual.cpp drive_local.cpp drive_cache.cpp drive_fat.cpp \ drive_iso.cpp dev_con.h dos_mscdex.cpp dos_keyboard_layout.cpp \ cdrom.h cdrom.cpp cdrom_image.cpp \ diff --git a/src/dos/dos_programs.cpp b/src/dos/dos_programs.cpp index 1441cf7a..c66a7182 100644 --- a/src/dos/dos_programs.cpp +++ b/src/dos/dos_programs.cpp @@ -39,6 +39,7 @@ #include "inout.h" #include "dma.h" #include "shell.h" +#include "program_autotype.h" #if defined(WIN32) #ifndef S_ISDIR @@ -1564,7 +1565,6 @@ static void KEYB_ProgramStart(Program * * make) { *make=new KEYB; } - void DOS_SetupPrograms(void) { /*Add Messages */ @@ -1771,6 +1771,7 @@ void DOS_SetupPrograms(void) { MSG_Add("PROGRAM_KEYB_INVCPFILE","None or invalid codepage file for layout %s\n\n"); /*regular setup*/ + PROGRAMS_MakeFile("AUTOTYPE.COM", AUTOTYPE_ProgramStart); PROGRAMS_MakeFile("MOUNT.COM",MOUNT_ProgramStart); PROGRAMS_MakeFile("MEM.COM",MEM_ProgramStart); PROGRAMS_MakeFile("LOADFIX.COM",LOADFIX_ProgramStart); diff --git a/src/dos/program_autotype.cpp b/src/dos/program_autotype.cpp new file mode 100644 index 00000000..c6e1e797 --- /dev/null +++ b/src/dos/program_autotype.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "support.h" +#include "mapper.h" +#include "dosbox.h" +#include "programs.h" +#include "program_autotype.h" + +void AUTOTYPE::PrintUsage() { + WriteOut( + "\033[32;1mAUTOTYPE\033[0m [-list] [-w WAIT] [-p PACE] button_1 [button_2 [...]] \n\n" + "Where:\n" + " -list: prints all available button names.\n" + " -w WAIT: seconds before typing begins. Two second default; max of 30.\n" + " -p PACE: seconds between each keystroke. Half-second default; max of 10.\n" + "\n" + " The sequence is comprised of one or more space-separated buttons.\n" + " Autotyping begins after WAIT seconds, and each button is entered \n" + " every PACE seconds. The , character inserts an extra PACE delay.\n" + "\n" + "Some examples:\n" + " \033[32;1mAUTOTYPE\033[0m -w 1 -p 0.3 up enter , right enter\n" + " \033[32;1mAUTOTYPE\033[0m -p 0.2 1 3 , , enter\n" + " \033[32;1mAUTOTYPE\033[0m -w 1.3 esc esc esc enter p l a y e r enter\n"); +} + +// Prints the key-names for the mapper's currently-bound events. +void AUTOTYPE::PrintKeys() { + const std::vector names = MAPPER_GetEventNames("key_"); + + // Keep track of the longest key name + size_t max_length = 0; + for (const auto &name : names) + max_length = (std::max)(name.length(), max_length); + + // Sanity check to avoid dividing by 0 + if (!max_length) { + WriteOut("AUTOTYPE: The mapper has no key bindings\n"); + return; + } + + // Setup our rows and columns + const size_t console_width = 72; + const size_t columns = console_width / max_length; + const size_t rows = ceil_udivide(names.size(), columns); + + // Build the string output by rows and columns + std::stringstream ss; + auto name = names.begin(); + for (size_t row = 0; row < rows; ++row) { + for (size_t i = row; i < names.size(); i += rows) + ss << std::setw(max_length + 1) << name[i]; + ss << std::endl; + } + + WriteOut(ss.str().c_str()); +} + +/* + * Reads a floating point argument from command line, where: + * - name is a human description for the flag, ie: DELAY + * - flag is the command-line flag, ie: -d or -delay + * - default is the default value if the flag doesn't exist + * - value will be populated with the default or provided value + * + * Returns: + * true if 'value' is set to the default or read from the arg. + * false if the argument was used but could not be parsed. + */ +bool AUTOTYPE::ReadDoubleArg(const std::string &name, + const char *flag, + const double &def_value, + const double &min_value, + const double &max_value, + double &value) { + bool result = false; + std::string str_value; + // Is the user trying to set this flag? + if (cmd->FindString(flag, str_value, true)) { + double user_value; + + // Can the user's value be parsed? + if (str_to_double(str_value, user_value)) { + result = true; + // Clamp the user's value if needed + value = clamp(user_value, min_value, max_value); + + // If we had to clamp the users value, then inform them + if (std::fabs(user_value - value) > std::numeric_limits::epsilon()) + WriteOut("AUTOTYPE: bounding %s value of %.2f to %.2f\n", + name.c_str(), user_value, value); + // Otherwise inform them we couldn't parse their value + } else { + WriteOut("AUTOTYPE: %s value '%s' is not a valid floating point number\n", + name.c_str(), str_value.c_str()); + } + // Otherwise the user hasn't set this flag, so use the default + } else { + value = def_value; + result = true; + } + return result; +} + +void AUTOTYPE::Run() { + + //Hack To allow long commandlines + ChangeToLongCmd(); + + // Usage + if (!cmd->GetCount()) { + PrintUsage(); + return; + } + + // Print available keys + if (cmd->FindExist("-list", false)) { + PrintKeys(); + return; + } + + // Get the wait delay in milliseconds + double wait_s; + constexpr double def_wait_s = 2.0; + constexpr double min_wait_s = 0.0; + constexpr double max_wait_s = 30.0; + if (!ReadDoubleArg("WAIT", "-w", def_wait_s, min_wait_s,max_wait_s, wait_s)) + return; + const auto wait_ms = static_cast(wait_s * 1000); + + // Get the inter-key pacing in milliseconds + double pace_s; + constexpr double def_pace_s = 0.5; + constexpr double min_pace_s = 0.0; + constexpr double max_pace_s = 10.0; + if (!ReadDoubleArg("PACE", "-p", def_pace_s, min_pace_s, max_pace_s, pace_s)) + return; + const auto pace_ms = static_cast(pace_s * 1000); + + // Get the button sequence + std::vector sequence; + cmd->FillVector(sequence); + if (sequence.empty()) { + WriteOut("AUTOTYPE: button sequence is empty\n"); + return; + } + MAPPER_AutoType(sequence, wait_ms, pace_ms); +} + +void AUTOTYPE_ProgramStart(Program * *make) { + *make = new AUTOTYPE; +} \ No newline at end of file diff --git a/src/dos/program_autotype.h b/src/dos/program_autotype.h new file mode 100644 index 00000000..600edc87 --- /dev/null +++ b/src/dos/program_autotype.h @@ -0,0 +1,17 @@ +#include "programs.h" + +class AUTOTYPE : public Program { + public: + void Run(); + private: + void PrintUsage(); + void PrintKeys(); + bool ReadDoubleArg(const std::string &name, + const char *flag, + const double &def_value, + const double &min_value, + const double &max_value, + double &value); +}; + +void AUTOTYPE_ProgramStart(Program * *make); \ No newline at end of file diff --git a/src/gui/sdl_mapper.cpp b/src/gui/sdl_mapper.cpp index 69ce01c8..8c509b9c 100644 --- a/src/gui/sdl_mapper.cpp +++ b/src/gui/sdl_mapper.cpp @@ -21,12 +21,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include @@ -128,7 +130,7 @@ public: Bits GetValue() { return current_value; } - char * GetName() { return entry; } + const char * GetName() const { return entry; } virtual bool IsTrigger() = 0; CBindList bindlist; protected: @@ -281,7 +283,7 @@ public: Bitu mods = 0; Bitu flags = 0; CEvent *event = nullptr; - CBindList *list; + CBindList *list = nullptr; bool active = false; bool holding = false; }; @@ -403,8 +405,8 @@ private: } protected: const char *configname = "key"; - CBindList *lists; - Bitu keys; + CBindList *lists = nullptr; + Bitu keys = 0; }; static std::list keybindgroups; @@ -1200,6 +1202,94 @@ protected: uint16_t button_state = 0; }; + +void MAPPER_TriggerEvent(const CEvent *event, const bool deactivation_state) { + assert(event); + for (auto &bind : event->bindlist) { + bind->ActivateBind(32767, true, false); + bind->DeActivateBind(deactivation_state); + } +} + +class Typer { + public: + Typer() = default; + Typer(const Typer&) = delete; // prevent copy + Typer& operator=(const Typer&) = delete; // prevent assignment + ~Typer() { + Stop(); + } + void Start(std::vector *ext_events, + std::vector &ext_sequence, + const uint32_t wait_ms, + const uint32_t pace_ms) { + // Guard against empty inputs + if (!ext_events || ext_sequence.empty()) + return; + Wait(); + m_events = ext_events; + m_sequence = std::move(ext_sequence); + m_wait_ms = wait_ms; + m_pace_ms = pace_ms; + m_stop_requested = false; + m_instance = std::thread(&Typer::Callback, this); + } + void Wait() { + if (m_instance.joinable()) + m_instance.join(); + } + void Stop() { + m_stop_requested = true; + Wait(); + } + private: + void Callback() { + // quit before our initial wait time + if (m_stop_requested) + return; + std::this_thread::sleep_for(std::chrono::milliseconds(m_wait_ms)); + for (const auto &button : m_sequence) { + bool found = false; + // comma adds an extra pause, similar to the pause used in a phone number + if (button == ",") { + found = true; + // quit before the pause + if (m_stop_requested) + return; + std::this_thread::sleep_for(std::chrono::milliseconds(m_pace_ms)); + // Otherwise trigger the matching button if we have one + } else { + const std::string bind_name = "key_" + button; + for (auto &event : *m_events) { + if (bind_name == event->GetName()) { + found = true; + MAPPER_TriggerEvent(event, true); + break; + } + } + } + /* + * Terminate the sequence for safety reasons if we can't find a button. + * For example, we don't wan't DEAL becoming DEL, or 'rem' becoming 'rm' + */ + if (!found) { + LOG_MSG("MAPPER: Couldn't find a button named '%s', stopping.", + button.c_str()); + return; + } + if (m_stop_requested) // quit before the pacing delay + return; + std::this_thread::sleep_for(std::chrono::milliseconds(m_pace_ms)); + } + } + std::thread m_instance; + std::vector m_sequence; + std::vector *m_events = nullptr; + uint32_t m_wait_ms = 0; + uint32_t m_pace_ms = 0; + bool m_stop_requested = false; +}; + static struct CMapper { SDL_Window *window = nullptr; SDL_Rect draw_rect = {0, 0, 0, 0}; @@ -1218,10 +1308,12 @@ static struct CMapper { unsigned int num = 0; unsigned int num_groups = 0; } sticks; + Typer typist; std::string filename = ""; } mapper; void CBindGroup::ActivateBindList(CBindList * list,Bits value,bool ev_trigger) { + assert(list); Bitu validmod=0; CBindList_it it; for (it = list->begin(); it != list->end(); ++it) { @@ -1235,6 +1327,7 @@ void CBindGroup::ActivateBindList(CBindList * list,Bits value,bool ev_trigger) { } void CBindGroup::DeactivateBindList(CBindList * list,bool ev_trigger) { + assert(list); CBindList_it it; for (it = list->begin(); it != list->end(); ++it) { (*it)->DeActivateBind(ev_trigger); @@ -1324,7 +1417,7 @@ public: } protected: - const char *text; + const char *text = nullptr; }; class CClickableTextButton : public CTextButton { @@ -1362,7 +1455,7 @@ public: last_clicked=this; } protected: - CEvent * event; + CEvent * event = nullptr; }; class CCaptionButton : public CButton { @@ -1377,7 +1470,7 @@ public: DrawText(x+2,y+2,caption,color); } protected: - char caption[128]; + char caption[128] = {}; }; void CCaptionButton::Change(const char * format,...) { @@ -1677,6 +1770,7 @@ static void change_action_text(const char* text,Bit8u col) { static void SetActiveBind(CBind * _bind) { + assert(_bind); mapper.abind=_bind; if (_bind) { bind_but.bind_title->Enable(true); @@ -1700,6 +1794,7 @@ static void SetActiveBind(CBind * _bind) { } static void SetActiveEvent(CEvent * event) { + assert(event); mapper.aevent=event; mapper.redraw=true; mapper.addbind=false; @@ -2073,7 +2168,7 @@ static SDL_Color map_pal[CLR_LAST]={ static void CreateStringBind(char * line) { line=trim(line); char * eventname=StripWord(line); - CEvent * event; + CEvent * event = nullptr; for (CEventVector_it ev_it = events.begin(); ev_it != events.end(); ++ev_it) { if (!strcasecmp((*ev_it)->GetName(),eventname)) { event=*ev_it; @@ -2083,7 +2178,7 @@ static void CreateStringBind(char * line) { LOG_MSG("MAPPER: Can't find key binding for %s event", eventname); return ; foundevent: - CBind * bind; + CBind * bind = nullptr; for (char * bindline=StripWord(line);*bindline;bindline=StripWord(line)) { for (CBindGroup_it it = bindgroups.begin(); it != bindgroups.end(); ++it) { bind=(*it)->CreateConfigBind(bindline); @@ -2178,6 +2273,9 @@ static struct { }; static void ClearAllBinds() { + // wait for the auto-typer to complete because it might be accessing events + mapper.typist.Wait(); + for (CEvent *event : events) { event->ClearBinds(); } @@ -2592,7 +2690,6 @@ void MAPPER_DisplayUI() { static void MAPPER_Init(Section *sec) { (void) sec; // unused but present for API compliance - // LOG_MSG("MAPPER: Initialized"); QueryJoysticks(); if (buttons.empty()) CreateLayout(); @@ -2603,6 +2700,9 @@ static void MAPPER_Init(Section *sec) { static void MAPPER_Destroy(Section *sec) { (void) sec; // unused but present for API compliance + // Stop any ongoing typing as soon as possible (because it access events) + mapper.typist.Stop(); + // Release all the accumulated allocations by the mapper for (auto & ptr : events) delete ptr; @@ -2637,8 +2737,6 @@ static void MAPPER_Destroy(Section *sec) { // Decrement our reference pointer to the Joystick subsystem SDL_QuitSubSystem(SDL_INIT_JOYSTICK); - // LOG_MSG("MAPPER: release resources"); - } void MAPPER_BindKeys() { @@ -2649,21 +2747,34 @@ void MAPPER_BindKeys() { if (!MAPPER_CreateBindsFromFile()) CreateDefaultBinds(); - for (CButton_it but_it = buttons.begin(); but_it != buttons.end(); ++but_it) { + for (CButton_it but_it = buttons.begin(); but_it != buttons.end(); ++but_it) (*but_it)->BindColor(); - } - if (SDL_GetModState()&KMOD_CAPS) { - for (CBindList_it bit = caps_lock_event->bindlist.begin(); bit != caps_lock_event->bindlist.end(); ++bit) { - (*bit)->ActivateBind(32767,true,false); - (*bit)->DeActivateBind(false); - } - } - if (SDL_GetModState()&KMOD_NUM) { - for (CBindList_it bit = num_lock_event->bindlist.begin(); bit != num_lock_event->bindlist.end(); ++bit) { - (*bit)->ActivateBind(32767,true,false); - (*bit)->DeActivateBind(false); + + if (SDL_GetModState()&KMOD_CAPS) + MAPPER_TriggerEvent(caps_lock_event, false); + + if (SDL_GetModState()&KMOD_NUM) + MAPPER_TriggerEvent(num_lock_event, false); +} + +std::vector MAPPER_GetEventNames(const std::string &prefix) { + std::vector key_names; + key_names.reserve(events.size()); + for (auto & e : events) { + const std::string name = e->GetName(); + const std::size_t found = name.find(prefix); + if (found != std::string::npos) { + const std::string key_name = name.substr(found + prefix.length()); + key_names.push_back(key_name); } } + return key_names; +} + +void MAPPER_AutoType(std::vector &sequence, + const uint32_t wait_ms, + const uint32_t pace_ms) { + mapper.typist.Start(&events, sequence, wait_ms, pace_ms); } // Activate user-specified or default binds diff --git a/src/misc/support.cpp b/src/misc/support.cpp index 0fcbe240..83e6116e 100644 --- a/src/misc/support.cpp +++ b/src/misc/support.cpp @@ -21,19 +21,31 @@ #include #include #include +#include #include #include #include #include #include -#include - +#include + #include "dosbox.h" #include "cross.h" #include "debug.h" #include "support.h" #include "video.h" +bool str_to_double(const std::string& input, double &value) { + bool result = false; + size_t bytes_read = 0; + try { + value = std::stod(input, &bytes_read); + if (bytes_read == input.size()) + result = true; + } catch (std::invalid_argument &) {} + return result; +} + std::string get_basename(const std::string& filename) { // Guard against corner cases: '', '/', '\', 'a' if (filename.length() <= 1) diff --git a/vs/dosbox.vcxproj b/vs/dosbox.vcxproj index e6ef5a63..fe5acf55 100644 --- a/vs/dosbox.vcxproj +++ b/vs/dosbox.vcxproj @@ -174,6 +174,7 @@ + @@ -346,6 +347,7 @@ +