mirror of
https://github.com/CoronaSend/ps4-usb-guitar-plugin.git
synced 2026-10-17 11:00:29 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2387b8615a | ||
|
|
759763d729 | ||
|
|
b779ff4334 | ||
|
|
f6415d6840 | ||
|
|
a063c0d5c4 | ||
|
|
f993ebb9a3 | ||
|
|
a8cd38a175 | ||
|
|
c8d514ce1c | ||
|
|
7076ef8ea3 | ||
|
|
97dbf7eb3d | ||
|
|
b7288bfe48 | ||
|
|
4a109bcd59 | ||
|
|
26095c19ef | ||
|
|
f3b72af773 | ||
|
|
86861f3fdf |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
bin/
|
||||
build/
|
||||
build_shim/
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,30 @@
|
||||
# USB Guitar Plugin for Rock Band 4 (PS4)
|
||||
# USB Guitar Plugin: Use a PS2 Guitar in Rock Band 4 on PS4
|
||||
|
||||
**Made by CoronaSend**
|
||||
**Made by CoronaSend** | [Join the Discord](https://discord.gg/mGRCKG9BB)
|
||||
|
||||
A GoldHEN plugin that lets a **PS2 guitar on a generic PS2-to-USB adapter** work in **Rock Band 4** on a jailbroken PS4.
|
||||
|
||||
Tested with a generic adapter (USB ID `2563:0575`) on a jailbroken PS4 with GoldHEN and Rock Band 4 version **02.21**, USA (`CUSA02084`), together with RB4DX.
|
||||
|
||||
**Start here: download `Guitar_Plugin_Release.zip` from the Releases page, extract it, and double-click `GuitarTool.exe` (see `READ_ME_FIRST.txt`).** One program installs the plugin on your PS4 and maps any guitar that does not work out of the box. Rebuild it with `tools\build_tool.ps1`.
|
||||
|
||||
**Support status**
|
||||
- `2563:0575`: verified on real hardware.
|
||||
- `0810:0001` and `0810:0003` (other generic PS2 adapters): **untested.** They use the same button layout as the verified adapter. If your buttons come out wrong, your adapter's report differs. `tools\read_raw_report.ps1` prints the raw bytes, so you can report them.
|
||||
- Only Rock Band 4 version **02.21** is supported. The plugin hooks fixed addresses in that build and does nothing on other versions.
|
||||
- Not tested without RB4DX.
|
||||
|
||||
## What does this do?
|
||||
|
||||
Rock Band 4 on PS4 only accepts official PS4 instruments (plus some PS3-era ones). An old PS2 guitar on a cheap PS2-to-USB adapter is not recognized, so the game ignores it.
|
||||
|
||||
This plugin loads into Rock Band 4 through GoldHEN and sits between the game and the PS4's USB stack. When it sees the adapter, it:
|
||||
|
||||
1. Makes the game think the adapter is a PS3 Rock Band guitar.
|
||||
2. Reads the adapter's raw button data and translates it into the format a PS3 Rock Band guitar sends (frets, strum, whammy, Start/Options).
|
||||
|
||||
You then play with your PS2 guitar as if it were a supported guitar. Nothing on the PS4 is permanently changed, and removing the plugin restores normal behavior.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Jailbroken PS4 running GoldHEN (2.3.0 or newer) with the plugin loader enabled
|
||||
@@ -36,7 +55,7 @@ Tested with a generic adapter (USB ID `2563:0575`) on a jailbroken PS4 with Gold
|
||||
|
||||
Don't replace an existing `plugins.ini` with the sample one in this folder. It exists only to show the format.
|
||||
5. **Plug the guitar adapter into the PS4 before launching the game.**
|
||||
6. Launch Rock Band 4. A "USB Guitar Plugin - Made by CoronaSend" notification means the plugin loaded.
|
||||
6. Launch Rock Band 4. A "USB Guitar Plugin - Made by CoronaSend - Discord: @CoronaSend" notification means the plugin loaded.
|
||||
|
||||
## Uninstall
|
||||
|
||||
@@ -49,8 +68,9 @@ Delete the `OrbisInstrumentalizer.prx` line from `plugins.ini`, and delete the `
|
||||
| Green, Red, Yellow, Blue, Orange | Same color fret |
|
||||
| Strum up / down | Strum up / down |
|
||||
| Whammy bar | Whammy |
|
||||
| Start or Select | Options / Start (both do the same thing) |
|
||||
| Tilt | Not mapped |
|
||||
| Start | Options / Start |
|
||||
| Select | Back, which activates **star power** in a song (tested) |
|
||||
| Tilt | Not working: the adapter reports a tilt bit, but the game ignored it in tests. Use Select for star power. |
|
||||
|
||||
## Remapping
|
||||
|
||||
@@ -95,18 +115,22 @@ if ((raw[0] & 0x02) != 0) parsed_report.buttons |= BIT(0); // red
|
||||
if ((raw[0] & 0x04) != 0) parsed_report.buttons |= BIT(2); // blue
|
||||
```
|
||||
|
||||
### Example: make Select act as Back instead of Options
|
||||
|
||||
Replace the Start/Select line with:
|
||||
|
||||
```c
|
||||
if ((raw[1] & 0x01) != 0) parsed_report.buttons |= BIT(8); // select -> back
|
||||
if ((raw[1] & 0x02) != 0) parsed_report.buttons |= BIT(9); // start -> options
|
||||
```
|
||||
|
||||
After any change, rebuild, copy the new `.prx` to the PS4, and **fully restart the game** (plugins only load at game launch).
|
||||
|
||||
## Using a different adapter
|
||||
## Different guitar or adapter: the mapping tool (no coding)
|
||||
|
||||
If the game detects your guitar but buttons do nothing, or you use another adapter, make a map file:
|
||||
|
||||
1. Plug the adapter into a Windows PC and double-click `tools\RUN_ME.bat`.
|
||||
2. Follow the prompts: press Enter, then hold each button for 3 seconds. Keep the guitar still while it records the idle state.
|
||||
3. It saves `guitar_map.ini` (and `guitar_capture.txt`) to your Desktop.
|
||||
4. Copy `guitar_map.ini` to `/data/GoldHEN/` on the PS4 (next to `plugins.ini`) and fully restart the game.
|
||||
|
||||
The klog shows `Mapping file loaded (id VVVV:PPPP)` when it is used. With no map file, the built-in layout for the verified 2563:0575 adapter and guitar is used. Map file format is one `key=byte,mask,value` per line (hex); a button is pressed when `(report[byte] & mask) == value`. Keys: `green red yellow blue orange start select strum_up strum_down`, plus `whammy=byte,rest,full` and `id=VID,PID`.
|
||||
|
||||
Tested: the generated layout for the 2563:0575 adapter and our guitar loads and works on a real PS4, including Select as star power. Other adapters and guitars are untested. Tilt does not work (see above), and the plugin still only supports Rock Band 4 version 02.21.
|
||||
|
||||
## Using a different adapter (manual)
|
||||
|
||||
1. Plug the adapter into a Windows PC, then run `tools\read_raw_report.ps1`:
|
||||
|
||||
@@ -129,10 +153,10 @@ You need a Windows PC with:
|
||||
- The [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/releases) release, extracted
|
||||
- The [GoldHEN Plugins SDK](https://github.com/GoldHEN/GoldHEN_Plugins_SDK), cloned
|
||||
|
||||
Then in PowerShell, from this folder:
|
||||
Then in PowerShell, from this folder (the bypass flag is needed if script execution is disabled):
|
||||
|
||||
```powershell
|
||||
.\build.ps1 -Toolchain "C:\path\to\OpenOrbis\PS4Toolchain" -GoldHenSdk "C:\path\to\GoldHEN_Plugins_SDK"
|
||||
powershell -ExecutionPolicy Bypass -File .\build.ps1 -Toolchain "C:\path\to\OpenOrbis\PS4Toolchain" -GoldHenSdk "C:\path\to\GoldHEN_Plugins_SDK"
|
||||
```
|
||||
|
||||
The result is `bin\OrbisInstrumentalizer.prx`.
|
||||
@@ -143,7 +167,7 @@ The result is `bin\OrbisInstrumentalizer.prx`.
|
||||
- **RB4DX stopped loading:** a new `plugins.ini` replaces GoldHEN's defaults. List every plugin you use under the heading, including `RB4DX-Plugin.prx`.
|
||||
- **Guitar not detected:** unplug the adapter, plug it into the PS4 **before** starting the game, and restart the game.
|
||||
- **A fret is the wrong color or a button does nothing:** see "Remapping".
|
||||
- **Menus can't be navigated with the guitar:** use a normal controller for menus. Start/Select work for Options only.
|
||||
- **Menus can't be navigated with the guitar:** use a normal controller for menus. Start works for Options only, and Select is Back.
|
||||
|
||||
## Credits and license
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
PS2 GUITAR FOR ROCK BAND 4 ON PS4 - READ ME FIRST
|
||||
=================================================
|
||||
|
||||
You need: a jailbroken PS4 with GoldHEN, Rock Band 4 version 02.21 (US),
|
||||
a PS2 guitar + USB adapter, and a Windows PC on the same network as the PS4.
|
||||
|
||||
Everything is in ONE program: GuitarTool.exe (the plugin is built into it).
|
||||
|
||||
STEP 1 - INSTALL (about 2 minutes)
|
||||
a) Right-click the zip and choose "Extract All".
|
||||
b) On the PS4: jailbreak it, then in the GoldHEN menu turn ON the FTP server.
|
||||
Find the PS4's IP address: Settings > Network > View Connection Status.
|
||||
c) On the PC, double-click GuitarTool.exe and type 1, press Enter.
|
||||
Type the PS4's IP address, press Enter. It does the rest.
|
||||
d) Plug the guitar adapter into the PS4, fully close Rock Band 4, start it again.
|
||||
|
||||
Try the guitar. If it works, you are finished.
|
||||
|
||||
STEP 2 - ONLY IF THE GAME SEES THE GUITAR BUT THE BUTTONS DO NOTHING (or are wrong)
|
||||
a) Unplug the adapter from the PS4 and plug it into the PC.
|
||||
b) Double-click GuitarTool.exe and type 2, press Enter.
|
||||
Follow the prompts: press Enter, then hold the named button when it says
|
||||
HOLD NOW. Strum UP and DOWN are the two directions of the strum bar.
|
||||
Hold the whammy bar all the way down for the last one.
|
||||
c) The tool saves guitar_map.ini (exactly that name) on your Desktop and offers
|
||||
to send it to the PS4 for you: type y and enter the PS4 IP.
|
||||
(Or run option 1 later; it sends the map file for you.)
|
||||
d) Plug the adapter back into the PS4, fully close Rock Band 4, start it again.
|
||||
|
||||
WHAT THE INSTALLER CHANGES ON THE PS4
|
||||
/data/GoldHEN/plugins/OrbisInstrumentalizer.prx (the plugin)
|
||||
/data/GoldHEN/plugins.ini (adds the plugin line under [CUSA02084];
|
||||
your old file is saved to your Desktop first)
|
||||
/data/GoldHEN/guitar_map.ini (only if you made a map in step 2)
|
||||
If you use RB4DX, its line in plugins.ini is kept. RB4DX itself is not included.
|
||||
|
||||
DOING IT BY HAND (no tool): copy the files in the PS4_Files folder to the same
|
||||
paths on the PS4 with any FTP program (port 2121) and add the plugin line to plugins.ini.
|
||||
|
||||
PROBLEMS
|
||||
- "Could not connect": the PS4 isn't jailbroken yet, FTP is off, or the IP is wrong.
|
||||
- Only Rock Band 4 version 02.21 works. Other versions do nothing.
|
||||
- Only ONE guitar adapter should be plugged in while testing.
|
||||
- To undo the map: delete /data/GoldHEN/guitar_map.ini from the PS4 and restart the game.
|
||||
- Help: Discord https://discord.gg/mGRCKG9BB (@CoronaSend). Attach guitar_capture.txt
|
||||
(made on your Desktop by step 2) if the buttons are wrong.
|
||||
|
||||
HONEST NOTES
|
||||
- Verified on one adapter and guitar (USB ID 2563:0575). Other adapters and
|
||||
guitars are untested; step 2 is how you teach it yours.
|
||||
- Tilt (star power by tilting) does not work. Press the Select button on the guitar to activate star power instead.
|
||||
- Windows may show a SmartScreen warning for an unsigned program: More info > Run anyway.
|
||||
@@ -49,7 +49,10 @@ foreach ($f in Get-ChildItem "$root\source\*.c") {
|
||||
ld.lld "$GoldHenSdk\build\crtprx.o" (Get-ChildItem "$build\*.o").FullName -o "$bin\OrbisInstrumentalizer.elf" -m elf_x86_64 -pie --script "$Toolchain\link.x" -e _init --eh-frame-hdr "-L$Toolchain\lib" "-L$GoldHenSdk" -lGoldHEN_Hook -lkernel -lSceLibcInternal -lSceSysmodule -lSceUsbd
|
||||
if ($LASTEXITCODE) { throw 'Link failed' }
|
||||
|
||||
cmd /c "`"$Toolchain\bin\windows\create-fself.exe`" -in=$bin\OrbisInstrumentalizer.elf -out=$bin\OrbisInstrumentalizer.oelf --lib=$bin\OrbisInstrumentalizer.prx --paid 0x3800000000000011"
|
||||
$env:OO_PS4_TOOLCHAIN = $Toolchain
|
||||
Push-Location $bin
|
||||
cmd /c "`"$Toolchain\bin\windows\create-fself.exe`" -in=OrbisInstrumentalizer.elf -out=OrbisInstrumentalizer.oelf --lib=OrbisInstrumentalizer.prx --paid 0x3800000000000011"
|
||||
Pop-Location
|
||||
if (-not (Test-Path "$bin\OrbisInstrumentalizer.prx")) { throw 'create-fself failed' }
|
||||
|
||||
Remove-Item $shim -Recurse -Force
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
; Sample only - add these lines to your existing plugins.ini, don't replace it.
|
||||
[settings]
|
||||
show_load_notification=true
|
||||
[CUSA02084]
|
||||
; /data/GoldHEN/plugins/RB4DX-Plugin.prx (uncomment if you use RB4DX)
|
||||
/data/GoldHEN/plugins/RB4DX-Plugin.prx
|
||||
/data/GoldHEN/plugins/OrbisInstrumentalizer.prx
|
||||
+1
-1
@@ -72,7 +72,7 @@ int32_t attr_module_hidden module_start(size_t argc, const void *args)
|
||||
for (int i = 0; i < num_UsbdHookTitleIDs; i++) {
|
||||
if (strcmp(procInfo.titleid, UsbdHookTitleIDs[i]) == 0) {
|
||||
final_printf("Applying sceUsbd hooks...\n");
|
||||
DoNotification("USB Guitar Plugin\nMade by CoronaSend");
|
||||
DoNotification("USB Guitar Plugin\nMade by CoronaSend\nDiscord: @CoronaSend");
|
||||
InitUsbdHooks();
|
||||
UsingUsbdHooks = true;
|
||||
}
|
||||
|
||||
+176
-8
@@ -27,9 +27,139 @@ typedef enum _OIRB4DeviceType {
|
||||
RB4_Type_PS2Guitar
|
||||
} OIRB4DeviceType;
|
||||
|
||||
// generic PS2-to-USB adapter (ShanWan style) with a PS2 guitar plugged in
|
||||
#define PS2_ADAPTER_VID 0x2563
|
||||
#define PS2_ADAPTER_PID 0x0575
|
||||
// generic PS2-to-USB adapters with a PS2 guitar plugged in
|
||||
// 2563:0575 (ShanWan style) is verified on hardware.
|
||||
// The others are generic PS2 adapters taken from SDL's controller list and are UNTESTED:
|
||||
// they use the same parser, so buttons may be wrong if their report layout differs.
|
||||
typedef struct _PS2AdapterId {
|
||||
uint16_t vid;
|
||||
uint16_t pid;
|
||||
} PS2AdapterId;
|
||||
|
||||
static const PS2AdapterId ps2_adapters[] = {
|
||||
{ 0x2563, 0x0575 }, // verified
|
||||
{ 0x0810, 0x0001 }, // untested
|
||||
{ 0x0810, 0x0003 }, // untested
|
||||
};
|
||||
|
||||
// Optional mapping file made by the PC capture tool (tools\guided_capture.ps1).
|
||||
// Each button is "byte,mask,value" in hex: pressed when (report[byte] & mask) == value.
|
||||
// With no file present, the built-in mapping for the verified adapter is used.
|
||||
#define MAP_PATH "/data/GoldHEN/guitar_map.ini"
|
||||
typedef struct _MapEntry {
|
||||
bool set;
|
||||
uint8_t byte, mask, val;
|
||||
} MapEntry;
|
||||
|
||||
static struct {
|
||||
bool loaded;
|
||||
bool has_id;
|
||||
uint16_t vid, pid;
|
||||
MapEntry green, red, yellow, blue, orange, start, select, up, down, tilt;
|
||||
bool whammy_set;
|
||||
uint8_t wbyte, wrest, wfull;
|
||||
int max_byte;
|
||||
} g_map;
|
||||
|
||||
static int HexVal(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// parses up to 3 comma-separated hex numbers, returns how many were read
|
||||
static int ParseHexList(const char *s, unsigned *out, int max) {
|
||||
int n = 0;
|
||||
while (*s && *s != '\n' && *s != '\r' && n < max) {
|
||||
unsigned v = 0;
|
||||
int digits = 0;
|
||||
while (HexVal(*s) >= 0) { v = (v << 4) | (unsigned)HexVal(*s); s++; digits++; }
|
||||
if (digits == 0) break;
|
||||
out[n++] = v;
|
||||
if (*s == ',') s++;
|
||||
else break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void SetEntry(MapEntry *e, const char *val) {
|
||||
unsigned v[3];
|
||||
if (ParseHexList(val, v, 3) == 3 && v[0] < 32) {
|
||||
e->set = true;
|
||||
e->byte = (uint8_t)v[0];
|
||||
e->mask = (uint8_t)v[1];
|
||||
e->val = (uint8_t)v[2];
|
||||
if ((int)v[0] > g_map.max_byte) g_map.max_byte = (int)v[0];
|
||||
}
|
||||
}
|
||||
|
||||
static void LoadMapFile(void) {
|
||||
char text[1024];
|
||||
int fd = sceKernelOpen(MAP_PATH, 0, 0);
|
||||
if (fd < 0)
|
||||
return;
|
||||
int len = (int)sceKernelRead(fd, text, sizeof(text) - 1);
|
||||
sceKernelClose(fd);
|
||||
if (len <= 0)
|
||||
return;
|
||||
text[len] = 0;
|
||||
|
||||
memset(&g_map, 0, sizeof(g_map));
|
||||
g_map.max_byte = -1;
|
||||
char *line = text;
|
||||
while (*line) {
|
||||
char *eol = line;
|
||||
while (*eol && *eol != '\n') eol++;
|
||||
char saved = *eol;
|
||||
*eol = 0;
|
||||
char *eq = line;
|
||||
while (*eq && *eq != '=') eq++;
|
||||
if (*eq == '=') {
|
||||
*eq = 0;
|
||||
const char *key = line, *val = eq + 1;
|
||||
unsigned v[3];
|
||||
if (strcmp(key, "id") == 0) {
|
||||
// "id=VID,PID"
|
||||
if (ParseHexList(val, v, 2) == 2) { g_map.has_id = true; g_map.vid = (uint16_t)v[0]; g_map.pid = (uint16_t)v[1]; }
|
||||
} else if (strcmp(key, "green") == 0) SetEntry(&g_map.green, val);
|
||||
else if (strcmp(key, "red") == 0) SetEntry(&g_map.red, val);
|
||||
else if (strcmp(key, "yellow") == 0) SetEntry(&g_map.yellow, val);
|
||||
else if (strcmp(key, "blue") == 0) SetEntry(&g_map.blue, val);
|
||||
else if (strcmp(key, "orange") == 0) SetEntry(&g_map.orange, val);
|
||||
else if (strcmp(key, "start") == 0) SetEntry(&g_map.start, val);
|
||||
else if (strcmp(key, "select") == 0) SetEntry(&g_map.select, val);
|
||||
else if (strcmp(key, "strum_up") == 0) SetEntry(&g_map.up, val);
|
||||
else if (strcmp(key, "strum_down") == 0) SetEntry(&g_map.down, val);
|
||||
else if (strcmp(key, "tilt") == 0) SetEntry(&g_map.tilt, val);
|
||||
else if (strcmp(key, "whammy") == 0) {
|
||||
// "whammy=byte,rest,full"
|
||||
if (ParseHexList(val, v, 3) == 3 && v[0] < 32 && v[1] != v[2]) {
|
||||
g_map.whammy_set = true;
|
||||
g_map.wbyte = (uint8_t)v[0];
|
||||
g_map.wrest = (uint8_t)v[1];
|
||||
g_map.wfull = (uint8_t)v[2];
|
||||
if ((int)v[0] > g_map.max_byte) g_map.max_byte = (int)v[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
*eol = saved;
|
||||
if (!saved) break;
|
||||
line = eol + 1;
|
||||
}
|
||||
g_map.loaded = g_map.green.set && g_map.red.set && g_map.yellow.set && g_map.blue.set && g_map.orange.set;
|
||||
klog("[OrbisInstrumentalizer] Mapping file %s (id %04x:%04x)\n", g_map.loaded ? "loaded" : "incomplete, ignored", g_map.vid, g_map.pid);
|
||||
}
|
||||
|
||||
static bool IsPS2AdapterId(uint16_t vid, uint16_t pid) {
|
||||
if (g_map.loaded && g_map.has_id && g_map.vid == vid && g_map.pid == pid)
|
||||
return true;
|
||||
for (size_t i = 0; i < sizeof(ps2_adapters) / sizeof(ps2_adapters[0]); i++) {
|
||||
if (ps2_adapters[i].vid == vid && ps2_adapters[i].pid == pid)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
typedef struct _OIRB4OpenDevice {
|
||||
bool is_open;
|
||||
@@ -141,6 +271,8 @@ static OIRB4DeviceType IdentifyDevice(libusb_device *device) {
|
||||
}
|
||||
|
||||
#define BIT(i) (1 << i)
|
||||
// accel_x value sent while the guitar is tilted up (0x00 is level; matches the original plugin tilt mapping)
|
||||
#define TILT_UP 0x7F
|
||||
// TODO: this seems bad i dont even know if it do drum
|
||||
typedef struct _ps3_rb_guitar_report {
|
||||
uint16_t buttons;
|
||||
@@ -197,8 +329,43 @@ void ParseXInputCallback(struct libusb_transfer *transfer) {
|
||||
// [1] bit0 select, bit1 start
|
||||
// [2] hat: 6 = idle, 5 = strum down, 7 = strum up
|
||||
// [4] whammy axis: 0x7F at rest, 0x00 fully pressed
|
||||
static bool MapPressed(const MapEntry *e, const uint8_t *raw) {
|
||||
return e->set && (raw[e->byte] & e->mask) == e->val;
|
||||
}
|
||||
|
||||
static void ParseMappedPS2(struct libusb_transfer *transfer) {
|
||||
uint8_t raw[32] = { 0 };
|
||||
int n = transfer->actual_length < (int)sizeof(raw) ? transfer->actual_length : (int)sizeof(raw);
|
||||
memcpy(raw, transfer->buffer, n);
|
||||
ps3_rb_guitar_report parsed_report = { 0 };
|
||||
|
||||
if (MapPressed(&g_map.green, raw)) parsed_report.buttons |= BIT(1);
|
||||
if (MapPressed(&g_map.red, raw)) parsed_report.buttons |= BIT(0);
|
||||
if (MapPressed(&g_map.yellow, raw)) parsed_report.buttons |= BIT(4);
|
||||
if (MapPressed(&g_map.blue, raw)) parsed_report.buttons |= BIT(2);
|
||||
if (MapPressed(&g_map.orange, raw)) parsed_report.buttons |= BIT(3);
|
||||
if (MapPressed(&g_map.start, raw)) parsed_report.buttons |= BIT(9);
|
||||
if (MapPressed(&g_map.select, raw)) parsed_report.buttons |= BIT(8); // back / star power
|
||||
|
||||
parsed_report.hat = 0x08;
|
||||
if (MapPressed(&g_map.up, raw)) parsed_report.hat = 0x00;
|
||||
else if (MapPressed(&g_map.down, raw)) parsed_report.hat = 0x04;
|
||||
|
||||
if (g_map.whammy_set) {
|
||||
int span = (int)g_map.wfull - (int)g_map.wrest;
|
||||
int v = ((int)raw[g_map.wbyte] - (int)g_map.wrest) * 255 / span;
|
||||
parsed_report.whammy = (uint8_t)(v < 0 ? 0 : (v > 255 ? 255 : v));
|
||||
}
|
||||
parsed_report.accel_x = MapPressed(&g_map.tilt, raw) ? TILT_UP : 0x00;
|
||||
|
||||
size_t len = transfer->length < sizeof(parsed_report) ? transfer->length : sizeof(parsed_report);
|
||||
memcpy(transfer->buffer, &parsed_report, len);
|
||||
}
|
||||
|
||||
void ParsePS2Callback(struct libusb_transfer *transfer) {
|
||||
if (transfer->status == LIBUSB_TRANSFER_COMPLETED && transfer->actual_length >= 7) {
|
||||
if (transfer->status == LIBUSB_TRANSFER_COMPLETED && g_map.loaded && transfer->actual_length > g_map.max_byte) {
|
||||
ParseMappedPS2(transfer);
|
||||
} else if (transfer->status == LIBUSB_TRANSFER_COMPLETED && !g_map.loaded && transfer->actual_length >= 7) {
|
||||
uint8_t raw[8];
|
||||
memcpy(raw, transfer->buffer, sizeof(raw));
|
||||
ps3_rb_guitar_report parsed_report = { 0 };
|
||||
@@ -209,15 +376,15 @@ void ParsePS2Callback(struct libusb_transfer *transfer) {
|
||||
if ((raw[0] & 0x01) != 0) parsed_report.buttons |= BIT(4); // yellow
|
||||
if ((raw[0] & 0x04) != 0) parsed_report.buttons |= BIT(2); // blue
|
||||
if ((raw[0] & 0x08) != 0) parsed_report.buttons |= BIT(3); // orange
|
||||
if ((raw[1] & 0x03) != 0) parsed_report.buttons |= BIT(9); // start and select both act as start/options
|
||||
if ((raw[1] & 0x02) != 0) parsed_report.buttons |= BIT(9); // start
|
||||
if ((raw[1] & 0x01) != 0) parsed_report.buttons |= BIT(8); // select = back / star power
|
||||
parsed_report.accel_x = (raw[0] & 0x40) ? TILT_UP : 0x00; // tilt switch
|
||||
|
||||
parsed_report.hat = 0x08;
|
||||
if (raw[2] == 7) parsed_report.hat = 0x00; // strum up
|
||||
if (raw[2] == 5) parsed_report.hat = 0x04; // strum down
|
||||
|
||||
parsed_report.whammy = (raw[4] >= 0x7F) ? 0x00 : (uint8_t)((0x7F - raw[4]) * 2);
|
||||
parsed_report.accel_x = 0x80;
|
||||
|
||||
size_t len = transfer->length < sizeof(parsed_report) ? transfer->length : sizeof(parsed_report);
|
||||
memcpy(transfer->buffer, &parsed_report, len);
|
||||
}
|
||||
@@ -302,7 +469,7 @@ int TsceUsbdGetDeviceDescriptor_hook(libusb_device *device, struct libusb_device
|
||||
}
|
||||
}
|
||||
|
||||
if (desc->idVendor == PS2_ADAPTER_VID && desc->idProduct == PS2_ADAPTER_PID) {
|
||||
if (IsPS2AdapterId(desc->idVendor, desc->idProduct)) {
|
||||
RegisterPS2Device(device);
|
||||
desc->idVendor = 0x12BA; // licensed by SCEA
|
||||
desc->idProduct = 0x0200; // PS3 Rock Band guitar
|
||||
@@ -350,6 +517,7 @@ void TsceUsbdFillInterruptTransfer_hook(struct libusb_transfer *transfer, libusb
|
||||
void InitUsbdHooks() {
|
||||
// make sure we have the USBD module loaded into memory
|
||||
sceSysmoduleLoadModule(ORBIS_SYSMODULE_USBD);
|
||||
LoadMapFile();
|
||||
struct proc_info procInfo;
|
||||
sys_sdk_proc_info(&procInfo);
|
||||
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
// GuitarTool.exe - installs the PS2 guitar plugin for Rock Band 4 on a jailbroken PS4 and
|
||||
// builds a guitar_map.ini for any guitar/adapter plugged into this Windows PC.
|
||||
// Built with the C# compiler that ships with Windows (no extra installs needed to run).
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
static class Native
|
||||
{
|
||||
[DllImport("hid.dll")] public static extern void HidD_GetHidGuid(out Guid g);
|
||||
[DllImport("hid.dll", SetLastError = true)] public static extern bool HidD_GetAttributes(SafeFileHandle h, ref HIDD_ATTRIBUTES a);
|
||||
[DllImport("hid.dll", SetLastError = true)] public static extern bool HidD_GetProductString(SafeFileHandle h, byte[] buf, int len);
|
||||
[DllImport("hid.dll", SetLastError = true)] public static extern bool HidD_GetPreparsedData(SafeFileHandle h, out IntPtr p);
|
||||
[DllImport("hid.dll")] public static extern bool HidD_FreePreparsedData(IntPtr p);
|
||||
[DllImport("hid.dll")] public static extern int HidP_GetCaps(IntPtr p, out HIDP_CAPS c);
|
||||
[DllImport("setupapi.dll", SetLastError = true)] public static extern IntPtr SetupDiGetClassDevs(ref Guid g, IntPtr e, IntPtr h, int flags);
|
||||
[DllImport("setupapi.dll", SetLastError = true)] public static extern bool SetupDiEnumDeviceInterfaces(IntPtr set, IntPtr info, ref Guid g, int idx, ref SP_DEVICE_INTERFACE_DATA d);
|
||||
[DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern bool SetupDiGetDeviceInterfaceDetail(IntPtr set, ref SP_DEVICE_INTERFACE_DATA d, IntPtr detail, int size, out int req, IntPtr info);
|
||||
[DllImport("setupapi.dll")] public static extern bool SetupDiDestroyDeviceInfoList(IntPtr set);
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern SafeFileHandle CreateFile(string f, uint access, uint share, IntPtr sec, uint disp, uint flags, IntPtr tmpl);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] public struct HIDD_ATTRIBUTES { public int Size; public ushort VendorID; public ushort ProductID; public ushort VersionNumber; }
|
||||
[StructLayout(LayoutKind.Sequential)] public struct SP_DEVICE_INTERFACE_DATA { public int cbSize; public Guid InterfaceClassGuid; public int Flags; public IntPtr Reserved; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct HIDP_CAPS
|
||||
{
|
||||
public ushort Usage, UsagePage, InputReportByteLength, OutputReportByteLength, FeatureReportByteLength;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] public ushort[] Reserved;
|
||||
public ushort NumberLinkCollectionNodes, NumberInputButtonCaps, NumberInputValueCaps, NumberInputDataIndices,
|
||||
NumberOutputButtonCaps, NumberOutputValueCaps, NumberOutputDataIndices,
|
||||
NumberFeatureButtonCaps, NumberFeatureValueCaps, NumberFeatureDataIndices;
|
||||
}
|
||||
}
|
||||
|
||||
class HidDev
|
||||
{
|
||||
public string Path; public ushort Vid, Pid, Page, Usage; public int InLen; public string Name;
|
||||
public string Id { get { return Vid.ToString("X4") + ":" + Pid.ToString("X4"); } }
|
||||
|
||||
public static List<HidDev> Enumerate()
|
||||
{
|
||||
var list = new List<HidDev>();
|
||||
Guid g; Native.HidD_GetHidGuid(out g);
|
||||
IntPtr set = Native.SetupDiGetClassDevs(ref g, IntPtr.Zero, IntPtr.Zero, 0x12);
|
||||
if (set == new IntPtr(-1)) return list;
|
||||
try
|
||||
{
|
||||
for (int i = 0; ; i++)
|
||||
{
|
||||
var d = new Native.SP_DEVICE_INTERFACE_DATA();
|
||||
d.cbSize = Marshal.SizeOf(typeof(Native.SP_DEVICE_INTERFACE_DATA));
|
||||
if (!Native.SetupDiEnumDeviceInterfaces(set, IntPtr.Zero, ref g, i, ref d)) break;
|
||||
int req;
|
||||
Native.SetupDiGetDeviceInterfaceDetail(set, ref d, IntPtr.Zero, 0, out req, IntPtr.Zero);
|
||||
IntPtr buf = Marshal.AllocHGlobal(req);
|
||||
try
|
||||
{
|
||||
Marshal.WriteInt32(buf, IntPtr.Size == 8 ? 8 : 6);
|
||||
if (!Native.SetupDiGetDeviceInterfaceDetail(set, ref d, buf, req, out req, IntPtr.Zero)) continue;
|
||||
string path = Marshal.PtrToStringAuto(new IntPtr(buf.ToInt64() + 4));
|
||||
var dev = Probe(path);
|
||||
if (dev != null) list.Add(dev);
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(buf); }
|
||||
}
|
||||
}
|
||||
finally { Native.SetupDiDestroyDeviceInfoList(set); }
|
||||
return list;
|
||||
}
|
||||
|
||||
static HidDev Probe(string path)
|
||||
{
|
||||
using (var h = Native.CreateFile(path, 0, 3, IntPtr.Zero, 3, 0, IntPtr.Zero))
|
||||
{
|
||||
if (h.IsInvalid) return null;
|
||||
var a = new Native.HIDD_ATTRIBUTES(); a.Size = Marshal.SizeOf(typeof(Native.HIDD_ATTRIBUTES));
|
||||
if (!Native.HidD_GetAttributes(h, ref a)) return null;
|
||||
IntPtr pp; if (!Native.HidD_GetPreparsedData(h, out pp)) return null;
|
||||
Native.HIDP_CAPS caps;
|
||||
int st = Native.HidP_GetCaps(pp, out caps);
|
||||
Native.HidD_FreePreparsedData(pp);
|
||||
if (st != 0x110000 || caps.InputReportByteLength == 0) return null;
|
||||
bool skip = (caps.UsagePage == 1 && (caps.Usage == 2 || caps.Usage == 6 || caps.Usage == 0x80)) || caps.UsagePage == 0x0C;
|
||||
if (skip) return null;
|
||||
var nb = new byte[256]; string name = "";
|
||||
if (Native.HidD_GetProductString(h, nb, nb.Length)) name = Encoding.Unicode.GetString(nb).TrimEnd('\0');
|
||||
return new HidDev { Path = path, Vid = a.VendorID, Pid = a.ProductID, Page = caps.UsagePage, Usage = caps.Usage, InLen = caps.InputReportByteLength, Name = name.Length > 0 ? name : "(unnamed device)" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Reader : IDisposable
|
||||
{
|
||||
FileStream fs; byte[] buf; Task<int> pending;
|
||||
public Reader(HidDev d)
|
||||
{
|
||||
var h = Native.CreateFile(d.Path, 0x80000000, 3, IntPtr.Zero, 3, 0x40000000, IntPtr.Zero);
|
||||
if (h.IsInvalid) throw new IOException("Could not open the device for reading.");
|
||||
fs = new FileStream(h, FileAccess.Read, 64, true);
|
||||
buf = new byte[Math.Max(64, d.InLen)];
|
||||
}
|
||||
// Returns the report without the leading report-ID byte (what the PS4 plugin sees), or null on timeout.
|
||||
public byte[] Next(int ms)
|
||||
{
|
||||
if (pending == null) pending = fs.ReadAsync(buf, 0, buf.Length);
|
||||
try { if (!pending.Wait(ms)) return null; }
|
||||
catch (AggregateException) { pending = null; throw new IOException("The device stopped responding. Unplug it, plug it back in, and try again."); }
|
||||
int n = pending.Result; pending = null;
|
||||
if (n < 2) return new byte[0];
|
||||
var r = new byte[n - 1]; Array.Copy(buf, 1, r, 0, n - 1); return r;
|
||||
}
|
||||
public void Dispose() { if (fs != null) fs.Dispose(); }
|
||||
}
|
||||
|
||||
class Entry { public int B, M, V; public Entry(int b, int m, int v) { B = b; M = m; V = v; } public override string ToString() { return B.ToString("x") + "," + M.ToString("x") + "," + V.ToString("x"); } }
|
||||
|
||||
static class Mapper
|
||||
{
|
||||
public static int Pop(int v) { int n = 0; while (v != 0) { n += v & 1; v >>= 1; } return n; }
|
||||
|
||||
// Pressed when (report[byte] & mask) == val. Picks the changed byte with the fewest changed bits, so
|
||||
// a single button bit wins over an analog byte that merely jitters.
|
||||
public static Entry Btn(byte[] idle, byte[] p)
|
||||
{
|
||||
Entry best = null; int bp = 99; int n = Math.Min(idle.Length, p.Length);
|
||||
for (int i = 0; i < n; i++)
|
||||
if (idle[i] != p[i]) { int m = idle[i] ^ p[i]; int pc = Pop(m); if (pc < bp) { bp = pc; best = new Entry(i, m, p[i] & m); } }
|
||||
return best;
|
||||
}
|
||||
|
||||
// Strum is often a hat value (several states in one nibble), so compare the whole nibble.
|
||||
public static bool Strum(byte[] idle, byte[] up, byte[] down, out Entry u, out Entry d)
|
||||
{
|
||||
int n = Math.Min(idle.Length, Math.Min(up.Length, down.Length));
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
if (idle[i] == up[i] || idle[i] == down[i] || up[i] == down[i]) continue;
|
||||
int ch = (idle[i] ^ up[i]) | (idle[i] ^ down[i]);
|
||||
int mask = 0;
|
||||
if ((ch & 0xF0) == 0) mask = 0x0F; else if ((ch & 0x0F) == 0) mask = 0xF0;
|
||||
if (mask != 0) { u = new Entry(i, mask, up[i] & mask); d = new Entry(i, mask, down[i] & mask); return true; }
|
||||
}
|
||||
u = Btn(idle, up); d = Btn(idle, down);
|
||||
return u != null && d != null;
|
||||
}
|
||||
|
||||
public static bool Whammy(byte[] idle, byte[] p, out int b, out int rest, out int full)
|
||||
{
|
||||
int n = Math.Min(idle.Length, p.Length), best = -1, diff = 0;
|
||||
for (int i = 0; i < n; i++) { int dd = Math.Abs(idle[i] - p[i]); if (dd > diff) { diff = dd; best = i; } }
|
||||
b = best; rest = best >= 0 ? idle[best] : 0; full = best >= 0 ? p[best] : 0;
|
||||
return best >= 0 && diff >= 16;
|
||||
}
|
||||
|
||||
public static List<string> Build(string vid, string pid, Dictionary<string, byte[]> c, List<string> problems)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
lines.Add("id=" + vid.ToLower() + "," + pid.ToLower());
|
||||
var idle = c["idle"]; var seen = new Dictionary<string, string>();
|
||||
foreach (var k in new[] { "green", "red", "yellow", "blue", "orange", "start", "select" })
|
||||
{
|
||||
var e = Btn(idle, c[k]);
|
||||
if (e == null) { problems.Add(k.ToUpper() + " - nothing changed while you held it"); continue; }
|
||||
string sig = e.ToString();
|
||||
if (seen.ContainsKey(sig)) { problems.Add(k.ToUpper() + " gave the same signal as " + seen[sig].ToUpper() + " - was the right button held?"); continue; }
|
||||
seen[sig] = k; lines.Add(k + "=" + sig);
|
||||
}
|
||||
Entry u, d;
|
||||
if (Strum(idle, c["strum_up"], c["strum_down"], out u, out d)) { lines.Add("strum_up=" + u); lines.Add("strum_down=" + d); }
|
||||
else problems.Add("STRUM - could not tell up from down");
|
||||
int wb, wr, wf;
|
||||
if (Whammy(idle, c["whammy"], out wb, out wr, out wf)) lines.Add("whammy=" + wb.ToString("x") + "," + wr.ToString("x") + "," + wf.ToString("x"));
|
||||
else problems.Add("WHAMMY - no analog change found (this guitar may have no whammy output)");
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
||||
static class Ftp
|
||||
{
|
||||
static FtpWebRequest Req(string ip, string path, string method)
|
||||
{
|
||||
var r = (FtpWebRequest)WebRequest.Create("ftp://" + ip + ":2121" + path);
|
||||
r.Method = method; r.Credentials = new NetworkCredential("anonymous", "guest@");
|
||||
r.UsePassive = true; r.UseBinary = true; r.KeepAlive = false; r.Timeout = 10000; r.ReadWriteTimeout = 15000;
|
||||
return r;
|
||||
}
|
||||
public static string List(string ip, string path)
|
||||
{
|
||||
using (var resp = (FtpWebResponse)Req(ip, path, WebRequestMethods.Ftp.ListDirectoryDetails).GetResponse())
|
||||
using (var sr = new StreamReader(resp.GetResponseStream())) return sr.ReadToEnd();
|
||||
}
|
||||
public static void Put(string ip, string path, byte[] data)
|
||||
{
|
||||
var r = Req(ip, path, WebRequestMethods.Ftp.UploadFile);
|
||||
using (var s = r.GetRequestStream()) s.Write(data, 0, data.Length);
|
||||
r.GetResponse().Close();
|
||||
}
|
||||
public static byte[] Get(string ip, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var resp = (FtpWebResponse)Req(ip, path, WebRequestMethods.Ftp.DownloadFile).GetResponse())
|
||||
using (var s = resp.GetResponseStream()) using (var ms = new MemoryStream()) { s.CopyTo(ms); return ms.ToArray(); }
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
var fr = ex.Response as FtpWebResponse;
|
||||
if (fr != null && fr.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable) return null;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Program
|
||||
{
|
||||
const string Section = "[CUSA02084]";
|
||||
const string PluginLine = "/data/GoldHEN/plugins/OrbisInstrumentalizer.prx";
|
||||
const string Rb4dxLine = "/data/GoldHEN/plugins/RB4DX-Plugin.prx";
|
||||
static string lastIp = "";
|
||||
static readonly UTF8Encoding Utf8 = new UTF8Encoding(false);
|
||||
|
||||
static string Desktop { get { return Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); } }
|
||||
static string Ask(string q) { Console.Write(q); string s = Console.ReadLine(); return s == null ? "" : s.Trim(); }
|
||||
static void Say(string s) { Console.WriteLine(s); }
|
||||
|
||||
// Adds missing lines under [CUSA02084] and leaves everything else as it was.
|
||||
public static string UpdateIni(string text, string[] need, out bool changed)
|
||||
{
|
||||
var L = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(text)) text = "[settings]\nshow_load_notification=true";
|
||||
foreach (var x in text.Replace("\r\n", "\n").Split('\n')) L.Add(x);
|
||||
while (L.Count > 0 && L[L.Count - 1].Trim() == "") L.RemoveAt(L.Count - 1);
|
||||
int idx = -1;
|
||||
for (int i = 0; i < L.Count; i++) if (L[i].Trim() == Section) { idx = i; break; }
|
||||
if (idx < 0) { L.Add(Section); idx = L.Count - 1; }
|
||||
int end = idx + 1;
|
||||
while (end < L.Count && !L[end].Trim().StartsWith("[")) end++;
|
||||
changed = false;
|
||||
foreach (var n in need)
|
||||
{
|
||||
bool has = false;
|
||||
for (int i = idx + 1; i < end; i++) { string t = L[i].Trim(); if (t == n || t.StartsWith(n + "=")) has = true; }
|
||||
if (!has) { L.Insert(end, n); end++; changed = true; }
|
||||
}
|
||||
return string.Join("\n", L.ToArray()) + "\n";
|
||||
}
|
||||
|
||||
static byte[] PluginBytes()
|
||||
{
|
||||
using (var s = Assembly.GetExecutingAssembly().GetManifestResourceStream("plugin.prx"))
|
||||
{
|
||||
if (s == null) throw new Exception("The plugin is missing from this program.");
|
||||
using (var ms = new MemoryStream()) { s.CopyTo(ms); return ms.ToArray(); }
|
||||
}
|
||||
}
|
||||
|
||||
static string GetIp(string given)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(given)) { lastIp = given; return given; }
|
||||
Say("");
|
||||
Say("Do these on the PS4 first:");
|
||||
Say(" 1. Turn the PS4 on and run the jailbreak so GoldHEN loads.");
|
||||
Say(" 2. Open the GoldHEN menu and turn ON the FTP server.");
|
||||
Say(" 3. Find the PS4's IP address: Settings > Network > View Connection Status.");
|
||||
Say("The PC and the PS4 must be on the same network (same router).");
|
||||
Say("");
|
||||
string def = lastIp.Length > 0 ? " [" + lastIp + "]" : "";
|
||||
string ip = Ask("Type the PS4 IP address (example 192.168.1.27)" + def + " and press Enter: ");
|
||||
if (ip.Length == 0) ip = lastIp;
|
||||
lastIp = ip;
|
||||
return ip;
|
||||
}
|
||||
|
||||
static bool Connect(string ip)
|
||||
{
|
||||
try { Ftp.List(ip, "/data/GoldHEN/"); return true; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Say("");
|
||||
Say("COULD NOT CONNECT to " + ip + " on port 2121.");
|
||||
Say("Check: the PS4 is on and jailbroken, the GoldHEN FTP server is ON, and the IP is right.");
|
||||
Say("(" + ex.Message + ")");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void Install(string ipArg)
|
||||
{
|
||||
Say("");
|
||||
Say("--- INSTALL THE PLUGIN ON THE PS4 ---");
|
||||
string ip = GetIp(ipArg);
|
||||
if (ip.Length == 0) { Say("No IP address entered."); return; }
|
||||
Say("[1/4] Connecting to the PS4...");
|
||||
if (!Connect(ip)) return;
|
||||
byte[] prx = PluginBytes();
|
||||
Say("[2/4] Sending the plugin (" + prx.Length + " bytes)...");
|
||||
Ftp.Put(ip, "/data/GoldHEN/plugins/OrbisInstrumentalizer.prx", prx);
|
||||
Say("[3/4] Checking plugins.ini...");
|
||||
|
||||
string plugins = Ftp.List(ip, "/data/GoldHEN/plugins/");
|
||||
var need = new List<string>();
|
||||
if (plugins.Contains("RB4DX-Plugin.prx")) need.Add(Rb4dxLine);
|
||||
need.Add(PluginLine);
|
||||
byte[] oldB = Ftp.Get(ip, "/data/GoldHEN/plugins.ini");
|
||||
string old = oldB == null ? "" : Utf8.GetString(oldB);
|
||||
bool changed; string neu = UpdateIni(old, need.ToArray(), out changed);
|
||||
if (changed)
|
||||
{
|
||||
if (old.Length > 0)
|
||||
{
|
||||
string bk = Path.Combine(Desktop, "plugins_backup_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".ini");
|
||||
File.WriteAllText(bk, old, Utf8);
|
||||
Say("Your old plugins.ini was saved to: " + bk);
|
||||
}
|
||||
Ftp.Put(ip, "/data/GoldHEN/plugins.ini", Utf8.GetBytes(neu));
|
||||
Say("plugins.ini updated.");
|
||||
}
|
||||
else Say("plugins.ini was already correct.");
|
||||
|
||||
Say("[4/4] Checking for a map file...");
|
||||
string map = Path.Combine(Desktop, "guitar_map.ini");
|
||||
bool sentMap = false;
|
||||
if (File.Exists(map))
|
||||
{
|
||||
Ftp.Put(ip, "/data/GoldHEN/guitar_map.ini", File.ReadAllBytes(map));
|
||||
Say("Found guitar_map.ini on your Desktop and sent it to the PS4.");
|
||||
sentMap = true;
|
||||
}
|
||||
else Say("No guitar_map.ini on your Desktop (that is fine).");
|
||||
Say("");
|
||||
Say("INSTALL COMPLETE. What to do now:");
|
||||
Say(" 1. Plug the guitar adapter into the PS4.");
|
||||
Say(" 2. FULLY CLOSE Rock Band 4 (PS button > Close Application), then start it again.");
|
||||
Say(" 3. Try the guitar in the menus.");
|
||||
if (!sentMap) Say(" 4. If the game sees the guitar but the buttons do nothing, come back and choose option 2.");
|
||||
}
|
||||
|
||||
static Dictionary<string, byte[]> cap;
|
||||
|
||||
static byte[] Capture(Reader r, double seconds, string label)
|
||||
{
|
||||
var counts = new Dictionary<string, int>(); var keep = new Dictionary<string, byte[]>();
|
||||
var sw = Stopwatch.StartNew();
|
||||
while (sw.Elapsed.TotalSeconds < seconds)
|
||||
{
|
||||
var b = r.Next(300);
|
||||
if (b == null || b.Length == 0) continue;
|
||||
string k = BitConverter.ToString(b);
|
||||
int c; counts.TryGetValue(k, out c); counts[k] = c + 1; keep[k] = b;
|
||||
}
|
||||
string best = null; int bc = 0;
|
||||
foreach (var kv in counts) if (kv.Value > bc) { bc = kv.Value; best = kv.Key; }
|
||||
return best == null ? null : keep[best];
|
||||
}
|
||||
|
||||
static void Drain(Reader r, int ms) { var sw = Stopwatch.StartNew(); while (sw.ElapsedMilliseconds < ms) r.Next(50); }
|
||||
|
||||
static void MakeMap()
|
||||
{
|
||||
Say("");
|
||||
Say("--- MAKE A MAP FILE FOR MY GUITAR ---");
|
||||
Say("This teaches the plugin which button is which on YOUR guitar. It takes about 2 minutes.");
|
||||
Say("");
|
||||
Say("Before you start:");
|
||||
Say(" 1. Unplug the guitar adapter from the PS4 and plug it into THIS PC.");
|
||||
Say(" 2. Have the guitar in your hands.");
|
||||
Say(" 3. You will be asked to hold 10 things, one at a time, for about 2 seconds each:");
|
||||
Say(" green, red, yellow, blue, orange, strum up, strum down, start, select, whammy.");
|
||||
Say(" Only hold the ONE thing it asks for. Let go when it says it got it.");
|
||||
Ask("Press Enter when the adapter is plugged into this PC. ");
|
||||
var devs = HidDev.Enumerate();
|
||||
if (devs.Count == 0) { Say("No game controller found. Plug the guitar adapter into THIS PC (not the PS4) and try again."); return; }
|
||||
HidDev dev = null;
|
||||
foreach (var d in devs) if ((d.Vid == 0x2563 && d.Pid == 0x0575) || d.Vid == 0x0810) { dev = d; break; }
|
||||
if (dev == null)
|
||||
{
|
||||
Say("Devices found:");
|
||||
for (int i = 0; i < devs.Count; i++) Say(string.Format(" [{0}] {1} ({2})", i + 1, devs[i].Name, devs[i].Id));
|
||||
int pick = 1;
|
||||
if (devs.Count > 1) { if (!int.TryParse(Ask("Type the number of your guitar adapter and press Enter: "), out pick) || pick < 1 || pick > devs.Count) { Say("Not a valid number."); return; } }
|
||||
dev = devs[pick - 1];
|
||||
}
|
||||
Say("Using: " + dev.Name + " (USB ID " + dev.Id + ")");
|
||||
var raw = new List<string>();
|
||||
cap = new Dictionary<string, byte[]>();
|
||||
using (var r = new Reader(dev))
|
||||
{
|
||||
Say("");
|
||||
Ask("STEP 0: Put the guitar DOWN on a table, touch NOTHING, then press Enter. ");
|
||||
Say(" recording the resting state...");
|
||||
Drain(r, 300);
|
||||
cap["idle"] = Capture(r, 2.5, "idle");
|
||||
if (cap["idle"] == null) { Say("No data came from the adapter. Try another USB port, or press a button while it is plugged in."); return; }
|
||||
raw.Add("IDLE: " + BitConverter.ToString(cap["idle"]));
|
||||
var steps = new[] {
|
||||
new[] { "green", "GREEN fret" }, new[] { "red", "RED fret" }, new[] { "yellow", "YELLOW fret" },
|
||||
new[] { "blue", "BLUE fret" }, new[] { "orange", "ORANGE fret" },
|
||||
new[] { "strum_up", "STRUM UP (push the strum bar up)" }, new[] { "strum_down", "STRUM DOWN (push the strum bar down)" },
|
||||
new[] { "start", "START" }, new[] { "select", "SELECT" }, new[] { "whammy", "WHAMMY bar pushed ALL the way down" } };
|
||||
int stepNo = 0;
|
||||
foreach (var s in steps)
|
||||
{
|
||||
stepNo++;
|
||||
Say("");
|
||||
Say(string.Format("STEP {0} of {1}: {2}", stepNo, steps.Length, s[1]));
|
||||
Ask(" Get your hand ready, press Enter, then HOLD it when it says HOLD NOW. ");
|
||||
Say(" get ready...");
|
||||
Drain(r, 900);
|
||||
Say(" >>> HOLD NOW! (keep holding about 2 seconds) <<<");
|
||||
var b = Capture(r, 2.5, s[0]);
|
||||
Say(" OK, let go.");
|
||||
if (b == null) b = cap["idle"];
|
||||
cap[s[0]] = b;
|
||||
raw.Add(s[0] + ": " + BitConverter.ToString(b));
|
||||
}
|
||||
}
|
||||
var problems = new List<string>();
|
||||
var lines = Mapper.Build(dev.Vid.ToString("X4"), dev.Pid.ToString("X4"), cap, problems);
|
||||
string safeName = dev.Name.Replace("=", "-").Replace("\r", " ").Replace("\n", " ");
|
||||
if (safeName.Length > 40) safeName = safeName.Substring(0, 40);
|
||||
var mapText = new List<string>();
|
||||
mapText.Add("# Guitar map made by GuitarTool for " + safeName + " on " + DateTime.Now.ToString("yyyy-MM-dd"));
|
||||
mapText.Add("# Put this file at /data/GoldHEN/guitar_map.ini on the PS4. Each line: button, byte, mask, value (hex).");
|
||||
mapText.AddRange(lines);
|
||||
string ini = Path.Combine(Desktop, "guitar_map.ini");
|
||||
File.WriteAllText(ini, string.Join("\r\n", mapText.ToArray()).Replace("\r\n", "\n") + "\n", Utf8);
|
||||
var report = new List<string>();
|
||||
report.Add("GuitarTool capture report - " + DateTime.Now.ToString("yyyy-MM-dd HH:mm"));
|
||||
report.Add("Device: " + dev.Name + " USB ID: " + dev.Id + " report length: " + (dev.InLen - 1) + " bytes");
|
||||
report.Add("");
|
||||
report.Add("Raw reports (one per button, hex bytes):");
|
||||
report.AddRange(raw);
|
||||
report.Add("");
|
||||
report.Add("Map that was generated:");
|
||||
report.AddRange(lines);
|
||||
if (problems.Count > 0) { report.Add(""); report.Add("Not detected:"); foreach (var p in problems) report.Add(" " + p); }
|
||||
File.WriteAllText(Path.Combine(Desktop, "guitar_capture.txt"), string.Join("\n", report.ToArray()) + "\n", Utf8);
|
||||
Say("");
|
||||
Say("Saved on your Desktop:");
|
||||
Say(" guitar_map.ini - the map file (exactly the name the PS4 needs)");
|
||||
Say(" guitar_capture.txt - the raw data (keep it, in case something needs fixing)");
|
||||
Say("");
|
||||
Say("The map that was made:");
|
||||
foreach (var l in lines) Say(" " + l);
|
||||
if (problems.Count > 0)
|
||||
{
|
||||
Say("");
|
||||
Say("PROBLEMS - these were not detected, so the map is incomplete:");
|
||||
foreach (var p in problems) Say(" - " + p);
|
||||
Say("Choose option 2 again and hold each of those the WHOLE time it says HOLD NOW.");
|
||||
Say("If it keeps failing, send guitar_capture.txt to the author (Discord @CoronaSend).");
|
||||
return;
|
||||
}
|
||||
Say("");
|
||||
Say("Map complete. Now it has to get onto the PS4:");
|
||||
string yn = Ask("Send guitar_map.ini to the PS4 now? Type y or n and press Enter: ");
|
||||
if (yn.ToLower().StartsWith("y"))
|
||||
{
|
||||
string ip = GetIp(null);
|
||||
if (ip.Length > 0 && Connect(ip))
|
||||
{
|
||||
Ftp.Put(ip, "/data/GoldHEN/guitar_map.ini", File.ReadAllBytes(ini));
|
||||
Say("Sent to /data/GoldHEN/guitar_map.ini on the PS4.");
|
||||
Say("");
|
||||
Say("What to do now:");
|
||||
Say(" 1. Unplug the adapter from this PC and plug it into the PS4.");
|
||||
Say(" 2. FULLY CLOSE Rock Band 4 (PS button > Close Application), then start it again.");
|
||||
Say(" 3. Try the guitar in the menus.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Say("OK. When you are ready, choose option 1 and it will send the map file for you.");
|
||||
}
|
||||
}
|
||||
|
||||
static int SelfTest()
|
||||
{
|
||||
var c = new Dictionary<string, byte[]>();
|
||||
Func<string, byte[]> B = s => { var p = s.Split(' '); var r = new byte[p.Length]; for (int i = 0; i < p.Length; i++) r[i] = Convert.ToByte(p[i], 16); return r; };
|
||||
c["idle"] = B("00 00 06 7F 7F 7F 00 00"); c["green"] = B("80 00 06 7F 7F 7F 00 00"); c["red"] = B("02 00 06 7F 7F 7F 00 00");
|
||||
c["yellow"] = B("01 00 06 7F 7F 7F 00 00"); c["blue"] = B("04 00 06 7F 7F 7F 00 00"); c["orange"] = B("08 00 06 7F 7F 7F 00 00");
|
||||
c["start"] = B("00 02 06 7F 7F 7F 00 00"); c["select"] = B("00 01 06 7F 7F 7F 00 00");
|
||||
c["strum_up"] = B("00 00 07 7F 7F 7F 00 00"); c["strum_down"] = B("00 00 05 7F 7F 7F 00 00"); c["whammy"] = B("00 00 06 7F 00 7F 00 00");
|
||||
var pr = new List<string>();
|
||||
foreach (var l in Mapper.Build("2563", "0575", c, pr)) Say(l);
|
||||
Say("problems: " + pr.Count);
|
||||
bool ch; Say("--- ini: empty"); Say(UpdateIni("", new[] { Rb4dxLine, PluginLine }, out ch));
|
||||
Say("--- ini: other plugins"); Say(UpdateIni("[settings]\r\nshow_load_notification=true\r\n[CUSA00419]\r\n/a.prx\r\n", new[] { PluginLine }, out ch));
|
||||
Say("--- ini: section in middle"); Say(UpdateIni("[CUSA02084]\n" + Rb4dxLine + "\n[CUSA11100]\n/b.prx\n", new[] { Rb4dxLine, PluginLine }, out ch));
|
||||
UpdateIni("[CUSA02084]\n" + Rb4dxLine + "\n" + PluginLine + "\n", new[] { Rb4dxLine, PluginLine }, out ch); Say("already complete -> changed: " + ch);
|
||||
Say("devices: " + HidDev.Enumerate().Count);
|
||||
Say("plugin bytes: " + PluginBytes().Length);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int Main(string[] args)
|
||||
{
|
||||
Console.Title = "Guitar Tool for Rock Band 4 on PS4";
|
||||
try
|
||||
{
|
||||
if (args.Length > 0 && args[0] == "--selftest") return SelfTest();
|
||||
if (args.Length > 1 && args[0] == "--install") { Install(args[1]); return 0; }
|
||||
while (true)
|
||||
{
|
||||
Say("");
|
||||
Say("==============================================");
|
||||
Say(" GUITAR TOOL - Rock Band 4 on PS4");
|
||||
Say("==============================================");
|
||||
Say("");
|
||||
Say(" What do you want to do?");
|
||||
Say("");
|
||||
Say(" Type 1 = Put the guitar plugin on my PS4");
|
||||
Say(" (do this first, the PS4 must be on and jailbroken)");
|
||||
Say("");
|
||||
Say(" Type 2 = My guitar's buttons are wrong or do nothing");
|
||||
Say(" (plug the guitar into this PC, I will learn its buttons)");
|
||||
Say("");
|
||||
Say(" Type 3 = Close this program");
|
||||
Say("");
|
||||
string c = Ask("Type 1, 2 or 3, then press the Enter key: ");
|
||||
try
|
||||
{
|
||||
if (c == "1") Install(null);
|
||||
else if (c == "2") MakeMap();
|
||||
else if (c == "3") return 0;
|
||||
}
|
||||
catch (Exception ex) { Say(""); Say("ERROR: " + ex.Message); }
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Say("ERROR: " + ex.Message); Ask("Press Enter to close."); return 1; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Builds GuitarTool.exe with the C# compiler that ships with Windows. The plugin is embedded in the exe.
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$root = Split-Path -Parent $here
|
||||
$csc = "$env:WINDIR\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
|
||||
$prx = Join-Path $root 'bin\OrbisInstrumentalizer.prx'
|
||||
$out = Join-Path $root 'bin\GuitarTool.exe'
|
||||
& $csc /nologo /target:exe /platform:anycpu /optimize+ "/out:$out" "/resource:$prx,plugin.prx" "$here\GuitarTool.cs"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'compile failed' }
|
||||
"Built $out"
|
||||
Reference in new issue
Block a user