The Guncon 3 is a light gun for the Play Station 3 that was bundled with Time Crisis 4. It is a USB device with two joysticks, 9 buttons and an IR LED based pointer (similar to the Wiimote). It is only compatible with the PS3 games Time Crisis 4 (TC4), Time Crisis 4: Razing Storm and Deadstorm Pirates. While it is only supported by a small number of games I found it to be a very good and accurate light gun, better than my AimTrak and with more buttons. The only problem is the lack of support for any other device - there are no drivers for Windows, Linux, etc. I hoped to fix this problem by creating a Linux kernel module to support the Guncon 3.
When plugged in to a computer the Guncon 3 appears as a USB Hub with an attached device with the following device descriptor and configuration descriptor.
I was hoping that the USB config and endpoint descriptors would provide useful information, however there isn't much apart from a few clues. We can see that there are 2 Interrupt endpoints on the USB device, one input and one output. If you are not familiar with the specifics of the USB protocol then all you really need to know is that USB devices can have endpoints of different types. The endpoints are unidirectional and are used to send data to and from the device. The interrupt endpoints must be polled by the driver and always send/receive a fixed amount of data. So this device has input (relative to the host) endpoint that will send a 15 byte packet, and an output endpoint that will receive 8 an 8 byte packet.
Connecting the device to my PC and polling the input endpoint did not return any data, so at that point I tried to send some random data to the output endpoint (I used a Python script with pyusb
to send and receive data). After sending 8 bytes to the Guncon data started appearing on the input endpoint (yay!). It seems that the Guncon requires a setup packet to initialise, however the data that gets sent back from Guncon appears to be scrambled in some way (boo!). The data packets being sent from the Guncon appear to repeat, for example there will be the same pair of packets sent many many times in a row:
56 C8 30 71 97 4B 3F F4 27 46 F1 40 98 EF F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
56 C7 30 71 97 4B 3F F4 27 46 F1 40 7D 29 F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
56 C7 30 71 97 4B 3F F4 27 46 F1 40 7D 29 F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
56 C7 30 71 97 4B 3F F4 27 46 F1 40 7D 29 F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
...
When you move a joystick or press a button you can see all of bytes in the output data changing and then changing back when the button is released. This suggests that the data is scrambled in some way, possibly encrypted. I determined that I could go no further with only the data from a PC, so I got a Play Station 3 and Time Crisis 4.
I used a Beagle USB 12 protocol analyser and hooked the Guncon up to the PS3 and started Time Crisis 4 (TC4), before starting TC4 the PS3 reports the Guncon as an unknown USB device - the game must provide a driver, because as soon as the game starts you can see the USB enumeration and setup packets, along with the magic initialisation packet sent to the output endpoint. In all cases I have observed the initialisation packet starts with 0x01
, in this case the packet was 01 12 6F 32 24 60 17 21
. I then observed the same behaviour as when it was connected to the PC, apparently scrambled data packets.
It did not seem likely that I would be able to decipher the scrambling based only on the observations of the data packets, so I took the next logic step - disassembly. You need to get a copy of the decrypted .elf file from your copy of TC4, and I cannot explain how to do this - there are a number of guides available online for decrypting EBOOT.BIN. Once I had the TC4 elf file I could load it up in IDA Pro 64Bit , I am using 6.8 (which was the latest version when I was doing this, 6.9 is out now...) An extra plug-in called PowerPC AltiVec Extension is required to decode the extra AltiVec instructions used in the CellBE processor.
I used a IDA script to analyse the TC4 elf (analyze_self.idc from kakaroto/ps3ida which will find the TOC address (which you can set in IDA under Options->General->Analyse->Processor specific analysis options) and it will find the function imports and exports (which is very useful for understanding the USB driver functions).
Note: The Cell Processor is a 64-bit RISC PowerPC with 32x64bit general purpose registers. TC4 was compiled with GCC 4.1.1, this is useful to note because most of the time when pointers are referenced there will be some pretty dubious ASM generated, addi
, clrldi
and stw
will be used instead of stwu
and relative addressing. When a pointer to a buffer is accessed (uint8_t *buffer;
) and you wish to write to an offset you may write something like: buffer[4] = 0x10;
in C, which could be compiled to
; r4=0x10, r5=buffer
stw r4, 4(r5) ; *(buffer+4) = 0x10;
However GCC will assume that the address could overflow and will truncate it with clrldi
.
;r4=0x10, r5=buffer
addi r6, r5, 4 ; temp = buffer + 4;
clrldi r6, r6, 32 ; ensure the address is with-in 32 bits
stw r4, 0(r6) ; *(buffer+4) = 0x10;
This pattern appears quite a bit in the code we will look at, and it makes everything look a bit messy.
The way the USB driver seems to work with the PS3 is that a few callbacks are defined for probing, attaching and detaching devices - probe is called when a new device is plugged in and tests to see if the new device is supported, attach and detach are called to setup or teardown a supported device. Most of the other USB functions take a callback argument and pass around a pointer to a struct that describe the device.
To find the various functions in the TC4 code I guessed that there might be some debugging messages still in the code (they were left there to help us...), searching in the Strings Window (View->Open Subviews->Strings) for "guncon"
reveals a fair number of references - had there not been any strings there is still the Vendor and Product ID (0x0b9a
and 0x0800
), which appear in the probe function. The interesting strings are Guncon 3: probe ...
and Guncon 3: attach_done...
, the attach_done
string appears in a subroutine that I called guncon_attached
and starts at $0787A08
.
The guncon_attached
function takes 3 arguments, int32_t x, int32_t y, GUNCON_UNIT_t* unit
- I do not understand exactly what the first 2 arguments are but the last argument is a pointer to a struct that describes the connection to the Guncon 3 which I called GUNCON_UNIT_t
.
This is the current state of the reversed GUNCON_UNIT_t
, not all of the fields are complete but it was sufficient to work out the communication functions.
The guncon_attached
function is passed a GUNCON_UNIT_t
struct that has been partially initialised with the dev_id
and pipes
set up, the function clears the buffer
and sets the send_buffer/_related
with a key of some sorts, I don't fully understand the method used to generate the key but the buffer send_buffer_related
appears to be derived from send_buffer
(hence the name). At the end of the function the 8 byte send_buffer_related
is sent to the Guncon using the InterruptTransfer
function.
The InterruptTransfer
function sets up a callback to a subroutine at $07880E4
that I called guncon_recv
. The callback has the same arguments as all the USB callbacks (int32_t x, int32_t y, GUNCON_UNIT_t*
), and it simply sets up the output polling. Another InterruptTransfer
call is made with a pointer to the receive buffer and a callback to a subroutine at $0787CC4
which I called guncon_DecodeGunData
- this is where the really interesting bits happen.
The receive buffer is a ring buffer that appears to defined as uint8_t buffer[32][16]
. The buffer_offset
points to the current offset in the buffer for the data to be written, the Guncon only sends 15 bytes but the buffer is 16 byte aligned for speed reasons - each of the buffer elements can be stored in two registers which speeds up processing.
The guncon_DecodeGunData
function has 4 parts, error testing/housekeeping, check summing, decrypting and setting up the next interrupt poll. The housekeeping and set up for the next call are not very interesting and I won't cover them. However, the checksum and decrypt are quite interesting. The checksum code starts at $787D40
and I have written a rough C port of it.
int checksum(uint8_t *data, uint8_t expected)
{
uint8_t checksum, temp_sum;
temp_sum = ((data[14] ^ data[13]) + data[12] + data[11] - data[10] - data[9]) ^ data[8];
checksum = (((data[7] ^ temp_sum) - data[6] - data[5]) ^ data[4]) + data[3] + data[2] - data[1];
if (checksum != expected) {
GUNCON_DEBUG("checksum mismatch: %02x != %02x\n", checksum, expected);
return -1;
}
return 0;
}
This is the commented version of the IDA disassembly:
0787D40 checksum: ; CODE XREF: guncon_DecodeGunData+2Cj
0787D40 n = r26 ; this block appears to calculate a checksum of the values returned
0787D40 addi r27, r5, GUNCON_UNIT_t.buffer
0787D44 li n, 0 ; r26 = 0;
0787D48 clrldi r28, r27, 32
0787D4C addi r5, r5, GUNCON_UNIT_t.send_buffer
0787D50 addi r3, r28, GUNCON_BUFFER.buffer_data
0787D54 clrldi r27, r3, 32 ; buffer_base
0787D58 lwz r7, GUNCON_BUFFER.buffer_offset(r28) ; buffer_offset
0787D5C clrlslwi r30, r7, 27,4 ; r30 = (r7 * 16) % (31 << 4);
0787D60 add r6, r30, r27
0787D64 clrldi r30, r6, 32 ; data = buffer[buffer_offset]
0787D68 ld r3, guncon_recv_buffer.field_b(r30) ; data[1]
0787D6C ld r6, guncon_recv_buffer(r30) ; data[0]
0787D70 srdi r31, r3, 8 ; b >> 8
0787D74 srdi r10, r3, 16 ; b >> 16
0787D78 srdi r9, r3, 24 ; b >> 24
0787D7C xor r12, r31, r10 ; (b >> 8) ^ (b >> 16)
0787D80 srdi r11, r3, 32 ; b >> 32
0787D84 add r4, r12, r9 ; ((b >> 8) ^ (b >> 16)) + (b >> 24)
0787D88 srdi r8, r3, 40 ; b >> 40
0787D8C add r0, r4, r11 ; ((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32)
0787D90 srdi r7, r3, 48 ; b >> 48
0787D94 subf r31, r8, r0 ; ((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32) - (b >> 40)
0787D98 srdi r9, r3, 56 ; b >> 56
0787D9C subf r12, r7, r31 ; ((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32) - (b >> 40) - (b >> 48)
0787DA0 srdi r11, r6, 8 ; a >> 8
0787DA4 xor r10, r12, r9 ; (((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32) - (b >> 40) - (b >> 48)) ^ (b >> 56)
0787DA8 srdi r8, r6, 16 ; a >> 16
0787DAC xor r4, r6, r10 ; a ^ b_sum
0787DB0 srdi r7, r6, 24 ; a >> 24
0787DB4 subf r3, r11, r4 ; (a ^ b_sum) - (a >> 8)
0787DB8 srdi r31, r6, 32 ; a >> 32
0787DBC subf r0, r8, r3 ; (a ^ b_sum) - (a >> 8) - (a >> 16)
0787DC0 srdi r9, r6, 40 ; a >> 40
0787DC4 xor r12, r0, r7 ; ((a ^ b_sum) - (a >> 8) - (a >> 16)) ^ (a >> 24)
0787DC8 srdi r11, r6, 48 ; a >> 48
0787DCC add r10, r12, r31 ; a ^ b_sum - (a >> 8) - (a >> 16) ^ (a >> 24) + (a >> 32)
0787DD0 add r4, r10, r9 ; a ^ b_sum - (a >> 8) - (a >> 16) ^ (a >> 24) + (a >> 32) + (a >> 40)
0787DD4 subf r3, r11, r4 ; a ^ b_sum - (a >> 8) - (a >> 16) ^ (a >> 24) + (a >> 32) + (a >> 40) - (a >> 48)
0787DD8 clrldi r10, r3, 56 ; r3 & 0xff
0787DDC
0787DDC decode: ; CODE XREF: guncon_DecodeGunData+228j
0787DDC slwi r4, n, 3 ; r4 = r26 * 8;
0787DDC ; r26 is incremented later, the first time it's 0
0787DDC ; this loop wil run twice, probably decoding each quad of the data from the gun
0787DE0 clrldi r31, r5, 32 ; r31 = send_buffer
0787DE4 add r8, r4, r31 ; send_buffer[n]
0787DE8 clrldi r11, r8, 32
0787DEC lbz r0, 7(r11) ; r11 = send_buffer[n][7]
0787DF0 cmpw cr1, r0, r10 ; compare checksum
0787DF4 bne cr1, invalid_data ; checksum mismatch
You can see that the see the address masking I mentioned before a few times in this snippet. The checksum is calculated and compared the last byte in send_buffer
(aka key
), if the checksum does not match then there is some retry logic and if that fails a new send_buffer/key
is generated and sent to the Guncon. I don't know if this a standard checksum algorithm or if it is something Namco came up with? The next part of the function deals with the decryption of the data from the Guncon.
There is a decryption table at $1009AA90
that is used to decrypt the data sent from the Guncon - this is probably the main reason no other games or drivers can use this lightgun (they don't have the decryption algorithm), which is a really big shame because it's an excellent light gun! Again I will show my recoded C version and then the relevant disassembly.
The key is used in combination with the last byte of the data to compute an offset in the KEY_TABLE
that is used as the starting point to decrypt the rest of the data. The 15th byte in the data is padding used so that the checksum will work out, the 1st of the buffer is not part of the data so this leaves 13 bytes for joystick state data. Only bytes 13 to 1 can be decoded, and only those are decoded by the function. The operation performed on each byte is determined by the byte in KEY_TABLE
, it is either added, subtracted, or XORed with a key byte and the byte from KEY_TABLE
.
int Guncon 3_decode(uint8_t *data, const unsigned char *key) {
int32_t x, y, key_index;
uint8_t bkey, keyr, key_offset, byte;
key_offset = (((((key[1] ^ key[2]) - key[3] - key[4]) ^ key[5]) + key[6] - key[7]) ^ data[15]) + (unsigned char)0x26;
key_index = 4;
// byte E is part of the key offset
// byte D is ignored, probably a padding byte - to make the checksum workout
for (x = 13; x >= 1; x--) {
byte = data[x];
for (y = 0; y < 3; ++y) {
key_offset--;
bkey = KEY_TABLE[key_offset + 0x41];
keyr = key[key_index];
if (--key_index == 0)
key_index = 7;
// bkey & 3 selects the mode operation for this byte
if ((bkey & 3) == 0)
byte =(byte - bkey) - keyr;
else if ((bkey & 3) == 1)
byte = ((byte + bkey) + keyr);
else
byte = ((byte ^ bkey) ^ keyr);
}
data[x] = byte;
}
return 0;
}
And now for the disassembly...
0787DDC decode: ; CODE XREF: guncon_DecodeGunData+228j
0787DDC slwi r4, n, 3 ; r4 = r26 * 8;
0787DDC ; r26 is incremented later, the first time it's 0
0787DDC ; this loop wil run twice, probably decoding each quad of the data from the gun
0787DE0 clrldi r31, r5, 32 ; r31 = send_buffer
0787DE4 add r8, r4, r31 ; send_buffer[n]
0787DE8 clrldi r11, r8, 32
0787DEC lbz r0, 7(r11) ; r11 = send_buffer[n][7]
0787DF0 cmpw cr1, r0, r10 ; compare checksum
0787DF4 bne cr1, invalid_data ; checksum mismatch
0787DF8 ld r6, 0(r11) ; send_buffer as quad
0787DFC lbz r7, 15(r30) ; last byte of buffer
0787E00 srdi r0, r6, 48
0787E04 srdi r12, r6, 40
0787E08 srdi r10, r6, 32
0787E0C xor r9, r0, r12
0787E10 srdi r31, r6, 24
0787E14 subf r11, r10, r9
0787E18 srdi r5, r6, 16
0787E1C subf r4, r31, r11
0787E20 lwz r31, guncon_table_p ; guncon_table+0x41
0787E24 .drop r31 ; 0x1009AAD11009A910
0787E24 srdi r8, r6, 8
0787E28 xor r3, r4, r5
0787E2C add r12, r3, r8
0787E30 li r3, 5
0787E34 subf r0, r6, r12
0787E38 li r12, 13 ; loop_counter = 13 // there are 15 bytes in the buffer, the last byte is a checksum and
0787E38 ; // the first byte is a counter and the 14th byte is ignored...
0787E3C xor r10, r0, r7
0787E40 rotrdi r7, r6, 16 ; rotate right 16
0787E44 addi r9, r10, 38 ; key_hash + 0x26 (0x26 is the minimum offset if key_hash is 0)
0787E48 clrldi r11, r9, 56 ; & 0xff // this has a wrap around
0787E4C add r5, r11, r31 ; key_table_[key_hash]
0787E50 clrldi r6, r5, 32 ; restrict to 32bit address space
0787E54 b decode_start ; buffer[r12]
0787E58 ; ---------------------------------------------------------------------------
0787E58
0787E58 decode_write: ; CODE XREF: guncon_DecodeGunData+1CCj
0787E58 byte = r10 ; r12--
0787E58 addi r12, r12, -1
0787E5C clrldi r5, r31, 32
0787E60 cmpdi cr1, r12, 0 ; if r12 == 0
0787E64 stb byte, 0(r5) ; buffer[r12] = decoded
0787E68 beq cr1, decode_end
0787E6C
0787E6C decode_start: ; CODE XREF: guncon_DecodeGunData+190j
0787E6C add r31, r12, r30 ; buffer[r12]
0787E70 .drop r31
0787E70 li r5, 4
0787E74 clrldi r8, r31, 32
0787E78 lbz byte, 0(r8) ; buffer[i]
0787E7C
0787E7C inner_loop: ; CODE XREF: guncon_DecodeGunData+21Cj
0787E7C ; guncon_DecodeGunData+29Cj ...
0787E7C addi r5, r5, -1 ; r5-- // first loop r5 = 3
0787E80 addi r0, r6, -1 ; key_ptr-- // KEY_TABLE offset is decremented first, before the loop test it made
0787E80 ; for (int y = 4; y > 0; --y, --key_ptr)
0787E84 cmpdi cr7, r5, 0
0787E88 li r4, 7
0787E8C li r11, 48
0787E90 beq cr7, decode_write ; if r5 == 0
0787E94 clrldi r6, r0, 32
0787E98 lbz r9, 0(r6) ; *key_ptr; // KEY_TABLE[key_offset]
0787E9C addi r0, r3, -1
0787EA0 cmpdi cr6, r0, 0
0787EA4 mtspr CTR, r0
0787EA8 beq cr6, change_rbits_skip ; if r3 == 0
0787EAC li r4, 0
0787EB0 li r11, 56
0787EB4
0787EB4 change_rbits_skip: ; CODE XREF: guncon_DecodeGunData+1E4j
0787EB4 key = r7 ; clear top 62 bits...
0787EB4 clrldi r3, r9, 62
0787EB8 rldcl key, key, r11,0 ; rotate right (16 or 8 bits)
0787EBC cmpdi r3, 0
0787EC0 mfspr r11, CTR
0787EC4 cmpdi cr6, r3, 1
0787EC8 bkey = r9
0787EC8 subf r8, bkey, byte ; r8 = byte - bkey
0787ECC add r3, r4, r11
0787ED0 beq key_0 ; byte = r8 - key
0787ED4 xor r0, byte, bkey ; r0 = byte ^ bkey
0787ED8 beq cr6, key_1 ; byte = byte + bkey + key
0787EDC xor byte, r0, key ; byte = (byte ^ bkey) ^ key
0787EE0 byte = r10
0787EE0 b inner_loop ; r5-- // first loop r5 = 3
0787EE4 ; ---------------------------------------------------------------------------
0787EE4
0787EE4 invalid_data: ; CODE XREF: guncon_DecodeGunData+130j
0787EE4 addi n, n, 1 ; Resend the init packet when 'n' reaches 2
0787EE8 cmpdi cr7, n, 2
0787EEC bne cr7, decode ; r4 = r26 * 8;
0787EEC ; r26 is incremented later, the first time it's 0
0787EEC ; this loop wil run twice, probably decoding each quad of the data from the gun
; TRUNCATED FOR BREVITY
; This section retries the interrupt request
; ---------------------------------------------------------------------------
0787F5C
0787F5C key_0: ; CODE XREF: guncon_DecodeGunData+20Cj
0787F5C key = r7 ; byte = r8 - key
0787F5C byte = r10
0787F5C bkey = r9
0787F5C subf byte, key, r8
0787F60 b inner_loop ; r5-- // first loop r5 = 3
0787F64 ; ---------------------------------------------------------------------------
0787F64
0787F64 key_1: ; CODE XREF: guncon_DecodeGunData+214j
0787F64 add r4, byte, bkey ; byte = byte + bkey + key
0787F68 add byte, r4, key
0787F6C b inner_loop ; r5-- // first loop r5 = 3
0787F70 ; ---------------------------------------------------------------------------
0787F70
0787F70 decode_end: ; CODE XREF: guncon_DecodeGunData+1A4j
0787F70 lwz r11, GUNCON_BUFFER.buffer_offset(r28)
0787F74 cmpdi cr6, r26, 0 ; r26 is the outer loop counter
0787F78 addi r12, r11, 1
0787F7C clrlslwi r0, r11, 27,4
0787F80 clrlslwi r10, r12, 27,4
0787F84 add r8, r0, r27
0787F88 add r9, r10, r27
0787F8C clrldi r31, r8, 32 ; buffer[i % 31]
0787F90 lbz r7, 0(r31)
0787F94 clrldi r26, r12, 32
0787F98 clrldi r30, r9, 32 ; buffer[i+1 % 31]
0787F9C addi r6, r7, 1
0787FA0 stb r6, 0(r30)
0787FA4 stw r26, GUNCON_BUFFER.buffer_offset(r28) ; buffer_offset++;
0787FA8 bne cr6, new_send_buffer ; if the first decode failed, then resend the sendbuffer
; Code after here sends the request for next interrupt request
Deciphering the code for the PS3 USB driver took me a little while, but most of the time was spent looking up PowerPC instructions like clrlslwi
. This was the first time that I have looked at PowerPC ASM, but it isn't too dissimilar to some other things I have worked on so picking it up wasn't too tricky. There may be some mistakes in the comments for ASM as most of the comments are still from the first pass I did in IDA. However, the C versions of the functions have been tested and do work.
Source code available: beardypig/guncon3 (provided as-is and possibly not in a full usable state)
Once the decryption algorithm was reversed I could decrypt all the information coming from the Guncon. To write the driver I had to look at the decoded data coming from the Guncon and work out what was what, which is quite easily done by pressing the buttons one at a time :) By watching the data when the buttons are pressed in turn we can easily see which bit is which for the buttons.
BTN_TRIGGER = (data[1] & 0x20));
BTN_A1 = (data[0] & 0x04));
BTN_A2 = (data[0] & 0x02));
BTN_B1 = (data[1] & 0x04));
BTN_B2 = (data[1] & 0x02));
BTN_C1 = (data[1] & 0x80));
BTN_C2 = (data[0] & 0x08));
BTN_JOY_A = (data[2] & 0x80));
BTN_JOY_B = (data[2] & 0x40));
The Joystick axes are also easy to work out, moving the joysticks one axis at a time shows that there is only 1 byte the changes. The unsigned bytes center to 127 and go from 0 to 255, this means that each axis for the joystick is a signed char.
JOY_A_X = (int8_t)data[11]
JOY_A_Y = (int8_t)data[12];
JOY_B_X = (int8_t)data[9];
JOY_B_Y = (int8_t)data[10];
The last piece of the puzzle is the aiming, pointing the gun around at the IR LEDs sees the remaining 6 bytes changing. With some trial and error I found there are 3 shorts, two of which correspond roughly to left/right and up/down movement of the gun and the other corresponds roughly to the distance from the sensors to the guns. I say roughly because there are maximum and minimum values, and it depends on the size of your TV and how far apart the sensors are. There is a need for some calibration as the IR LEDs can be placed at different distances, the extra value sent by the Guncon is probably used to adjust the other values and as a reference for the calibration.
AIM_X = ((short)data[4] << 8) | (short)data[3];
AIM_Y = ((short)data[6] << 8) | (short)data[5];
AIM_Z = (data[8] << 8) + data[7]
I really wanted to use this light gun in MAME for all the old arcade shooting games, so I wrote a kernel module so that it will work as a joystick. I haven't really done much kernel programming before and nothing to do with the joydev
module, I based my module heavily on other already existing modules including the xpad
and guncon2
modules. There are known bugs and omissions, you may have noticed there was not information about how the key was generated - I am currently using a fixed key that works with the two Guncon 3 guns that I have.
There also appears to be a strange bug where 100s of button presses being generated when the joysticks are held in a specific position, I have yet to track it down. It doesn't appear to be generated by the Guncon, as the raw data remains stable and I believe there is a bug in my kernel module code :)
There is also no way to calibrate the aiming for the gun and it is not perfect, but should work OK if you are able to position the IR LEDs as far apart as they go and use the gun at a 2-3m distance from the LEDs. I plan continue updating the kernel module for the Guncon 3 and I hope that other people will be interested enough to help out :)
This information is provided for educational and interoperability purposes.