Table of Contents
NTR Protocol
Some better resources to look at would be:
TCP Client
- Source 56602 (probably doesn't matter)
- Destination: 8000 (3DS)
Send 84 bytes:
- 4 - Magic: 78 56 34 12
- 4 - Sequence: Multiples of 1000? Might not even matter
- 4 - Type (1=memory write, 0=everything else)
- 4 - Command:
- 901: Remoteplay
- 10: Write memory patch
- 9: Read memory
- 5: Connect?
- 4: Reload
- 3: Hello
- 1: Write Save
- 0: Heartbeat
- 4 x 16 arguments
- 4 - Data Len (data in next packet)
If the Data Len is greater than 0 then the next TCP packet(s) will be that data.
Commands
901: Remoteplay
- byte: priorityFactor (default 5)
- byte: mode (default 1)
- 2 bytes: (padding, i.e. the previous 2 bytes make up an int up to here)
- 4 bytes: jpegQuality
- 4 bytes: qosInByte (qosValue * 1024 * 1024 / 8) (e.g. 13238272?)
- Default qosValue is 30.0, use 101 to disable qos
5: Connect
No args used.
0: Heartbeat
No args, but there will be a DataLen so expect that data in the next TCP packets. You don't actually need to send this more than once, but you may want to send Connect and this again to get a list of process names/IDs.
UDP Client
- Port 8001
- After TCP sends Remoteplay / Connect / Heartbeat the 3DS will continuously send UDP to the same IP even if the TCP disconnects.
- The data is only TurboJPEG data.
An easy/dumb way to get started with viewing the video frames is to loop until the packet size is smaller than usual, then you can do the same from then on knowing you probably have the entire frames.
If the joined packets decompress successfully you can then look at the length. 288000 bytes is a top screen frame, otherwise it's the bottom screen.
Input Redirection
Example of buttons only:
NTRPacket ntrIR = NTRPacket.NewWriteMemory(1000, 16, 1105696, 12, 12); //Seq=1000 doesn't matter, pid=0x10 or 16, address=0x10DF20 or 1105696, payload 12 bytes Network.tcpClientStream.Write(ntrIR.GetBytes(), 0, 84); byte[] inputredirection = new byte[] { b0, b1, 0x00, 0x00, //Buttons 0x00, 0x00, 0x00, 0x02, //Touch 0x00, 0x08, 0x80, 0x00 //Circle Pad }; Network.tcpClientStream.Write(inputredirection, 0, inputredirection.Length);
A no-input payload would like like: ff0f0000 | 00000002 | 00088000 (buttons | touch | circlepad)
Wireshark filter to not see that: data.len == 12 && !(data.data == ff:0f:00:00:00:00:00:02:00:08:80:00)
Buttons
Bits “b0”: DULR SEBA 0000 YXLR = Down, Up, Left, Right, Start, Select (E), B, A
Bits “b1”: 4×0, Y, X, L, R
The 0's are always 0, and every normal button is 0 when pressed.
Touchscreen
The best way to use the touchscreen is probably to use the same code as other Input Redirection clients.
X = (int)Math.Round(((double)X / 319) * 4095); Y = (int)Math.Round(((double)Y / 239) * 4095); uint touch = (uint)(X | (Y << 12) | (0x01 << 24)); if (touch != oldtouch) { byte[] bValue = BitConverter.GetBytes(touch); bValue.CopyTo(inputredirection, 4);
Circle Pad
Again you probably should use the same code as other clients. I didn't for the Circle Pad I went for something I could understand later.
The Xbox 360/One controller uses 16 bits for each stick axis but the Circle Pad only uses 12 bits. Shaving off 4 bits removes some jitter anyway so you don't need to add your own deadzone code. It doesn't seem to matter if you leave the 4th byte set to 0x01 all the time.
uint leftX = (uint)(((report.Data[1] << 8) + (report.Data[0])) & 0xFFF0) >> 0x4; uint leftY = (uint)(((report.Data[3] << 8) + (report.Data[2])) & 0xFFF0) >> 0x4; uint circle = ((0xFFF - leftY) << 12) + leftX; if (oldcircle != circle) { byte[] bCircle = BitConverter.GetBytes(circle); bCircle[3] = 0x01; bCircle.CopyTo(inputredirection, 8);
Luma Input Redirection (Not BootNTR)
Oh boy this just dropped for Luma and it supports ZL/ZR/C-Stick input redirection. The inputs are the same as NTR input redirection.
UDP Port: 4950
20 bytes = ff0f0000 | 00000002 | fff77f00 | 81008080 | 00000000 (buttons | touch | circle | c-stick | special)
Just continuously send it.
C-Stick
C-stick includes ZL/ZR. Replace that 0x00 with either ZL = 0x02 and/or ZR = 0x04;
int rightX = (((report.Data[5] << 8) + (report.Data[4])) & 0xFFF0) >> 0x08; int rightY = 0xFF - ((((report.Data[7] << 8) + (report.Data[6])) & 0xFFF0) >> 0x08); //Includes flip // The C-Stick needs to be rotated 45 degrees... // Read: https://stackoverflow.com/questions/2259476/rotating-a-point-about-another-point-2d double pX = rightX - 127; double pY = rightY - 127; byte rotatedCX = (byte)((pX * c - pY * s) + 127); byte rotatedCY = (byte)((pX * s + pY * c) + 127); uint cstick = (uint)(rotatedCX << 8) + (rotatedCY); if (oldz != z || oldcstick != cstick) { inputredirection[13] = z; inputredirection[14] = (byte)rotatedCX; inputredirection[15] = (byte)rotatedCY;
Just testing on the home menu it seems this is good enough to fix the rotation issue.
You might notice that the X and Y of the C-Stick are 8 bits each, but I reused the Xbox controller code taking 16 bytes of each to 12 bits. Ideally I should try to add deadzone or check that the highest bits are the ones being used.
Special
The left most byte of Special includes:
- 0x00 = No button
- 0x01 = Home
- 0x02 = Power
- 0x04 = Power Long (will turn off the 3DS).
So send the button you want then send 0x00 straight after to release it.