There is not, as far as I'm aware.
Because VoiceAttack only allows a single joystick to be mapped per command, you can't really swap them out(otherwise you could have the physical stick as VoiceAttack joystick 1, and the virtual as joystick 2, then swapping between them).
In theory you could swap out the entries in the user.config file while VoiceAttack isn't running(the joystick values are loaded at startup), but you need to be familiar with XML, and(as you want to automate this) have experience with a programming language and its XML library.
Even then, if a VoiceAttack update makes a change to the way joystick data is stored, you'd need to update your application(and have a version check, so it doesn't break the config file).EDIT 2024: Utility updated to support up to 8 joystick slots (note that the current release version of VoiceAttack supports 4 slots; more slots are supported by this utility for future-proofing)
This utility now requires .NET Framework 4.8 (which you should have anyway, as VoiceAttack also requires it)
Alright, so a few(I lost count) hours later I have this:
Run application 'C:\Program Files(x86)\VoiceAttack\JoystickSwapper.exe' -with parameters 'File="C:\Users\User\AppData\Local\VoiceAttack.com\VoiceAttack.exe_Url_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0.0.0.1\user.config" JoystickNumber=1 JoystickID=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa JoystickName="My Joystick" JoystickEnabled=True JoystickEnablePOV=False JoystickPOV1Style=0 JoystickPOV2Style=0 JoystickPOV3Style=0 JoystickPOV4Style=0'
Run application 'C:\Program Files(x86)\VoiceAttack\RestartVA.bat' (hidden)
Close window 'VoiceAttack'
What the application will do is replace the config data for a given joystick with data you provide.
Now, before you actually try any of this, make sure you have backups of your VoiceAttack files(the contents of "%localappdata%\VoiceAttack.com" and "%appdata%/VoiceAttack/" at least, so you have your working config file and profile database.
I take no responsibility if this application or anything else affects your installation of VoiceAttack, your computer, your life, or the universe as we know it.
I ran it through VirusTotal as a matter of course,
and it comes back clean, as it should.
EDIT 2024: 8-joystick version scanned and linked
The setup would be like so:
Find your VoiceAttack config file, it will be in a subfolder of "%localappdata%\VoiceAttack.com". You can easily copy the path by holding Shift, then right clicking the config file and clicking "Copy as path" in the context menu.
Now run the application with the "File" parameter, E.G.
File="C:\Users\User\AppData\Local\VoiceAttack.com\VoiceAttack.exe_Url_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0.0.0.1\user.config"
Note that the double quotes are important here. If you used "Copy as path" they'll automatically be included, so make sure you only have one set of them.
This will make the application read your config file and output the current data for joystick 1, for convenience it will copy this data to the clipboard so you can paste it.
Now setup the VoiceAttack command as shown above, and paste the arguments into the "With these parameters" field of the Run an Application" dialog.
The batch file(RestartVA.bat) contains the following:
:wait
tasklist|find /i "VoiceAttack"
IF "%ERRORLEVEL%"=="1" (
start "" "C:\Program Files(x86)\VoiceAttack\VoiceAttack.exe"
GOTO exit
)
GOTO wait
:exit
You need this file to restart VoiceAttack, because the date from the config file is only loaded at startup(and an application cannot restart itself).
When you run the command, the application will swap out the data you specified and VoiceAttack will restart with the new data in place.
However, because this only modifies the config file and joystick button assignments are stored within the profiles themselves(which makes those assignments device-independent, in theory), this cannot change those assignments.
I'm hoping the Target virtual joystick either has the same assignments or can be programmed to that effect, because otherwise this application is likely useless to you(And just about anyone else).
If you want to modify joystick 2 instead, you can supply the "JoystickNumber" argument together with the "File" argument.
E.G.
'File="C:\Users\User\AppData\Local\VoiceAttack.com\VoiceAttack.exe_Url_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0.0.0.1\user.config" JoystickNumber=2
As long as only the "File" and "JoystickNumber" arguments are supplied, the application will read data and copy it to the clipboard, if any other arguments are supplied, data will be written.
You don't have to supply all arguments if you don't want to, E.G.
File="C:\Users\User\AppData\Local\VoiceAttack.com\VoiceAttack.exe_Url_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0.0.0.1\user.config" JoystickID=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab JoystickName="My other Joystick"
Is perfectly valid.
"File" is always mandatory, as the application needs to know where to read and write data from, but argument order is not important.
E.G. you could use
JoystickName="My other Joystick" JoystickID=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaab File="C:\Users\User\AppData\Local\VoiceAttack.com\VoiceAttack.exe_Url_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0.0.0.1\user.config"
Here's the source code of the application(In C#) if anyone's curious(Or crazy enough to try and update/improve the thing at some point):
EDIT 2024: Source code updated to 8-joystick version
using System;
using System.Xml;
using System.Windows.Forms;
namespace Config_Swapper
{
class Program
{
[STAThread]
public static int Main(string[] args)
{
// Test if input arguments were supplied:
if (args.Length == 0)
{
Console.WriteLine("Error: No input arguments supplied");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
string file = "";
int joystickNumber = 1;
string joystickID = "";
string joystickName = "";
string joystickEnabled = "";
string joystickEnablePOV = "";
string joystickPOV1Style = "";
string joystickPOV2Style = "";
string joystickPOV3Style = "";
string joystickPOV4Style = "";
bool dumpParameters = false;
foreach (string s in args)
{
if (!s.Contains("="))
{
Console.WriteLine("Error: \"" + s + "\" must supply a value separated by \"=\"");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
string[] arg = s.Split('=');
if (string.IsNullOrEmpty(arg[1]))
{
Console.WriteLine("Error: No value supplied for \"" + arg[0] + "\"");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
switch (arg[0])
{
case "File":
file = arg[1];
break;
case "JoystickNumber":
if (arg[1] != "1" && arg[1] != "2" && arg[1] != "3" && arg[1] != "4" && arg[1] != "5" && arg[1] != "6" && arg[1] != "7" && arg[1] != "8")
{
Console.WriteLine("Error: JoystickNumber value must be 1, 2, 3, 4, 5, 6, 7, or 8");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
joystickNumber = Convert.ToInt32(arg[1]);
break;
case "JoystickID":
joystickID = arg[1];
break;
case "JoystickName":
joystickName = arg[1];
break;
case "JoystickEnabled":
if (arg[1] == "True" || arg[1] == "1")
{
joystickEnabled = "True";
}
else if (arg[1] == "False" || arg[1] == "0")
{
joystickEnabled = "False";
}
else
{
Console.WriteLine("Error: JoystickEnabled value must be True or False(Case sensitive)");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
break;
case "JoystickEnablePOV":
if (arg[1] == "True" || arg[1] == "1")
{
joystickEnablePOV = "True";
}
else if (arg[1] == "False" || arg[1] == "0")
{
joystickEnablePOV = "False";
}
else
{
Console.WriteLine("Error: JoystickEnablePOV value must be True or False(Case sensitive)");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
break;
case "JoystickPOV1Style":
if (arg[1] != "0" && arg[1] != "1" && arg[1] != "2" && arg[1] != "3" && arg[1] != "4" && arg[1] != "8")
{
Console.WriteLine("Error: JoystickPOV1Style value must be 0, 1, 2, 3, 4 or 8");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
joystickPOV1Style = arg[1];
break;
case "JoystickPOV2Style":
if (arg[1] != "0" && arg[1] != "1" && arg[1] != "2" && arg[1] != "3" && arg[1] != "4" && arg[1] != "8")
{
Console.WriteLine("Error: JoystickPOV2Style value must be 0, 1, 2, 3, 4 or 8");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
joystickPOV2Style = arg[1];
break;
case "JoystickPOV3Style":
if (arg[1] != "0" && arg[1] != "1" && arg[1] != "2" && arg[1] != "3" && arg[1] != "4" && arg[1] != "8")
{
Console.WriteLine("Error: JoystickPOV3Style value must be 0, 1, 2, 3, 4 or 8");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
joystickPOV3Style = arg[1];
break;
case "JoystickPOV4Style":
if (arg[1] != "0" && arg[1] != "1" && arg[1] != "2" && arg[1] != "3" && arg[1] != "4" && arg[1] != "8")
{
Console.WriteLine("Error: JoystickPOV4Style value must be 0, 1, 2, 3, 4 or 8");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
joystickPOV4Style = arg[1];
break;
default:
Console.WriteLine("Error: invalid argument \"" + arg[0] + "\"");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
}
if (file == "")
{
Console.WriteLine("Error: File has not been specified, this is the only mandatory argument(the application must know where to look for data in order to function!)");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
if (joystickID == "" && joystickName == "" && joystickEnabled == "" && joystickEnablePOV == "" && joystickPOV1Style == "" && joystickPOV2Style == "" && joystickPOV3Style == "" && joystickPOV4Style == "")
{
dumpParameters = true;
}
// instantiate XmlDocument and load XML from file
XmlDocument doc = new XmlDocument();
doc.Load(@file);
// get a list of nodes
XmlNodeList aNodes = doc.SelectNodes("/configuration/userSettings/VoiceAttack.Properties.Settings/setting");
// loop through all "setting" nodes
foreach (XmlNode aNode in aNodes)
{
// grab the "name" attribute
XmlAttribute idAttribute = aNode.Attributes["name"];
// check if that attribute even exists...
if (idAttribute != null)
{
// if yes - read its current value
string currentValue = idAttribute.Value;
if (!string.IsNullOrEmpty(currentValue))
{
//Basic info
if (idAttribute.Value == "Joystick" + joystickNumber + "Name")
{
if (joystickName != "")
{
aNode.InnerXml = "<value>" + joystickName + "</value>";
}
else
{
joystickName = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "Name: " + aNode.InnerText);
}
else if(idAttribute.Value == "Joystick" + joystickNumber + "Id")
{
if (joystickID != "")
{
aNode.InnerXml = "<value>" + joystickID + "</value>";
}
else
{
joystickID = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "ID: " + aNode.InnerText);
}
else if(idAttribute.Value == "Joystick" + joystickNumber + "Enabled")
{
if (joystickEnabled != "")
{
aNode.InnerXml = "<value>" + joystickEnabled + "</value>";
}
else
{
joystickEnabled = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "Enabled: " + aNode.InnerText);
}
//POV Hats
else if(idAttribute.Value == "Joystick" + joystickNumber + "EnablePOV")
{
if (joystickEnablePOV != "")
{
aNode.InnerXml = "<value>" + joystickEnablePOV + "</value>";
}
else
{
joystickEnablePOV = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "EnablePOV: " + aNode.InnerText);
}
else if(idAttribute.Value == "Joystick" + joystickNumber + "POV1Style")
{
if (joystickPOV1Style != "")
{
aNode.InnerXml = "<value>" + joystickPOV1Style + "</value>";
}
else
{
joystickPOV1Style = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "POV1Style: " + aNode.InnerText);
}
else if(idAttribute.Value == "Joystick" + joystickNumber + "POV2Style")
{
if (joystickPOV2Style != "")
{
aNode.InnerXml = "<value>" + joystickPOV2Style + "</value>";
}
else
{
joystickPOV2Style = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "POV2Style: " + aNode.InnerText);
}
else if(idAttribute.Value == "Joystick" + joystickNumber + "POV3Style")
{
if (joystickPOV3Style != "")
{
aNode.InnerXml = "<value>" + joystickPOV3Style + "</value>";
}
else
{
joystickPOV3Style = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "POV3Style: " + aNode.InnerText);
}
else if(idAttribute.Value == "Joystick" + joystickNumber + "POV4Style")
{
if (joystickPOV4Style != "")
{
aNode.InnerXml = "<value>" + joystickPOV4Style + "</value>";
}
else
{
joystickPOV4Style = aNode.InnerText;
}
//Console.WriteLine("Joystick" + joystickNumber + "POV4Style: " + aNode.InnerText);
}
}
else
{
Console.WriteLine("Error: Value is empty");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
}
else
{
Console.WriteLine("Error: No such attribute");
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
return 1;
}
}
if (dumpParameters)
{
string output = "File=\"" + file + "\" JoystickNumber=" + joystickNumber + " JoystickID=" + joystickID + " JoystickName=\"" + joystickName + "\" JoystickEnabled=" + joystickEnabled + " JoystickEnablePOV=" + joystickEnablePOV + " JoystickPOV1Style=" + joystickPOV1Style + " JoystickPOV2Style=" + joystickPOV2Style + " JoystickPOV3Style=" + joystickPOV3Style + " JoystickPOV4Style=" + joystickPOV4Style;
Console.WriteLine("Arguments for joystick " + joystickNumber + ":");
Console.WriteLine(output);
Console.WriteLine("This has been copied to the clipboard(you can use Ctrl-V to paste it when creating a new shortcut)");
Clipboard.SetText(output);
Console.Write("Press any key to exit . . . ");
Console.ReadKey(true);
}
else
{
doc.Save(@file);
Console.WriteLine("Values have been written to " + file);
}
return 0;
}
}
}
I don't advise using anything you see in there; It's slapped together with a StackExchange example(I'd say quick and dirty, but while the latter applies it took too much time for the former), it's not flexible at all(It could have been more of a universal config manipulation thing, but that way more work to implement), it probably doesn't use any industry-standard best practices(If it does, that's by accident, I assure you), there was no attempt at optimizing performance(though it's so "simple" that no modern system should care about that), there's plenty of repeated code, there's some error checking though probably not enough(is there ever?), and it's just a novice effort(from a novice programmer/code-together-slapperer).
Also, the forum seems to chew up some of the indentation, so yeah.