1
0
Fork 0

Add AUTOTYPE to DOSBox's programs

AUTOTYPE performs scripted keyboard entry into the running
DOS program.

It can be used to reliably skip intros, answer Q&A style questions
that some games ask on startup, or conduct a simple demo.

It allows for delaying input by any number of fractional seconds,
as well defining the pacing between keystrokes. It uses the
comma character "," to insert additional delays similar to modern
phone numbers.

It uses key_* names as defined by the mapper to avoid using SDL
scancodes[1], which are unstable across platforms. This approach
also allows the triggering of custom key bindings the use has
defined.

[1] https://wiki.libsdl.org/SDL_GetScancodeName

"Warning: The returned name is by design not stable across
platforms, e.g. the name for SDL_SCANCODE_LGUI is "Left GUI" under
Linux but "Left Windows" under Microsoft Windows, and some
scancodes like SDL_SCANCODE_NONUSBACKSLASH don't have any name at
all. There are even scancodes that share names, e.g.
SDL_SCANCODE_RETURN and SDL_SCANCODE_RETURN2 (both called
"Return"). This function is therefore unsuitable for creating a
stable cross-platform two-way mapping between strings and
scancodes."
This commit is contained in:
krcroft 2020-03-25 18:48:24 -07:00 committed by Patryk Obara
parent ee7107470e
commit 239396fec8
11 changed files with 377 additions and 29 deletions

View file

@ -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,

28
README
View file

@ -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.

View file

@ -19,6 +19,11 @@
#ifndef DOSBOX_MAPPER_H
#define DOSBOX_MAPPER_H
#include <string>
#include <vector>
#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<std::string> MAPPER_GetEventNames(const std::string &prefix);
void MAPPER_AutoType(std::vector<std::string> &sequence,
const uint32_t wait_ms,
const uint32_t pacing_ms);
#define MMOD1 0x1
#define MMOD2 0x2

View file

@ -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);

View file

@ -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 \

View file

@ -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);

View file

@ -0,0 +1,158 @@
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>
#include <string>
#include <sstream>
#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<std::string> 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<double>::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<uint32_t>(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<uint32_t>(pace_s * 1000);
// Get the button sequence
std::vector<std::string> 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;
}

View file

@ -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);

View file

@ -21,12 +21,14 @@
#include <algorithm>
#include <cassert>
#include <cctype>
#include <chrono>
#include <cinttypes>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <list>
#include <thread>
#include <vector>
#include <SDL.h>
@ -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<CKeyBindGroup *> 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<CEvent*> *ext_events,
std::vector<std::string> &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<std::string> m_sequence;
std::vector<CEvent*> *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<std::string> MAPPER_GetEventNames(const std::string &prefix) {
std::vector<std::string> 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<std::string> &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

View file

@ -21,19 +21,31 @@
#include <assert.h>
#include <cctype>
#include <ctype.h>
#include <cstring>
#include <functional>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <cstring>
#include <stdexcept>
#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)

View file

@ -174,6 +174,7 @@
<ClCompile Include="..\src\dos\drive_local.cpp" />
<ClCompile Include="..\src\dos\drive_overlay.cpp" />
<ClCompile Include="..\src\dos\drive_virtual.cpp" />
<ClCompile Include="..\src\dos\program_autotype.cpp" />
<ClCompile Include="..\src\fpu\fpu.cpp" />
<ClCompile Include="..\src\gui\midi.cpp" />
<ClCompile Include="..\src\gui\render.cpp" />
@ -346,6 +347,7 @@
<ClInclude Include="..\src\dos\Ntddcdrm.h" />
<ClInclude Include="..\src\dos\Ntddscsi.h" />
<ClInclude Include="..\src\dos\Ntddstor.h" />
<ClInclude Include="..\src\dos\program_autotype.h" />
<ClInclude Include="..\src\fpu\fpu_instructions.h" />
<ClInclude Include="..\src\fpu\fpu_instructions_x86.h" />
<ClInclude Include="..\src\gui\midi_win32.h" />