diff --git a/src/main/java/chokistream/KeypressHandler.java b/src/main/java/chokistream/KeypressHandler.java index a558f89..8c885c0 100644 --- a/src/main/java/chokistream/KeypressHandler.java +++ b/src/main/java/chokistream/KeypressHandler.java @@ -55,6 +55,14 @@ public void keyPressed(KeyEvent e) { } else if(ck.get(Controls.INTERLACE).matches(e)) { c.toggleInterlacing(); } + } else if(client instanceof NTRClient) { + NTRClient c = (NTRClient) client; + + if(ck.get(Controls.QUALITY_UP).matches(e)) { + c.incrementQuality(5); + } else if(ck.get(Controls.QUALITY_DOWN).matches(e)) { + c.incrementQuality(-5); + } } } catch(IOException e1) { output.displayError(e1); diff --git a/src/main/java/chokistream/NTRClient.java b/src/main/java/chokistream/NTRClient.java index 1d3bbe7..3a9fe16 100644 --- a/src/main/java/chokistream/NTRClient.java +++ b/src/main/java/chokistream/NTRClient.java @@ -24,12 +24,18 @@ package chokistream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.net.ConnectException; import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Random; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import chokistream.props.ColorMode; import chokistream.props.DSScreen; @@ -38,15 +44,40 @@ public class NTRClient implements StreamingInterface { + // In practice, this probably doesn't need to be AtomicBoolean, as there should only ever be one instance of NTRClient. But this provides a little extra safety. + public static AtomicBoolean instanceIsRunning; + /** * Thread used by NTRClient to read and buffer Frames received from the 3DS. */ - private final NTRUDPThread thread; + private final NTRUDPThread udpThread; + private final HeartbeatThread hbThread; + + private final Random random = new Random(); private static final Logger logger = Logger.INSTANCE; private int topFrames; private int bottomFrames; + + private Socket soc = null; + private OutputStream socOut = null; + private InputStream socIn = null; + + private String host = ""; + + private static class SettingsChangeQueue { + public boolean queued = false; + // safe defaults just in case + public int quality = 70; + public DSScreen screen = DSScreen.TOP; + public int priority = 4; + public int qos = 16; + public SettingsChangeQueue() {} + } + private static SettingsChangeQueue scq = new SettingsChangeQueue(); + private static NFCPatchType nfcPatchQueued = null; + private int qualityDeltaQueue = 0; /** * Create an NTRClient. @@ -60,40 +91,75 @@ public class NTRClient implements StreamingInterface { * @throws UnknownHostException * @throws InterruptedException */ - public NTRClient(String host, int quality, DSScreen screen, int priority, int qos, ColorMode colorMode, int port) throws UnknownHostException, IOException, InterruptedException { - thread = new NTRUDPThread(screen, colorMode, port); - thread.start(); - - try { - - sendInitPacket(host, port, screen, priority, quality, qos); - - // Give NTR some time to think - TimeUnit.SECONDS.sleep(3); - - // NTR expects us to reconnect, so we will. And then disconnect again! - Socket client = new Socket(host, 8000); - client.close(); + public NTRClient(String host, int quality, DSScreen screen, int priority, int qos, ColorMode colorMode, int port) throws Exception, UnknownHostException, IOException, InterruptedException { + instanceIsRunning.set(true); + scq.queued = false; + udpThread = new NTRUDPThread(screen, colorMode, port); + udpThread.start(); + hbThread = new HeartbeatThread(); + logger.log("Connecting..."); - } catch (ConnectException e) { - if(thread.isReceivingFrames()) { - logger.log(e.getClass()+": "+e.getMessage()+System.lineSeparator()+Arrays.toString(e.getStackTrace()), LogLevel.VERBOSE); - logger.log("NTR's NFC Patch seems to be active. Proceeding as normal..."); - } else { + this.host = host; + if(udpThread.isReceivingFrames()) { + // defer TCP communication init to HeartbeatThread + //logger.log("NTR UDP client connected"); + scq.quality = quality; + scq.screen = screen; + scq.priority = priority; + scq.qos = qos; + scq.queued = true; + hbThread.start(); + } else { + try { + reopenSocket(); + //soc = new Socket(host, 8000); + //soc.setSoTimeout(10000); + //socOut = soc.getOutputStream(); + //socIn = soc.getInputStream(); + + sendInitPacket(quality, screen, priority, qos); + + // Give NTR some time to think + TimeUnit.SECONDS.sleep(2); + + hbThread.start(); + } catch (ConnectException e) { + if(udpThread.isReceivingFrames()) { + logger.log("NTRClient warning: "+e.getClass()+": "+e.getMessage()); + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.VERBOSE); + logger.log("NTR's NFC Patch seems to be active. Proceeding as normal..."); + } else { + close(); + throw e; + } + } catch (Exception e) { + close(); throw e; } } } @Override - public void close() throws IOException { - thread.interrupt(); - thread.close(); + public void close() { + udpThread.interrupt(); + udpThread.close(); + if(hbThread != null) { + hbThread.close(); + hbThread.interrupt(); + } + if(soc != null && !soc.isClosed()) { + try { + soc.close(); + } catch (Exception e) { + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.REGULAR); + } + } + instanceIsRunning.set(false); } @Override public Frame getFrame() throws InterruptedException { - Frame f = thread.getFrame(); + Frame f = udpThread.getFrame(); if(f.screen == DSScreen.TOP) { topFrames++; } else { @@ -123,17 +189,233 @@ public int framesSinceLast(DSScreenBoth screens) { } } - public static void sendNFCPatch(String host, int chooseAddr) { + private class HeartbeatThread extends Thread { + private boolean nfcPatchSent = false; + public AtomicBoolean shouldDie = new AtomicBoolean(false); + HeartbeatThread() {} + + public void close() { + shouldDie.set(true); + } + + @Override + public void run() { + if(soc == null || soc.isClosed()) { + try { + reopenSocket(); + scq.queued = true; + } catch (Exception e) { + boolean b = false; + try { + b = udpThread.isReceivingFrames(); + } catch (InterruptedException e2) {} + if(b) { + logger.log("NTR's NFC Patch seems to be active. Shutting down HeartbeatThread..."); + logger.log("NTRClient$HeartbeatThread warning: "+e.getClass()+": "+e.getMessage()); + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.EXTREME); + } else { + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.REGULAR); + } + shouldDie.set(true); + } + } + + while (!shouldDie.get()) { + if(qualityDeltaQueue != 0) { + int newQual = scq.quality + qualityDeltaQueue; + qualityDeltaQueue = 0; + if(newQual < 10) { + newQual = 10; + } else if(newQual > 100) { + newQual = 100; + } + if(scq.quality != newQual) { + scq.quality = newQual; + scq.queued = true; + } + } + + if(scq.queued) { + try { + changeSettingsWhileRunning(scq.quality, scq.screen, scq.priority, scq.qos); + } catch (Exception e) { + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.REGULAR); + } + scq.queued = false; + } + + try { + heartbeat(); // TODO: use reply + } catch (SocketException e) { + /** + * "Connection reset" or "Connection reset by peer" (TODO: test for that string) + * Which usually means the NFC Patch is now fully active. + * NTR disconnected from Chokistream, and we can't reconnect over TCP. + * so kill this thread. + */ + logger.log("NTRClient$HeartbeatThread warning: "+e.getClass()+": "+e.getMessage()); + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.EXTREME); + //if (nfcPatchSent) + //logger.log("NTR's NFC Patch seems to be active. Shutting down HeartbeatThread..."); + shouldDie.set(true); + } catch (SocketTimeoutException e) { + logger.log("NTRClient$HeartbeatThread warning: "+e.getClass()+": "+e.getMessage()); + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.EXTREME); + } catch (Exception e) { + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.REGULAR); + } + + if (!nfcPatchSent && nfcPatchQueued != null) { + try { + sendNFCPatch(nfcPatchQueued); + nfcPatchSent = true; + } catch (Exception e) { + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.REGULAR); + } + nfcPatchQueued = null; + } + //TimeUnit.SECONDS.sleep(1); + } + scq.queued = false; + } + } + + /** + * (Re-) Opens the Socket at the specified IP address. + * + * @throws SocketException + * @throws UnknownHostException + * @throws IOException + */ + public void reopenSocket() throws SocketException, UnknownHostException, IOException { + if(soc != null && !soc.isClosed()) { + soc.close(); + } + + try { + /** + * TODO: Socket init takes waaaay too long. But I don't understand + * Java standard libraries enough to improve this yet. + * Specifically, the first line following this comment is the main hangup, + * Because we can't manually set a timeout period. And default is too long. + * I don't notice anything breaking due to this, but it's inconvenient. -C + */ + Socket newSoc = new Socket(host, 8000); + soc = newSoc; + soc.setSoTimeout(10000); + socOut = soc.getOutputStream(); + socIn = soc.getInputStream(); + } catch (IOException e) { + // TODO: Maybe close soc, for the sake of predictable behavior. + throw e; + } + } + + /** + * NTR Heartbeat; gets any new available debug log output from NTR, and logs it via Chokistream.Logger. + * + *

This function works with NTR 3.6, NTR 3.6.1, and NTR-HR.

+ * + * @return Debug output data received from NTR, converted to a UTF-8 String but otherwise unmodified. + * If no debug output is received from NTR, this function returns an empty String. + * @throws Exception Thrown when the supposed response from NTR is invalid. + * Also rethrows any Exceptions thrown by {@link #recvPacket()}, and by extension {@link #Packet(byte[])}. + * @throws IOException refer to {@link #sendPacket(Packet)} and {@link #recvPacket()} + */ + public String heartbeat() throws Exception, IOException { + Packet pak = new Packet(); + int heartbeatSeq = random.nextInt(100); + pak.seq = heartbeatSeq; + pak.type = 0; + pak.cmd = 0; // heartbeat command + + try { + logger.log("Sending NTR Heartbeat packet...", LogLevel.EXTREME); + sendPacket(pak); + logger.log("NTR Heartbeat packet sent.", LogLevel.EXTREME); + } catch(IOException e) { + logger.log("NTR Heartbeat error: Packet failed to send."); + throw e; + } + + /* + try { + TimeUnit.SECONDS.sleep(1); + } catch(InterruptedException e) { + // lol + } + */ + + Packet reply = recvPacket(); + + // TODO: sooner or later i'll implement this very differently. but this should work fine for now. -C + if(reply.cmd != 0 || reply.seq != heartbeatSeq) { + logger.log("NTR Heartbeat error: Received non-matching response packet."); + if(reply.cmd != 0) { + logger.log("cmd "+reply.cmd+" != 0"); + } + if(reply.seq != heartbeatSeq) { + logger.log("seq "+reply.seq+" != "+heartbeatSeq); + } + throw new Exception(); // TODO: More specific; add a message + } + + logger.log("NTR Heartbeat response received.", LogLevel.EXTREME); + + if(reply.exdata.length > 0) { + String debugOutUnmodified = new String(reply.exdata, StandardCharsets.UTF_8); + String debugOut = debugOutUnmodified; + if(debugOut.charAt(debugOut.length()-1) == '\n') { + debugOut = debugOut.substring(0, debugOut.length()-1); + } + // custom formatting + String ntrText = null; + if(debugOut.charAt(0) == '[') { // then it's most likely NTR-HR + ntrText = "[NTR]"; + } else { + ntrText = "[NTR] "; + } + debugOut = debugOut.replace("\n", "\n"+ntrText); + logger.log(ntrText+debugOut, LogLevel.REGULAR); + return debugOutUnmodified; + } else { + logger.log("NTR Heartbeat response is empty.", LogLevel.EXTREME); + return ""; + } + } + + /** + * Send a Reload command to NTR. + *

Not applicable to NTR-HR; in such a case, + * the command will be ignored and this function should return safely.

+ * + *

Note: It is unknown whether NTR's Reload functionality works.

+ * + * @throws IOException refer to {@link #sendPacket(Packet)} + */ + public void sendReloadPacket() throws IOException { + Packet pak = new Packet(); + pak.cmd = 4; + try { + logger.log("Sending Reload packet", LogLevel.VERBOSE); + sendPacket(pak); + } catch(IOException e) { + logger.log("NTR Reload failed!"); + throw e; + } + } + + public void sendNFCPatch(NFCPatchType type) { Packet pak = new Packet(); pak.seq = 24000; pak.type = 1; pak.cmd = 10; pak.args[0] = 26; // pid; 0x1A - pak.args[1] = switch(chooseAddr) { - case 0: + pak.args[1] = switch(type) { + case OLD: yield 0x00105AE4; // Sys ver. < 11.4 - default: + case NEW: yield 0x00105B00; // Sys ver. >= 11.4 }; @@ -141,15 +423,52 @@ public static void sendNFCPatch(String host, int chooseAddr) { pak.args[2] = pak.exdata.length; try { - sendPacket(host, pak); + sendPacket(pak); logger.log("NFC Patch sent!"); } catch(IOException e) { - e.printStackTrace(); // TODO: change this? + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.REGULAR); logger.log("NFC Patch failed to send"); } } - public static void sendInitPacket(String host, int port, DSScreen screen, int priority, int quality, int qos) throws UnknownHostException, ConnectException, IOException { + /** + * Queues up the NFC Patch. + * Note: the queue only has one slot. + * + * @param ver Which version of the NFC Patch is to be used. + * 1 = NFC Patch for System Update 11.4.x or higher + * 0 = NFC Patch for System Update 11.3.x or lower + * -1 = Un-queue a queued NFC Patch. + */ + public static void queueNFCPatch(NFCPatchType ver) { + if(ver == null && nfcPatchQueued != null) { + nfcPatchQueued = ver; + logger.log("NTR NFC Patch un-queued"); + } else { + nfcPatchQueued = ver; + if(!instanceIsRunning.get()) { + logger.log("NTR NFC Patch queued"); + } + } + } + + /** + * Sends a packet to NTR which configures settings and signals to start streaming image data. + * + *

+ * Note: For NTR 3.6 and 3.6.1, if NTR has already started streaming, + * these settings cannot be changed just by using this function. + * This limitation does not apply to NTR-HR. + *

+ * + * @param quality + * @param screen + * @param priority + * @param qos NTR "Quality of Service" (misnomer) + * + * @throws IOException refer to {@link #sendPacket(Packet)} + */ + public void sendInitPacket(int quality, DSScreen screen, int priority, int qos) throws IOException { Packet pak = new Packet(); pak.seq = 3000; pak.type = 0; @@ -161,22 +480,163 @@ public static void sendInitPacket(String host, int port, DSScreen screen, int pr try { logger.log("Sending init packet", LogLevel.VERBOSE); - sendPacket(host, pak); + sendPacket(pak); } catch(IOException e) { logger.log("Init packet failed to send"); throw e; } } - public static void sendPacket(String host, Packet packet) throws UnknownHostException, ConnectException, IOException { + /** + * Try to change NTR video settings while NTR is already connected and running. + * For NTR-HR, this is essentially just a wrapper for sendInitPacket. + * + * TODO: Make the Exception from heartbeat() more specific. + * + * @throws IOException, InterruptedException, Exception + */ + public void changeSettingsWhileRunning(int quality, DSScreen screen, int priority, int qos) throws IOException, InterruptedException, Exception { + try { + // settings unchanged + //if(this.screen == screen && this.priority == priority && this.quality == quality && this.qos == qos) + //return; + + sendInitPacket(quality, screen, priority, qos); + + // Give NTR some time to think + TimeUnit.SECONDS.sleep(1); + + String heartbeatReply = heartbeat(); + + /** + * NTR (3.6 or 3.6.1) needs to reload to reinitialize quality, priority screen, etc. (?) + * This is a somewhat hacky solution because a proper one doesn't exist. + * TODO: Account for the possible presence of irrelevant backlog debug output? (This *should* be harmless though.) + */ + if(heartbeatReply.contains("remote play already started")) { + //logger.log("Reloading NTR..."); + //sendReloadPacket(); + //TimeUnit.SECONDS.sleep(3); + //reopenSocket(host); + //sendInitPacket(port, screen, priority, quality, qos); + //TimeUnit.SECONDS.sleep(3); + //heartbeat(); + } + } catch (ConnectException e) { + if(udpThread.isReceivingFrames()) { + logger.log("NTRClient warning: "+e.getClass()+": "+e.getMessage()); + logger.log(Arrays.toString(e.getStackTrace()), LogLevel.VERBOSE); + logger.log("NTR's NFC Patch seems to be active. Proceeding as normal..."); + } else { + throw e; + } + } + } + + public static void queueSettingsChange(int quality, DSScreen screen, int priority, int qos) { + if(instanceIsRunning.get()) { + scq.quality = quality; + scq.screen = screen; + scq.priority = priority; + scq.qos = qos; + scq.queued = true; + } + } + + /** + * Increases or decreases video quality. + * + * @param delta The amount by which to increase or decrease. + */ + public void incrementQuality(int delta) { + qualityDeltaQueue = qualityDeltaQueue + delta; + } + + + /** + * Sends a packet to NTR using this NTRClient's Socket soc. + * + * @param packet the Packet to send. + * @throws IOException if an I/O error occurs. + */ + public void sendPacket(Packet packet) throws IOException { byte[] pak = packet.getRaw(); logger.log("Sending packet to NTR...", LogLevel.EXTREME); logger.log(pak, LogLevel.EXTREME); - Socket mySoc = new Socket(host, 8000); - OutputStream myOut = mySoc.getOutputStream(); - myOut.write(pak); - myOut.close(); - mySoc.close(); + socOut.write(pak); + } + + /** + * Receives a packet from NTR using this NTRClient's Socket soc. + * If an Exception is thrown, this function logs as much received data as it can, for debugging purposes. + * + * @return the received Packet. + * @throws Exception if unable to receive a full 84-byte header, or if unable to receive full exdata section. + * Also rethrows any Exceptions thrown by {@link #Packet(byte[])}. + * @throws IOException if an I/O error occurs. + */ + public Packet recvPacket() throws Exception, IOException { + Exception exception = null; + Packet pak = null; + byte[] header = new byte[84]; + int bytesReadHeader = 0; + int bytesReadExdata = 0; + + try { + logger.log("Listening for NTR TCP packet...", LogLevel.VERBOSE); + + try { + bytesReadHeader = socIn.readNBytes(header, 0, 84); + } catch(IOException e) { + bytesReadHeader = 84; // err towards logging too much, rather than nothing at all. + throw e; + } + + if(bytesReadHeader < 84) { + logger.log("NTR recvPacket error: Received only "+bytesReadHeader+" of expected "+84+" bytes."); + throw new Exception(); // TODO: More specific; add a message + } + + pak = new Packet(header); + + if(pak.exdata.length > 0) { + bytesReadExdata = socIn.readNBytes(pak.exdata, 0, pak.exdata.length); + // maybe log some amount of exdata when this line throws an IOException? + + if(bytesReadExdata < pak.exdata.length) { + // TODO: if this becomes a regular problem, maybe handle more elegantly. + logger.log("NTR recvPacket error: Received only "+bytesReadExdata+" of expected "+pak.exdata.length+" bytes."); + throw new Exception(); // TODO: More specific; add a message + } + } + } catch(Exception e) { + exception = e; + } + + // This section logs the raw packet data, mainly. + + byte[] debugOutRawPacketData; + if(bytesReadHeader == 84) { // full header and some exdata + debugOutRawPacketData = new byte[84+bytesReadExdata]; + System.arraycopy(header, 0, debugOutRawPacketData, 0, 84); + if(pak != null && pak.exdata.length > 0) { + System.arraycopy(pak.exdata, 0, debugOutRawPacketData, 84, bytesReadExdata); + } + } else { // incomplete header (only) + debugOutRawPacketData = new byte[bytesReadHeader]; + System.arraycopy(header, 0, debugOutRawPacketData, 0, bytesReadHeader); + } + + if(exception != null) { + if(debugOutRawPacketData.length > 0) { + logger.log("NTR TCP packet received! (incomplete)"); + logger.log(debugOutRawPacketData, LogLevel.REGULAR); + } + throw exception; + } + logger.log("NTR TCP packet received!", LogLevel.EXTREME); + logger.log(debugOutRawPacketData, LogLevel.EXTREME); + return pak; } /** @@ -188,7 +648,7 @@ public static void sendPacket(String host, Packet packet) throws UnknownHostExce */ public static int bytesToInt(byte[] dat, int i) { try { - return dat[i+3]<<24 | dat[i+2]>>8 & 0xff00 | dat[i+1]<<8 & 0xff0000 | dat[i]>>>24; + return (dat[i+3]&0xff)<<24 | (dat[i+2]&0xff)<<16 | (dat[i+1]&0xff)<<8 | (dat[i]&0xff); } catch(ArrayIndexOutOfBoundsException e) { e.printStackTrace(); // TODO: change this @@ -221,38 +681,78 @@ public static byte[] intToBytes(int num) { return data; } + /** + * Represents a type of NFC patch + */ + public static enum NFCPatchType { + /** NFC Patch for System Update 11.3.x or lower */ + OLD, + /** NFC Patch for System Update 11.4.x or higher */ + NEW + } + /** * Represents a (TCP) packet received from NTR / NTR-HR */ private static class Packet { - /* Header */ + /** Header */ + + /** NTR magic number. */ + // public static final int magic = 0x12345678; - /* Sequence ID. More or less optional. */ + /** Sequence ID. More or less optional. */ public int seq = 0; - /* "Type." - * Traditionally set to 0 if the Extra Data section is empty, and 1 otherwise. - * This may or may not matter to NTR. Refer to docs. (TODO) + /** + * "Type" + *

+ * As a general rule, this should be 1 if the exdata section contains data, + * and 0 if the exdata section is empty (0 bytes in length). + * Default value is -1, which tells {@link #getRaw()} to ignore this variable + * and use either 1 or 0 according to this rule. + *

+ * + *

In practice, it is unknown whether this variable actually affects NTR's behavior. + * Refer to docs. (TODO)

*/ - public int type = -1; // placeholder; + public int type = -1; - /* Command. Required. */ - public int cmd; + /** + * Command + *

+ * Required. + * Default value is -1, which is an invalid command, so NTR just ignores the packet after receiving it. + * For a list of valid commands, refer to docs. (TODO) + *

+ */ + public int cmd = -1; /** * Arguments. Context-dependent, based on the Command. - * Supports arbitrary array length between 0 and 16 (inclusive). - * Technically unsigned 32-bit integers. + * These are unsigned 32-bit integers, but that usually doesn't matter. + * In this implementation, this array may be of arbitrary length between 0 and 16 (inclusive). */ public int[] args = new int[16]; + /** Length of the exdata section. */ + //public int exdataLen; + + /** Non-Header */ + /** - * Extra Data (aka "Data") section. - * Supports arbitrary array length. + * Exdata section + *

+ * NTR calls this the "Data" section. However for the sake of clarity, + * Chokistream's code and documentation will almost always refer to this as the + * "Exdata" or "exdata" section. (short for "extra data") + *

+ * + *

This array may be of arbitrary length.

*/ - // TODO: Implement a length limit? public byte[] exdata = new byte[0]; + // TODO: Implement a length limit? + Packet(){}; @@ -271,20 +771,45 @@ private static class Packet { /** * Convert raw data into a Packet. + *

+ * When an exception is thrown, this Packet object may or may not have properly assigned all its variables. + * Doesn't do any sanity checks on seq, type, or cmd. + *

+ *

+ * It is acceptable for the input data to only consist of the 84-byte packet header. + * In such a case, the exdata variable will be a placeholder byte array, of length specified in the header. + * The intended use-case of this behavior is as follows: + *

+ *
    + *
  1. Receive the header data over the network.
  2. + *
  3. Pass the header data into this constructor to interpret the packet header.
  4. + *
  5. Check the length of the exdata section. (exdata.length)
  6. + *
  7. Receive the exdata over the network.
  8. + *
  9. Fill the exdata of this Packet object with the exdata received.
  10. + *
+ * + *

Note: Other edge-case behavior related to exdata length is currently undefined, + * and subject to change in this implementation. (TODO)

+ * * @param pak A packet, in the form of raw bytes. + * @throws Exception Thrown in some cases of invalid packet data. Specifically: + * */ - Packet(byte[] pak) { + Packet(byte[] pak) throws Exception { + // minimum valid packet length; size of header if(pak.length < 84) { - // TODO: throw an exception? - logger.log("NTRClient Packet error: pak.length < 84"); - return; + logger.log("NTRClient Packet error: Invalid packet size. "+pak.length+" bytes is too small."); + throw new Exception(); // TODO: More specific; add a message } // verify magic number if(pak[0] != 0x78 || pak[1] != 0x56 || pak[2] != 0x34 || pak[3] != 0x12) { - // TODO: throw an exception? - logger.log("Processed NTR packet does not seem to match the expected format."); - return; + logger.log("NTRClient Packet error: Processed packet is most likely malformed."); + throw new Exception(); // TODO: More specific; add a message } seq = bytesToInt(pak, 4); @@ -295,21 +820,28 @@ private static class Packet { args[i] = bytesToInt(pak, i*4+16); } + // TODO: maybe make sure this number is (more) sane // Unsigned 32-bit integer int exdataLen = bytesToInt(pak, 80); - if(exdataLen > 0) { - int expectedExdataLen = pak.length-84; - if(expectedExdataLen != exdataLen) { // shouldn't ever happen; code logic error. - logger.log("NTRClient Packet error: pak.length - 84 != exdataLen. "+expectedExdataLen+" != "+exdataLen); - if(expectedExdataLen < exdataLen) { - exdataLen = expectedExdataLen; + if(exdataLen < 0) { // :( + // unsigned int -> signed int conversion error; please don't send >2GB of data :( + logger.log("NTRClient Packet error: Reported exdata length is "+Integer.toUnsignedString(exdataLen)+" bytes. Something has gone wrong."); + } else if(exdataLen != 0) { + if(pak.length == 84) { // Calling method passed header only (this is supported) + exdata = new byte[exdataLen]; + } else { + int expectedExdataLen = pak.length-84; + // TODO: I'm undecided on whether to correct this mismatch issue, and how. -C + if(expectedExdataLen != exdataLen) { + logger.log("NTRClient Packet error: Reported exdata length ("+exdataLen+") is not equal to actual exdata length ("+expectedExdataLen+")."); + if(expectedExdataLen < exdataLen) { + exdataLen = expectedExdataLen; + } } + exdata = new byte[exdataLen]; + System.arraycopy(pak, 84, exdata, 0, exdataLen); } - exdata = new byte[exdataLen]; - System.arraycopy(pak, 84, exdata, 0, exdataLen); - } else if(exdataLen < 0) { // :( - logger.log("NTRClient Packet error: exdataLen < 0. exdataLen = "+exdataLen); } } diff --git a/src/main/java/chokistream/NTRUDPThread.java b/src/main/java/chokistream/NTRUDPThread.java index bc06f1e..0955451 100644 --- a/src/main/java/chokistream/NTRUDPThread.java +++ b/src/main/java/chokistream/NTRUDPThread.java @@ -76,8 +76,10 @@ public void close() { } public boolean isReceivingFrames() throws InterruptedException { - if (amIReceivingFrames == false) { - Thread.sleep(2000); + for (int i = 0; i < 4; i++) { + if (amIReceivingFrames) + return true; + Thread.sleep(500); } return amIReceivingFrames; } @@ -170,7 +172,7 @@ public void run() { } } catch (SocketTimeoutException e) { amIReceivingFrames = false; - logger.log("[NTR UDP] "+e.getClass()+": "+e.getMessage()); + logger.log("NTRUDPThread: "+e.getClass()+": "+e.getMessage()); } catch (IOException e) { amIReceivingFrames = false; close(); diff --git a/src/main/java/chokistream/SwingGUI.java b/src/main/java/chokistream/SwingGUI.java index 4e39754..56ca271 100644 --- a/src/main/java/chokistream/SwingGUI.java +++ b/src/main/java/chokistream/SwingGUI.java @@ -12,10 +12,10 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; -import java.io.IOException; import java.util.EnumMap; import javax.swing.BorderFactory; @@ -758,7 +758,7 @@ public void createNTRSettings() { qos = new JTextField("Packet QoS value (Set to >100 to disable)"); add(qos, p, c, 1, 4); - JButton patch = new JButton("Patch NTR"); + JButton patch = new JButton("NFC Patch"); add(patch, p, c, 0, 5, 2, 1); patch.addActionListener(new ActionListener() { @Override @@ -773,6 +773,7 @@ public void actionPerformed(ActionEvent e) { @Override public void actionPerformed(ActionEvent e) { saveSettings(); + NTRClient.queueSettingsChange(getPropInt(Prop.QUALITY), getPropEnum(Prop.PRIORITYSCREEN), getPropInt(Prop.PRIORITYFACTOR), getPropInt(Prop.QOS)); ntrSettings.setVisible(false); } }); @@ -790,29 +791,30 @@ public void createNFCPatch() { JLabel header = new JLabel("NFC Patch"); header.setFont(new Font("System", Font.PLAIN, 20)); add(header, p, c, 0, 0, 2, 1); + add(new JLabel("What is your 3DS system update version?"), p, c, 0, 1, 2, 1); - JButton latest = new JButton(">= 11.4"); - add(latest, p, c, 0, 1); + JButton latest = new JButton("11.4 or higher"); + add(latest, p, c, 0, 2); latest.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { nfcPatch.setVisible(false); - NTRClient.sendNFCPatch(getPropString(Prop.IP), 1); + NTRClient.queueNFCPatch(NTRClient.NFCPatchType.NEW); } catch (RuntimeException ex) { displayError(ex); } } }); - JButton pre11_4 = new JButton("< 11.4"); - add(pre11_4, p, c, 1, 1); + JButton pre11_4 = new JButton("11.3 or lower"); + add(pre11_4, p, c, 1, 2); pre11_4.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { nfcPatch.setVisible(false); - NTRClient.sendNFCPatch(getPropString(Prop.IP), 0); + NTRClient.queueNFCPatch(NTRClient.NFCPatchType.OLD); } catch (RuntimeException ex) { displayError(ex); }