package com.jotuntech.sketcher.client; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Properties; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import com.jotuntech.sketcher.client.command.PingCommand; import com.jotuntech.sketcher.client.command.SignInCommand; import com.jotuntech.sketcher.client.command.SignOutCommand; import com.jotuntech.sketcher.client.voice.VoiceClient; import com.jotuntech.sketcher.client.voice.VoiceEvent; import com.jotuntech.sketcher.client.voice.VoiceListener; import com.jotuntech.sketcher.common.Brush; import com.jotuntech.sketcher.common.Canvas; import com.jotuntech.sketcher.common.Layer; import com.jotuntech.sketcher.common.Log; import com.jotuntech.sketcher.common.StreamableUtils; import com.jotuntech.sketcher.common.TwoWayHashMap; import com.jotuntech.sketcher.common.User; /** * @author Thor Harald Johansen * */ public class Client extends Thread { private UserInterface userInterface; private TwoWayHashMap userMap; private User[] userArray; private Canvas canvas; private long lastLayerClean; private Connection connection; private BlockingQueue commandQueue; private ByteBuffer commandBuffer; private String login; private String password; private String hostname; private int port; private VoiceClient voiceClient; private boolean soundEnabled; private boolean ads; //private Random sendRandom = new Random(0x707733360A596E0AL); //private Random recvRandom = new Random(0x59A70A401C8CD1EAL); private Properties props; private File propsFile; Timer propsTimer = new Timer("PropsTimer", true); private Brush defaultBrushes[] = new Brush[] { new Brush("Pen", 255, 255, 3.375f, 1, 0.27f, true, false, true, 0, 0, 0, false, 0.5f), new Brush("Pencil", 255, 255, 2f, 2, 0.45f, true, false, true, 1.5f, 0, 0, false, 0.5f), new Brush("Water", 255, 255, 16f, 4, 0.22f, true, false, true, 0, 0, 128, false, 0.5f), new Brush("Eraser", -255, 192, 16f, 4, 0.2f, false, true, false, 0, 0, 0, false, 0.5f), new Brush("Ink", 255, 255, 4f, 1, 0.04f, false, false, true, 0, 0, 0, false, 0.5f), new Brush("Wipe", -255, 255, 64f, 7, 0.2f, false, false, false, 0, 0, 0, false, 0.5f) }; protected Brush[] brushes = new Brush[defaultBrushes.length]; public Client(String hostname, int port, String login, String password, boolean ads) { super("ClientThread"); /** Save hostname and port */ this.hostname = hostname; this.port = port; /** Save login and password */ this.login = login; this.password = password; /** Save ad setting */ this.ads = ads; /** Create user map & array */ userMap = new TwoWayHashMap(); userMap.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent arg0) { /** Populate user array in a thread-safe way */ User[] newUserArray = new User[0]; newUserArray = userMap.values().toArray(newUserArray); userArray = newUserArray; } }); /** Create user array */ userArray = new User[0]; /** Create command queue */ commandQueue = new ArrayBlockingQueue(65536, true); for(int i = 0; i < brushes.length; i++) { brushes[i] = defaultBrushes[i].copy(); } /** Create user interface */ userInterface = new UserInterface(this); /** Load properties file */ try { propsFile = new File(System.getProperty("user.home"), "sketcher.properties"); } catch(SecurityException e) { propsFile = null; } loadProps(); /** Set last layer cleanup time */ lastLayerClean = System.currentTimeMillis(); propsTimer.schedule(new TimerTask() { public void run() { saveProps(); } }, 60000, 60000); } synchronized public void resetProps() { if(propsFile != null) { propsFile.delete(); } loadProps(); } synchronized private void loadProps() { props = new Properties(); if(propsFile != null) { try { if(propsFile.exists()) { FileInputStream is = new FileInputStream(propsFile); props.load(is); is.close(); } } catch(IOException e) { } catch(SecurityException e) { } } int propsVersion = Integer.valueOf(props.getProperty("file.version", "0")); soundEnabled = "true".equals(props.getProperty("sound.enabled", "true")); userInterface.getSoundItem().setSelected(soundEnabled); userInterface.setSmoothZoom("true".equals(props.getProperty("zoom.smooth", "true"))); userInterface.getEditor().setTagsEnabled("true".equals(props.getProperty("tags.enabled", "true"))); userInterface.getEditor().setSoftwareCursorEnabled("true".equals(props.getProperty("cursor.software", "false"))); userInterface.getTagsItem().setSelected(userInterface.getEditor().isTagsEnabled()); for(JCollapsiblePanel panel : userInterface.getToolPanels()) { panel.setExpanded("true".equals(props.getProperty("panel" + panel.getId() + ".expanded", panel.isExpandedByDefault() ? "true" : "false")), false); panel.setStuck("true".equals(props.getProperty("panel" + panel.getId() + ".stuck", panel.isStuckByDefault() ? "true" : "false"))); } for(int i = 0; i < brushes.length; i++) { brushes[i] = defaultBrushes[i].copy(); } for(int i = 0; i < defaultBrushes.length; i++) { String name = props.getProperty("brush" + i + ".name"); String opacity = props.getProperty("brush" + i + ".opacity"); String flow = props.getProperty("brush" + i + ".flow"); String radius = props.getProperty("brush" + i + ".radius"); String hardness = props.getProperty("brush" + i + ".hardness"); String spacing = props.getProperty("brush" + i + ".spacing"); String pressureToFlow, pressureToOpacity; if(propsVersion < 1) { pressureToFlow = props.getProperty("brush" + i + ".pressureToOpacity"); pressureToOpacity = props.getProperty("brush" + i + ".realPressureToOpacity"); } else { pressureToFlow = props.getProperty("brush" + i + ".pressureToFlow"); pressureToOpacity = props.getProperty("brush" + i + ".pressureToOpacity"); } String pressureToRadius = props.getProperty("brush" + i + ".pressureToRadius"); String jitter = props.getProperty("brush" + i + ".jitter"); String water = props.getProperty("brush" + i + ".water"); String waterArea = props.getProperty("brush" + i + ".waterArea"); String lockTransparency = props.getProperty("brush" + i + ".lockTransparency"); Brush b = brushes[i]; if(name != null) { b.setName(name); } if(opacity != null) { b.setOpacity(Integer.valueOf(opacity)); } if(flow != null) { b.setFlow(Integer.valueOf(flow)); } if(radius != null) { b.setRadius(Float.valueOf(radius)); } if(hardness != null) { b.setHardness(Integer.valueOf(hardness)); } if(spacing != null) { b.setSpacing(Float.valueOf(spacing)); } if(pressureToOpacity != null) { b.setPressureToOpacity("true".equals(pressureToOpacity)); } if(pressureToFlow != null) { b.setPressureToFlow("true".equals(pressureToFlow)); } if(pressureToRadius != null) { b.setPressureToRadius("true".equals(pressureToRadius)); } if(jitter != null) { b.setJitter(Float.valueOf(jitter)); } if(water != null) { b.setWater(Integer.valueOf(water)); } if(waterArea != null) { b.setWaterArea(Float.valueOf(waterArea)); } if(lockTransparency != null) { b.setLockTransparency("true".equals(lockTransparency)); } } } synchronized private void saveProps() { if(propsFile == null) { return; } props.setProperty("file.version", "1"); // Increase this when making format changes! props.setProperty("sound.enabled", soundEnabled ? "true" : "false"); props.setProperty("zoom.smooth", userInterface.isSmoothZoom() ? "true" : "false"); props.setProperty("tags.enabled", userInterface.getEditor().isTagsEnabled() ? "true" : "false"); props.setProperty("cursor.software", userInterface.getEditor().isSoftwareCursorEnabled() ? "true" : "false"); for(JCollapsiblePanel panel : userInterface.getToolPanels()) { props.setProperty("panel" + panel.getId() + ".expanded", panel.isExpanded() ? "true" : "false"); props.setProperty("panel" + panel.getId() + ".stuck", panel.isStuck() ? "true" : "false"); } for(int i = 0; i < brushes.length; i++) { props.setProperty("brush" + i + ".name", brushes[i].getName()); props.setProperty("brush" + i + ".opacity", String.valueOf(brushes[i].getOpacity())); props.setProperty("brush" + i + ".flow", String.valueOf(brushes[i].getFlow())); props.setProperty("brush" + i + ".radius", String.valueOf(brushes[i].getRadius())); props.setProperty("brush" + i + ".hardness", String.valueOf(brushes[i].getHardness())); props.setProperty("brush" + i + ".spacing", String.valueOf(brushes[i].getSpacing())); props.setProperty("brush" + i + ".pressureToOpacity", brushes[i].isPressureToOpacity() ? "true" : "false"); props.setProperty("brush" + i + ".pressureToFlow", brushes[i].isPressureToFlow() ? "true" : "false"); props.setProperty("brush" + i + ".pressureToRadius", brushes[i].isPressureToRadius() ? "true" : "false"); props.setProperty("brush" + i + ".jitter", String.valueOf(brushes[i].getJitter())); props.setProperty("brush" + i + ".noise", String.valueOf(brushes[i].getNoise())); props.setProperty("brush" + i + ".water", String.valueOf(brushes[i].getWater())); props.setProperty("brush" + i + ".waterArea", String.valueOf(brushes[i].getWaterArea())); props.setProperty("brush" + i + ".lockTransparency", brushes[i].isLockTransparency() ? "true" : "false"); } try { FileOutputStream os = new FileOutputStream(propsFile); props.store(os, "Sketcher Properties File"); os.close(); } catch(IOException e) { } catch(SecurityException e) { propsTimer.cancel(); } } public void close(String message) { propsTimer.cancel(); saveProps(); /** Empty the send queue */ connection.getSendQueue().clear(); /** Send sign-out command */ connection.getSendQueue().offer(new CommandEntry(0, new SignOutCommand(message))); /** Schedule connection death */ connection.setTimeOfDeath(System.currentTimeMillis() + 1000); /** Play sound */ if("true".equals(props.getProperty("sound.enabled", "true"))) { UserInterface.AUDIO_OUTTRO.play(); } } public TwoWayHashMap getUserMap() { return userMap; } public User[] getUserArray() { return userArray; } public Canvas getCanvas() { return canvas; } public void setCanvas(Canvas canvas) { this.canvas = canvas; userInterface.setCanvas(canvas); } public UserInterface getUserInterface() { return userInterface; } public Connection getConnection() { return connection; } public void run() { try { /** Create client connection */ connection = new Connection(SocketChannel.open()); userInterface.println("Connecting... "); /** Enable blocking for connect */ connection.getChannel().configureBlocking(true); /** Attempt to connect to server */ connection.getChannel().connect(new InetSocketAddress(hostname, port)); /** Disable blocking after connect */ connection.getChannel().configureBlocking(false); userInterface.print("Success!"); /** Play audio */ if("true".equals(props.getProperty("sound.enabled", "true"))) { UserInterface.AUDIO_INTRO.play(); } /** Create command buffer */ commandBuffer = ByteBuffer.allocate(65538); /** Send sign-in command */ connection.getSendQueue().offer(new CommandEntry(0, new SignInCommand(0, login, password, false))); /** Set last ping time */ connection.setLastPing(0); while(!Thread.interrupted()) { if(!connection.getChannel().isConnected()) { userInterface.println("Disconnected (Connection was closed)"); break; } else if(connection.isTimeOfDeath()) { try { connection.getChannel().close(); } catch(IOException e) { } userInterface.println("Disconnected (Exit)"); break; } if(System.currentTimeMillis() - connection.getLastPing() >= Connection.PING_INTERVAL) { long timestamp = System.currentTimeMillis(); connection.getSendQueue().offer(new CommandEntry(0, new PingCommand(timestamp))); connection.setLastPing(timestamp); } for(CommandEntry e = commandQueue.poll(20, TimeUnit.MILLISECONDS); e != null; e = commandQueue.poll(20, TimeUnit.MILLISECONDS)) { if(e.getCommand().perform(this, e.getSourceKey() == 0 ? connection.getUser() : userMap.get(e.getSourceKey())) == Connection.SEND_OTHERS) { connection.getSendQueue().offer(new CommandEntry(e.getSourceKey(), e.getCommand())); } } if(canvas != null && lastLayerClean + 30000 < System.currentTimeMillis()) { lastLayerClean = System.currentTimeMillis(); for(Layer e : canvas.getLayerMap().values()) { e.clean(); } } ByteBuffer inputBuffer = connection.getInputBuffer(); ByteBuffer outputBuffer = connection.getOutputBuffer(); ArrayBlockingQueue sendQueue = connection.getSendQueue(); //int startPosition = outputBuffer.position(); /** Only check queue if room for command length, peer key, name length, and one character of name */ while(outputBuffer.remaining() > 2 + 4 + 1 + 2 && sendQueue.size() > 0) { CommandEntry e = sendQueue.peek(); Integer k = e.getSourceKey(); Command c = e.getCommand(); /** Clear command buffer */ commandBuffer.clear(); /** Write peer key */ commandBuffer.putInt(k); /** Write command name */ String commandName = c.getClass().getSimpleName(); commandBuffer.put((byte) (commandName.length() - 1)); for(int i = 0; i < commandName.length(); i++) { commandBuffer.putChar(commandName.charAt(i)); } /** Write command */ c.encode(commandBuffer); /** Flip command buffer before reading */ commandBuffer.flip(); /** Do not send this command yet if no room in output buffer */ if(commandBuffer.remaining() + 2 > outputBuffer.remaining()) { break; } /** Write append command length and command data to output buffer */ outputBuffer.putShort((short) (commandBuffer.remaining() - 1)); outputBuffer.put(commandBuffer); /** Remove command from queue */ sendQueue.remove(); } // Primitive encryption //for(int i = startPosition; i < outputBuffer.position(); i++) { // outputBuffer.put(i, (byte) (outputBuffer.get(i) ^ sendRandom.nextInt(256))); //} /** Flip output buffer */ outputBuffer.flip(); if(outputBuffer.remaining() > 0) { connection.getChannel().write(outputBuffer); } /** Compact buffer, preparing it for appending */ outputBuffer.compact(); if(canvas == null || !canvas.isDrawing()) { try { /** Append to input buffer */ //startPosition = inputBuffer.position(); int readResult = connection.getChannel().read(inputBuffer); if(readResult == -1) { try { connection.getChannel().close(); } catch(IOException e) { } userInterface.println("Disconnected (Connection was closed)"); break; } else if(readResult > 0) { //for(int i = startPosition; i < inputBuffer.position(); i++) { // inputBuffer.put(i, (byte) (inputBuffer.get(i) ^ recvRandom.nextInt(256))); //} /** Prepare buffer for reading */ inputBuffer.flip(); /** Long enough to peek at length? */ while((canvas == null || !canvas.isDrawing()) && inputBuffer.remaining() >= 2) { /** Peek at length */ int commandLength = (inputBuffer.getShort(inputBuffer.position()) & 0xFFFF) + 1; /** Don't read if not long enough to read entire command. */ if(inputBuffer.remaining() < commandLength + 2) { break; } /** We already have the command length */ inputBuffer.getShort(); /** Read command buffer */ ByteBuffer commandBuffer = inputBuffer.slice(); commandBuffer.limit(commandLength); inputBuffer.position(inputBuffer.position() + commandLength); /** Read user key */ Integer userKey = commandBuffer.getInt(); /** Read command name */ int commandNameLength = (commandBuffer.get() & 0xFF) + 1; StringBuffer commandNameBuffer = new StringBuffer(); for(int i = 0; i < commandNameLength; i++) { commandNameBuffer.append(commandBuffer.getChar()); } String commandName = commandNameBuffer.toString(); Command command = (Command) StreamableUtils.create("com.jotuntech.sketcher.client.command." + commandName, commandBuffer); command.perform(this, userMap.get(userKey)); //++iterations; } /** Prepare input buffer for appending */ inputBuffer.compact(); } } catch (IOException e) { try { connection.getChannel().close(); } catch(IOException e2) { } userInterface.println("Disconnected (Connection was broken: " + e.getMessage() + ")"); break; } catch(Throwable t) { sendQueue.clear(); sendQueue.offer(new CommandEntry(0, new SignOutCommand("Client crashed: " + t.getClass().getSimpleName()))); connection.setTimeOfDeath(System.currentTimeMillis() + 5000); Log.error(t); userInterface.println("Disconnected (Client crashed: " + t.getClass().getSimpleName() + ")"); } } } } catch (InterruptedException e) { Log.debug("Client interrupted."); } catch (IOException e) { userInterface.println("Disconnected (Connection was broken: " + e.getMessage() + ")"); Log.error(e); } try { connection.getChannel().close(); } catch(IOException e) { } connection.setChannel(null); connection.setUser(null); if("true".equals(props.getProperty("sound.enabled", "true"))) { UserInterface.AUDIO_OUTTRO.play(); } } public BlockingQueue getCommandQueue() { return commandQueue; } public boolean isVoiceEnabled() { return voiceClient != null; } public void setVoiceEnabled(boolean voiceEnabled) { if(voiceEnabled) { final Integer peerKey = getUserMap().getKeyForValue(getConnection().getUser()); voiceClient = new VoiceClient(new InetSocketAddress(hostname, port + 100), peerKey); voiceClient.setListener(new VoiceListener() { public void voiceEvent(VoiceEvent e) { switch(e.getType()) { case VoiceEvent.TYPE_CHANNEL_NEW: Integer userKey = e.getChannel(); User user = userMap.get(userKey); if(user == null) { break; } break; case VoiceEvent.TYPE_CHANNEL_DEAD: { JUserEntry ue = userInterface.getUserList().getEntry(e.getChannel()); if(ue == null) { break; } ue.clearVoice(); break; } case VoiceEvent.TYPE_PACKET_VOLUME: { JUserEntry ue = userInterface.getUserList().getEntry(e.getChannel()); if(ue == null) { break; } if(e.getChannel() == peerKey) { ue.setVolume(e.getVolume(), 0); } else { ue.setVolume(e.getVolume(), 12); } break; } } } }); voiceClient.start(); } else { voiceClient.interrupt(); try { voiceClient.join(); } catch (InterruptedException e) { } voiceClient = null; userInterface.getUserList().clearVoice(); } } public VoiceClient getVoiceClient() { return voiceClient; } public Properties getProps() { return props; } public void setSoundEnabled(boolean soundEnabled) { this.soundEnabled = soundEnabled; } public boolean isSoundEnabled() { return soundEnabled; } public void setAds(boolean ads) { this.ads = ads; } public boolean isAds() { return ads; } public String getLogin() { return login; } public String getPassword() { return password; } public String getHostname() { return hostname; } }