• Welcome to PiBoSo Official Forum. Please login or sign up.
 
April 26, 2024, 04:51:34 AM

News:

GP Bikes beta21c available! :)


Input plugin - inject to existing controller

Started by Nihil, February 27, 2022, 02:26:14 PM

Previous topic - Next topic

Nihil

Hi I'm trying to write an input plugin that would inject inputs to an existing controller (i.e Xbox gamepad).

I've created and compiled a DLI, setting the `SControllerInfo_t` m_szUUID to the existing external controller UUID taken from `controls.txt` in profile (Don't know where to take `m_iID` from),
And injecting max values in `GetControllerData`.


This doesn't seem to work though.
I think it's compiled correctly as `printf` statements work.

Is that even possible?

Also, am I using the data structures / populating _psData correctly?
What are _iID and _iIndex for? they seem to be always zero.

Thanks!

Here's my input file:
/*
If compiled as C++, extern "C" must be added to declaration of functions to export
*/
#include <string.h>
#include <stdio.h>

typedef struct
{
char m_szName[100];
char m_szUUID[37]; /* universally unique identifier */
int m_iID; /* internal unique ID */
char m_iNumAxis; /* number of axis */
short m_aaiAxisRange[6][3]; /* min, max and center value of each axis */
char m_iNumSliders; /* number of sliders */
short m_aiSliderRange[6]; /* max value of each slider */
char m_iNumButtons; /* number of buttons */
char m_iNumPOV; /* number of POVs */
char m_iNumDials; /* number of dials */
char m_aiDialRange[8]; /* max value of dials */
} SControllerInfo_t;

typedef struct
{
short m_aiAxis[6];
short m_aiSlider[6];
char m_aiButton[32];
unsigned short m_aiPOV[2];
char m_aiDial[8];
} SControllerData_t;

__declspec(dllexport) int Version()
{
return 3;
}

/* called when software is started. If return value is not 0, the plugin is disabled */
__declspec(dllexport) int Startup()
{
FILE *fp;
fp = fopen ("C:/Users/nihil/Desktop/testInput.txt", "w");
fprintf(fp, "test");
fclose(fp);
return 0;
}

/* called when software is closed */
__declspec(dllexport) void Shutdown()
{
}

/* called every rendering frame. This function is optional */
__declspec(dllexport) void Update()
{

}

/* called when a control is queried */
__declspec(dllexport) void Reset()
{
}

/* called every few seconds to support hot plugging. The return value is the number of active controllers */
__declspec(dllexport) int GetNumControllers()
{
//return 0;
return 1;
}

/* _iIndex is the 0 based controller index. _psInfo must be filled with controller info */
__declspec(dllexport) int GetControllerInfo(int _iIndex,SControllerInfo_t *_psInfo)
{
SControllerInfo_t myinfo = {
"myInput",
"47f219dc-2cb8-4afc-b670-56c637f64a5a",
0,
4,
{
{0, 32767, 16383},
{0, 32767, 16383},
{0, 32767, 16383},
{0, 32767, 16383}
},
2,
{32767,32767,0,0,0,0},
6,
2,
2,
{255,255,0,0,0,0,0,0}
};

FILE *fp;
fp = fopen ("C:/Users/nihil/Desktop/testInput.txt", "a");
fprintf(fp, "iIndex = %d", _iIndex);
fclose(fp);
return 0;

_psInfo = &myinfo;


return 0;
}

/* _iID is the unique controller ID. _psData must be filled with controller data */
__declspec(dllexport) int GetControllerData(int _iID,SControllerData_t *_psData)
{

SControllerData_t mydata = {
{32767, 32767, 32767, 32767, 0, 0},
{32767, 32767, 32767, 32767, 0, 0},
"BUTTONS",  // This is probably wrong, but not focusing on buttons at the moment
{20, 20},
"DIAL"   // This is probably wrong, but not focusing on dials at the moment
};

FILE *fp;
fp = fopen ("C:/Users/nihil/Desktop/testInput.txt", "a");
fprintf(fp, "iID = %d", _iID);
fclose(fp);
return 0;

_psData = &mydata;

return 0;
}

HornetMaX

I'm not sure of what you're trying to achieve here.

An input plugin is to send data from a device to GPB, not the other way around.
You don't need to create one for an XBox gamepad as GPB already supports gamepads.


Quote from: Nihil on February 27, 2022, 02:26:14 PMAlso, am I using the data structures / populating _psData correctly?
I'd say no. In GetControllerInfo you declare myinfo as a local variable (hence allocated on the stack) but then you pass its address back to GPB: that address is garbage as soon as the execution leaves your function. I'd expect GPB to crash badly.

Same thing in GetControllerData. Plus, in GetControllerData you have twice a return statement, the first one being even before assigning _psData...

Nihil

February 27, 2022, 11:38:43 PM #2 Last Edit: February 27, 2022, 11:44:34 PM by Nihil
Quote from: HornetMaX on February 27, 2022, 09:18:58 PMI'm not sure of what you're trying to achieve here.

An input plugin is to send data from a device to GPB, not the other way around.
You don't need to create one for an XBox gamepad as GPB already supports gamepads.

I'm trying to write a "rider assist" plugin that will correct user input, like prevent 100% throttle at max lean,
Or will save the bike from crash like giving a steering command when the front wheel looses grip.

For that I want an input plugin that will override/inject inputs to GPB when a condition is met, otherwise allow the user to control the bike with his controller.

Quote from: HornetMaX on February 27, 2022, 09:18:58 PMI'd say no. In GetControllerInfo you declare myinfo as a local variable (hence allocated on the stack) but then you pass its address back to GPB: that address is garbage as soon as the execution leaves your function. I'd expect GPB to crash badly.

Same thing in GetControllerData. Plus, in GetControllerData you have twice a return statement, the first one being even before assigning _psData...

Oops, sorry about those brainfarts (it didn't crash though!).
I've corrected these issues, taking the struct population out of the function.
Still not working/not injecting input into GPB as the controller.

Added some debug prints -
/*
If compiled as C++, extern "C" must be added to declaration of functions to export
*/
#include <string.h>
#include <stdio.h>

typedef struct
{
 char m_szName[100];
 char m_szUUID[37]; /* universally unique identifier */
 int m_iID; /* internal unique ID */
 char m_iNumAxis; /* number of axis */
 short m_aaiAxisRange[6][3]; /* min, max and center value of each axis */
 char m_iNumSliders; /* number of sliders */
 short m_aiSliderRange[6]; /* max value of each slider */
 char m_iNumButtons; /* number of buttons */
 char m_iNumPOV; /* number of POVs */
 char m_iNumDials; /* number of dials */
 char m_aiDialRange[8]; /* max value of dials */
} SControllerInfo_t;

typedef struct
{
 short m_aiAxis[6];
 short m_aiSlider[6];
 char m_aiButton[32];
 unsigned short m_aiPOV[2];
 char m_aiDial[8];
} SControllerData_t;

 SControllerInfo_t myinfo = {
 "XInput",
 "47f219dc-2cb8-4afc-b670-56c637f64a5a",
 0,
 4,
 {
 {0, 32767, 16383},
 {0, 32767, 16383},
 {0, 32767, 16383},
 {0, 32767, 16383}
 },
 2,
 {32767,32767,0,0,0,0},
 6,
 2,
 2,
 {255,255,0,0,0,0,0,0}
 };

 SControllerData_t mydata = {
 {32767, 32767, 32767, 32767, 0, 0},
 {32767, 32767, 32767, 32767, 0, 0},
 "C_AXIS",
 {20, 20},
 "C_AXIS"
 };

__declspec(dllexport) int Version()
{
 return 3;
}

/* called when software is started. If return value is not 0, the plugin is disabled */
__declspec(dllexport) int Startup()
{
 FILE *fp;
 fp = fopen ("C:/Users/nihil/Desktop/testInput.txt", "w");
 fprintf(fp, "test \n");
 fclose(fp);
 return 0;
}

/* called when software is closed */
__declspec(dllexport) void Shutdown()
{
}

/* called every rendering frame. This function is optional */
__declspec(dllexport) void Update()
{

}

/* called when a control is queried */
__declspec(dllexport) void Reset()
{
}

/* called every few seconds to support hot plugging. The return value is the number of active controllers */
__declspec(dllexport) int GetNumControllers()
{
 //return 0;
 return 1;
}

/* _iIndex is the 0 based controller index. _psInfo must be filled with controller info */
__declspec(dllexport) int GetControllerInfo(int _iIndex,SControllerInfo_t *_psInfo)
{

 FILE *fp;
 fp = fopen ("C:/Users/nihil/Desktop/testInput.txt", "a");
 fprintf(fp, "iIndex = %d \n", _iIndex);
 fprintf(fp, "_psInfo.iID = %d \n", _psInfo->m_iID);

 _psInfo = &myinfo;

 fprintf(fp, "my _psInfoiID = %d \n", _psInfo->m_iID);
 fclose(fp);

 return 0;
}

/* _iID is the unique controller ID. _psData must be filled with controller data */
__declspec(dllexport) int GetControllerData(int _iID,SControllerData_t *_psData)
{


 FILE *fp;
 fp = fopen ("C:/Users/nihil/Desktop/testInput.txt", "a");
 fprintf(fp, "iID = %d \n", _iID);
 fprintf(fp, "_psData m_aiAxis = %d \n", _psData->m_aiAxis[0]);

 _psData = &mydata;

 fprintf(fp, "my _psData m_aiAxis = %d \n", _psData->m_aiAxis[0]);
 fclose(fp);

 return 0;
}

and the output log, you can see my input value "32767" is being set, at least inside the function
test
iIndex = 0
_psInfo.iID = 0
my _psInfoiID = 0
iID = 0
_psData m_aiAxis = 0
my _psData m_aiAxis = 32767
iID = 0
_psData m_aiAxis = 0
my _psData m_aiAxis = 32767
iID = 0
_psData m_aiAxis = 0
my _psData m_aiAxis = 32767
iID = 0
_psData m_aiAxis = 0
my _psData m_aiAxis = 32767

HornetMaX

I never tried input plugins so I can easily be wrong on the below:
  • I'm not sure you're populating the fields m_aiButton and m_aiDial (in mydata) correctly: I suspect they should not be really "chars" ... I guess they are in fact integer numbers (as the prefix m_ai* indicates), for the status of each button.
  • In myinfo m_szName is set to "XInput": bad idea. This should be the name you give to your custom controller (I think), so call it "MyOwnController" or something special.
  • Do you actually see your own controller in GPB settings page ?

@PiBoSO: what's the intended usage of Update() and Reset() calls ?

Nihil

March 01, 2022, 05:13:48 AM #4 Last Edit: March 01, 2022, 05:21:20 AM by Nihil
Quote from: HornetMaX on February 28, 2022, 05:11:40 PMI'm not sure you're populating the fields m_aiButton and m_aiDial (in mydata) correctly: I suspect they should not be really "chars" ... I guess they are in fact integer numbers (as the prefix m_ai* indicates), for the status of each button.
Yes, I'm pretty sure these should be 0/1 ints, I was just lazy to create an array of 32 zeroes. I'll try correcting it and see if it works.

Quote from: HornetMaX on February 28, 2022, 05:11:40 PMIn myinfo m_szName is set to "XInput": bad idea. This should be the name you give to your custom controller (I think), so call it "MyOwnController" or something special.

Do you actually see your own controller in GPB settings page
Agreed. I tried naming it "XInput" to hijack my physical controller that's defined in GPB under that name, and make the input plugin write to the same controller.
That didn't work. And naming it anything else makes no difference-

I can not actually see it in GPB, not in selection dropdown, not in "calibrate" screen.
I can't really see the xbox controller either, but it's axis/buttons are shown once I bind them to something.
So maybe if I try binding lean/throttle to my pluin it will show, but I need the plugin to be able to send a key when I'm changing the settings. might try that.

Quote from: HornetMaX on February 28, 2022, 05:11:40 PM@PiBoSO: what's the intended usage of Update() and Reset() calls ?

I was wondering about the Update() func as well -
Should I read controller state/data values every frame in Update(),
Or should I read controller data only when GetControllerData() is called?
It's frequency doesn't seem very high.

And of course the main question - can an input plugin write data as a controller already connected, i.e "two controllers, acting as one"

Thanks for your help!!!
 

Vini

March 01, 2022, 03:00:07 PM #5 Last Edit: March 01, 2022, 03:07:36 PM by Vini
You might want to go through external tools/libraries for this.
Grab all the telemtry from GPB and the inputs from your physical controller, modify/modulate inputs as needed and then pipe everything into a new, separate virtual X360 pad which is used as the sole input feeder for GPB.
This will probably be the cleanest and most adaptable solution in the long run.
ViGEm is a very powerful toolkit. Even just the AHK wrapper is enough to quickly write useful input scripts.
You will possibly have to hide/disable the physical controller, either through external tools or internal settings: https://forum.piboso.com/index.php?topic=9789.0

BTW, you do realize that you are essentially creating a cheat program?

HornetMaX

If you're not even seeing your custom input device in the calibration thing (Settings/Inputs tab, Calibration button) then that's already a problem.

@Vini: I don't think what you suggest is needed or any cleaner. Input plugin feature of GPB + a way to get telemetry from the bike (e.g. via the proxy plugin) should be enough and doesn't require anything external. My understanding is that if you manage to create an input plugin, it will be visible to GPB as an independent input device: if your input plugin actually uses a physical pad, both will be visible to GPB. Not sure how to assign what to what but ...

P.S.
At some point I had in mind to create an input plugin to distort the X-Y limitations of joypads (in some cases you can't get max X and max Y at the same time) but then I dropped the idea.

Vini

Directly mixing physical controller inputs with emulated ones seems a bit messy to me and you wouldn't be able to adapt it to other games.
But if he gets it working, it is of course the quickest solution.

Nihil

March 01, 2022, 07:28:13 PM #8 Last Edit: March 01, 2022, 07:33:29 PM by Nihil
Thanks @Vini, I'm thinking along the same lines -

I prefer the input plugin to mix controller inputs with emulated ones as it's a bit simpler and stand-alone GPB solution. Just a tool you can enable without external executables.

But as you suggested, my plan B is to pass the physical controller inputs through a MITM.
I've already started working on my own implementation of XInput to read the controller, now I can either feed the modified output to VJoy or ViGEm (looks interesting! thanks!) and bind that virt-controller in GPB,

Or I could include my XInput reader code in the input plugin directly (and probably will need to disable the GPB original xinput.dli or use HidHide).
BTW, in this approach, Update() will probably be where I read the controller inputs.

As for it being a cheat program - I'm also thinking that.
For me it's more of an experiment in developing rider assist tech that could maybe be used in the real world.
I know BMW were working on something similar. "Smart Bike" or something it was called.

I can justify it being used in gameplay by saying in reality riders get feedback from the front wheel and can maybe save a front slide, but in GPB we don't get those physical sensations.
But still, I don't know if I should release it or use it beyond research purposes.

Vini

March 01, 2022, 08:20:30 PM #9 Last Edit: March 01, 2022, 08:22:47 PM by Vini
It's an interesting idea for sure.
I would gladly contribute telemetry and other feedback.
Would be nice to finally have realistic TC/AW in GPB, not the stoneage one we currently have.
From years of playing without TC and direct lean 100%, I know a couple of quirks that could potentially provide helpful info for your control algorithms.

Regardless of the path you choose to emulate inputs, you should probably hide the physical controller from the game to avoid bugs.
I have some experience in working with multiple virtual controllers in GPB and even just calibrating it properly can become quite ugly.

Vini

Quote from: Nihil on March 01, 2022, 07:28:13 PMI can justify it being used in gameplay by saying in reality riders get feedback from the front wheel and can maybe save a front slide, but in GPB we don't get those physical sensations.
I hope you are taking full of advantage of MaxHUD rumble. When set up properly, you can get extremely useful feedback and really build a feeling for the tyre limit.

HornetMaX

In case it helps, I use SFML for joypad related stuff. No need to write your own low-level one.

PiBoSo

Quote from: HornetMaX on February 28, 2022, 05:11:40 PM@PiBoSO: what's the intended usage of Update() and Reset() calls ?

The "Update" function is called before the GetNumControllers and GetControllerInfo polling.
It is used in the XInput plugin to handle hot-plugging and returning the correct number of connected controllers.

The "Reset" function is called before a control is queried and is used by the "minput" plugin to reset the position to zero and allow the mouse to be assigned.
"La perfezione non è il nostro obiettivo, è la nostra tendenza".

HornetMaX

Quote from: PiBoSo on March 02, 2022, 11:29:02 PMThe "Update" function is called before the GetNumControllers and GetControllerInfo polling.
It is used in the XInput plugin to handle hot-plugging and returning the correct number of connected controllers.
But why separating Update() from GetNumControllers() ?
The comments seem to indicate Update() is called every rendering frame (so high freq) but GetNumControllers() only every few secs. Couldn't one do whatever is done in Update() directly into GetNumControllers() ?

Quote from: PiBoSo on March 02, 2022, 11:29:02 PMThe "Reset" function is called before a control is queried and is used by the "minput" plugin to reset the position to zero and allow the mouse to be assigned.
I.e. called before GetControllerData() ?



@Nihil: Just noticed: you must probably fill _psInfo and _psData, not assign a new address. I.e. something like "psInfo->m_iNumSliders = 0;" etc.

Nihil

Quote from: HornetMaX on March 03, 2022, 12:34:05 AM@Nihil: Just noticed: you must probably fill _psInfo and _psData, not assign a new address. I.e. something like "psInfo->m_iNumSliders = 0;" etc.

Oh hey! Good catch!!
I replaced my address assignment -
_psData = &mydata;With this (too lazy to assign member by member)-
(*_psData) = mydata;
And it works!
Guess the original _psData address is used elsewhere so can't re-assign new address or something?
I think using "psInfo->m_iNumSliders = 0;" will also work and allow setting a specific axis/slider/button instead of building the whole struct like I did.

Anyway, results -
As you remember I wanted to have this input plugin pretend to be the controller, so both it and the controller would be bound to lean or throttle and mix inputs.
So I gave my input plugin the same UUID as the gamepad.
That didn't work.

1. Plugin does not appear in calibration screen. even when working and transmitting data.

2. In-game controller name -
original binding, without plugin - gamepad name when bound to lean - "XInput X-Axis"
original binding, with plugin - gamepad name when bound to lean - "C0 X-Axis"
rebind, with plugin - plugin name when bound to lean - "C1 X-Axis"
rebind, with plugin - gamepad name when bound to lean - "Joy - X-Axis"

3. m_szName is ignored? I have it as "XInput" in my plugin but still shows as "C1".

So even if plugin uses the same UUID as the gamepad, they do not mix signals.
Not changing the binding from original (bound to gamepad), the plugin just "takes over" the UUID and original settings (even though the name changed to "C1").
Re-mapping the controls shows gamepad is now a new device.

So I guess the m_iID for the gamepad gets changed and I can't control that, and UUID is just responsible for binding, which is why existing binds are transferred to plugin.

Bottom line - no mixing. I'll probably go with extermal mixing and feeding to ViGEm.

Good stuff :)