/* This file is part of jpcsp. Jpcsp is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Jpcsp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.network.proonline; import static jpcsp.HLE.modules.sceNetAdhoc.isSameMacAddress; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.LinkedList; import java.util.List; import jpcsp.Emulator; import jpcsp.GUI.ChatGUI; import jpcsp.HLE.Modules; import jpcsp.HLE.kernel.types.pspNetMacAddress; import jpcsp.HLE.modules.sceNet; import jpcsp.HLE.modules.sceNetAdhoc; import jpcsp.HLE.modules.sceNetApctl; import jpcsp.HLE.modules.sceNetAdhoc.GameModeArea; import jpcsp.network.BaseNetworkAdapter; import jpcsp.network.INetworkAdapter; import jpcsp.network.adhoc.AdhocMatchingEventMessage; import jpcsp.network.adhoc.AdhocMessage; import jpcsp.network.adhoc.MatchingObject; import jpcsp.network.adhoc.PdpObject; import jpcsp.network.adhoc.PtpObject; import jpcsp.network.proonline.PacketFactory.SceNetAdhocctlPacketBaseC2S; import jpcsp.network.proonline.PacketFactory.SceNetAdhocctlPacketBaseS2C; import jpcsp.network.upnp.UPnP; import jpcsp.settings.AbstractBoolSettingsListener; import jpcsp.settings.AbstractIntSettingsListener; import jpcsp.settings.AbstractStringSettingsListener; import jpcsp.settings.Settings; import jpcsp.util.Utilities; import org.apache.log4j.Logger; /** * @author gid15 * */ public class ProOnlineNetworkAdapter extends BaseNetworkAdapter { protected static Logger log = Logger.getLogger("ProOnline"); private static boolean enabled = false; private UPnP upnp; private Socket metaSocket; private static int metaPort = 27312; private static String metaServer = "coldbird.net"; private static final int pingTimeoutMillis = 2000; private volatile boolean exit; private volatile boolean friendFinderActive; // All access to macIps have to be synchronized because they can be executed // from different threads (PSP thread + Friend Finder thread). private List<MacIp> macIps = new LinkedList<MacIp>(); private PacketFactory packetFactory = new PacketFactory(); private PortManager portManager; private InetAddress broadcastInetAddress; private InetAddress loopbackInetAddress; private InetAddress localHostInetAddress; private ChatGUI chatGUI; private boolean connectComplete; private static class MetaServerSettingsListener extends AbstractStringSettingsListener { @Override protected void settingsValueChanged(String value) { metaServer = value; } } private static class MetaServerPortSettingsListener extends AbstractIntSettingsListener { @Override protected void settingsValueChanged(int value) { metaPort = value; } } private static class EnabledSettingsListener extends AbstractBoolSettingsListener { @Override protected void settingsValueChanged(boolean value) { setEnabled(value); } } public static boolean isEnabled() { return enabled; } public static void setEnabled(boolean enabled) { ProOnlineNetworkAdapter.enabled = enabled; if (enabled) { log.info("Enabling ProOnline network"); } } public static String getMetaServer() { return metaServer; } protected class FriendFinder extends Thread { @Override public void run() { friendFinder(); } } @Override public void start() { super.start(); log.info(String.format("ProOnline start, server %s:%d", metaServer, metaPort)); try { broadcastInetAddress = InetAddress.getByAddress(new byte[] { 1, 1, 1, 1 }); } catch (UnknownHostException e) { log.error("Unable to set the broadcast address", e); } try { loopbackInetAddress = InetAddress.getByName("localhost"); } catch (UnknownHostException e) { log.error("Unable to set the loopback address", e); } try { localHostInetAddress = InetAddress.getByName(sceNetApctl.getLocalHostIP()); } catch (UnknownHostException e) { log.error("Unable to set the local address", e); } upnp = new UPnP(); upnp.discoverInBackground(); } protected void sendToMetaServer(SceNetAdhocctlPacketBaseC2S packet) throws IOException { if (metaSocket != null) { metaSocket.getOutputStream().write(packet.getBytes()); metaSocket.getOutputStream().flush(); if (log.isTraceEnabled()) { log.trace(String.format("Sent packet to meta server: %s", packet)); } } else { if (log.isDebugEnabled()) { log.debug(String.format("Message not sent to meta server because not connected: %s", packet)); } } } protected void safeSendToMetaServer(SceNetAdhocctlPacketBaseC2S packet) { try { sendToMetaServer(packet); } catch (IOException e) { // Ignore exception } } private void openChat() { if (chatGUI == null || !chatGUI.isVisible()) { chatGUI = new ChatGUI(); Emulator.getMainGUI().startBackgroundWindowDialog(chatGUI); chatGUI.updateMembers(Modules.sceNetAdhocctlModule.getPeersNickName()); } } private void closeChat() { if (chatGUI != null) { chatGUI.dispose(); chatGUI = null; } } private void waitForFriendFinderToExit() { while (friendFinderActive && exit) { Utilities.sleep(1, 0); } } @Override public void sceNetAdhocctlInit() { if (log.isDebugEnabled()) { log.debug("sceNetAdhocctlInit"); } // Wait for a previous instance of the Friend Finder thread to terminate waitForFriendFinderToExit(); terminatePortManager(); closeConnectionToMetaServer(); connectToMetaServer(); exit = false; portManager = new PortManager(upnp); if (metaSocket != null) { Thread friendFinderThread = new FriendFinder(); friendFinderThread.setName("ProOnline Friend Finder"); friendFinderThread.setDaemon(true); friendFinderThread.start(); } } @Override public void sceNetAdhocctlConnect() { if (log.isDebugEnabled()) { log.debug("sceNetAdhocctlConnect redirecting to sceNetAdhocctlCreate"); } sceNetAdhocctlCreate(); } @Override public void sceNetAdhocctlCreate() { if (log.isDebugEnabled()) { log.debug("sceNetAdhocctlCreate"); } try { sendToMetaServer(new PacketFactory.SceNetAdhocctlConnectPacketC2S(this)); openChat(); } catch (IOException e) { log.error("sceNetAdhocctlCreate", e); } } @Override public void sceNetAdhocctlJoin() { if (log.isDebugEnabled()) { log.debug("sceNetAdhocctlJoin redirecting to sceNetAdhocctlCreate"); } sceNetAdhocctlCreate(); } @Override public void sceNetAdhocctlDisconnect() { if (log.isDebugEnabled()) { log.debug("sceNetAdhocctlDisconnect"); } try { sendToMetaServer(new PacketFactory.SceNetAdhocctlDisconnectPacketC2S(this)); setConnectComplete(false); deleteAllFriends(); closeChat(); } catch (IOException e) { log.error("sceNetAdhocctlDisconnect", e); } } @Override public void sceNetAdhocctlTerm() { if (log.isDebugEnabled()) { log.debug("sceNetAdhocctlTerm"); } exit = true; terminatePortManager(); } @Override public void sceNetAdhocctlScan() { if (log.isDebugEnabled()) { log.debug("sceNetAdhocctlScan"); } try { sendToMetaServer(new PacketFactory.SceNetAdhocctlScanPacketC2S(this)); } catch (IOException e) { log.error("sceNetAdhocctlScan", e); } } public void sceNetPortOpen(String protocol, int port) { if (log.isDebugEnabled()) { log.debug(String.format("sceNetPortOpen %s, port=%d", protocol, port)); } portManager.addPort(port, protocol); } public void sceNetPortClose(String protocol, int port) { if (log.isDebugEnabled()) { log.debug(String.format("sceNetPortClose %s, port=%d", protocol, port)); } portManager.removePort(port, protocol); } private void connectToMetaServer() { try { metaSocket = new Socket(metaServer, metaPort); metaSocket.setReuseAddress(true); metaSocket.setSoTimeout(500); PacketFactory.SceNetAdhocctlLoginPacketC2S loginPacket = new PacketFactory.SceNetAdhocctlLoginPacketC2S(this); sendToMetaServer(loginPacket); } catch (UnknownHostException e) { log.error(String.format("Could not connect to meta server %s:%d", metaServer, metaPort), e); } catch (IOException e) { log.error(String.format("Could not connect to meta server %s:%d", metaServer, metaPort), e); } } /** * Delete all the port/host mappings */ private void terminatePortManager() { if (portManager != null) { portManager.clear(); portManager = null; } } private void closeConnectionToMetaServer() { if (metaSocket != null) { try { metaSocket.close(); } catch (IOException e) { log.error("friendFinder", e); } metaSocket = null; } } protected void friendFinder() { long lastPing = Emulator.getClock().currentTimeMillis(); byte[] buffer = new byte[1024]; int offset = 0; if (log.isDebugEnabled()) { log.debug("Starting friendFinder"); } friendFinderActive = true; while (!exit) { long now = Emulator.getClock().currentTimeMillis(); if (now - lastPing >= pingTimeoutMillis) { lastPing = now; safeSendToMetaServer(new PacketFactory.SceNetAdhocctlPingPacketC2S(this)); } try { int length = metaSocket.getInputStream().read(buffer, offset, buffer.length - offset); if (length > 0) { offset += length; } else if (length < 0) { // The connection has been closed by the server, try to reconnect... closeConnectionToMetaServer(); connectToMetaServer(); } } catch (SocketTimeoutException e) { // Ignore read timeout } catch (IOException e) { log.error("friendFinder", e); } if (offset > 0) { if (log.isTraceEnabled()) { log.trace(String.format("Received from meta server: OPCODE %d", buffer[0])); } int consumed = 0; SceNetAdhocctlPacketBaseS2C packet = packetFactory.createPacketS2C(this, buffer, offset); if (packet == null) { // Skip the unknown opcode consumed = 1; } else if (offset >= packet.getLength()) { if (log.isDebugEnabled()) { log.debug(String.format("Incoming server packet %s", packet)); } packet.process(); consumed = packet.getLength(); } if (consumed > 0) { System.arraycopy(buffer, consumed, buffer, 0, offset - consumed); offset -= consumed; } } } if (log.isDebugEnabled()) { log.debug("Exiting friendFinder"); } // Be clean, send a disconnect message to the server try { sendToMetaServer(new PacketFactory.SceNetAdhocctlDisconnectPacketC2S(this)); } catch (IOException e) { // Ignore error } closeConnectionToMetaServer(); exit = false; friendFinderActive = false; } public static String convertIpToString(int ip) { return String.format("%d.%d.%d.%d", ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, (ip >> 24) & 0xFF); } protected synchronized void addFriend(String nickName, pspNetMacAddress mac, int ip) { if (log.isDebugEnabled()) { log.debug(String.format("Adding friend nickName='%s', mac=%s, ip=%s", nickName, mac, convertIpToString(ip))); } Modules.sceNetAdhocctlModule.hleNetAdhocctlAddPeer(nickName, mac); if (chatGUI != null) { chatGUI.updateMembers(Modules.sceNetAdhocctlModule.getPeersNickName()); } boolean found = false; for (MacIp macIp : macIps) { if (mac.equals(macIp.mac)) { macIp.setIp(ip); found = true; break; } } if (!found) { MacIp macIp = new MacIp(mac.macAddress, ip); macIps.add(macIp); portManager.addHost(convertIpToString(ip)); } } protected synchronized void deleteFriend(int ip) { if (log.isDebugEnabled()) { log.debug(String.format("Deleting friend ip=%s", convertIpToString(ip))); } for (MacIp macIp : macIps) { if (macIp.ip == ip) { // Delete the MacIp mapping macIps.remove(macIp); // Delete the peer Modules.sceNetAdhocctlModule.hleNetAdhocctlDeletePeer(macIp.mac); // Delete the router ports portManager.removeHost(convertIpToString(ip)); // Delete the nickName from the Chat members if (chatGUI != null) { chatGUI.updateMembers(Modules.sceNetAdhocctlModule.getPeersNickName()); } break; } } } protected synchronized void deleteAllFriends() { while (!macIps.isEmpty()) { MacIp macIp = macIps.get(0); deleteFriend(macIp.ip); } } public boolean isBroadcast(SocketAddress socketAddress) { if (socketAddress instanceof InetSocketAddress) { InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; return inetSocketAddress.getAddress().equals(broadcastInetAddress); } return false; } public int getBroadcastPort(SocketAddress socketAddress) { if (socketAddress instanceof InetSocketAddress) { InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; if (inetSocketAddress.getAddress().equals(broadcastInetAddress)) { return inetSocketAddress.getPort(); } } return -1; } @Override public SocketAddress getSocketAddress(byte[] macAddress, int realPort) throws UnknownHostException { InetAddress inetAddress = getInetAddress(macAddress); if (inetAddress == null && sceNetAdhoc.isMyMacAddress(macAddress)) { inetAddress = localHostInetAddress; } if (inetAddress == null) { throw new UnknownHostException(String.format("ProOnline: unknown MAC address %s", sceNet.convertMacAddressToString(macAddress))); } return new InetSocketAddress(inetAddress, realPort); } public synchronized int getNumberMacIps() { return macIps.size(); } public synchronized MacIp getMacIp(int index) { if (index < 0 || index >= macIps.size()) { return null; } return macIps.get(index); } public InetAddress getInetAddress(byte[] macAddress) { if (sceNetAdhoc.isAnyMacAddress(macAddress)) { return broadcastInetAddress; } MacIp macIp = getMacIp(macAddress); if (macIp == null) { return null; } return macIp.inetAddress; } public int getIp(byte[] macAddress) { MacIp macIp = getMacIp(macAddress); if (macIp == null) { return 0; } return macIp.ip; } public synchronized MacIp getMacIp(byte[] macAddress) { for (MacIp macIp : macIps) { if (isSameMacAddress(macAddress, macIp.mac)) { return macIp; } } return null; } private boolean isLocalInetAddress(InetAddress inetAddress) { return inetAddress.equals(loopbackInetAddress) || inetAddress.equals(localHostInetAddress); } public synchronized MacIp getMacIp(InetAddress inetAddress) { for (MacIp macIp : macIps) { if (inetAddress.equals(macIp.inetAddress)) { return macIp; } // When using 2 instances of Jpcsp on the local machine if (isLocalInetAddress(inetAddress) && isLocalInetAddress(macIp.inetAddress)) { return macIp; } } return null; } public byte[] getMacAddress(InetAddress inetAddress) { MacIp macIp = getMacIp(inetAddress); if (macIp == null) { return null; } return macIp.mac; } @Override public PdpObject createPdpObject() { return new ProOnlinePdpObject(this); } @Override public PtpObject createPtpObject() { return new ProOnlinePtpObject(this); } @Override public AdhocMessage createAdhocPdpMessage(int address, int length, byte[] destMacAddress) { return new ProOnlineAdhocMessage(this, address, length, destMacAddress); } @Override public AdhocMessage createAdhocPdpMessage(byte[] message, int length) { return new ProOnlineAdhocMessage(this, message, length); } @Override public AdhocMessage createAdhocPtpMessage(int address, int length) { return new ProOnlineAdhocMessage(this, address, length); } @Override public AdhocMessage createAdhocPtpMessage(byte[] message, int length) { return new ProOnlineAdhocMessage(this, message, length); } @Override public AdhocMessage createAdhocGameModeMessage(GameModeArea gameModeArea) { log.error("Adhoc GameMode not supported by ProOnline"); return null; } @Override public AdhocMessage createAdhocGameModeMessage(byte[] message, int length) { log.error("Adhoc GameMode not supported by ProOnline"); return null; } @Override public MatchingObject createMatchingObject() { return new ProOnlineMatchingObject(this); } @Override public AdhocMatchingEventMessage createAdhocMatchingEventMessage(MatchingObject matchingObject, int event) { return MatchingPacketFactory.createPacket(this, matchingObject, event); } @Override public AdhocMatchingEventMessage createAdhocMatchingEventMessage(MatchingObject matchingObject, int event, int data, int dataLength, byte[] macAddress) { return MatchingPacketFactory.createPacket(this, matchingObject, event, data, dataLength, macAddress); } @Override public AdhocMatchingEventMessage createAdhocMatchingEventMessage(MatchingObject matchingObject, byte[] message, int length) { return MatchingPacketFactory.createPacket(this, matchingObject, message, length); } public boolean isForMe(AdhocMessage adhocMessage, int port, InetAddress address) { byte[] fromMacAddress = getMacAddress(address); if (fromMacAddress == null) { if (log.isDebugEnabled()) { log.debug(String.format("not for me: port=%d, address=%s, message=%s", port, address, adhocMessage)); } // Unknown source IP address, ignore the message return false; } // Copy the source MAC address from the source InetAddress adhocMessage.setFromMacAddress(fromMacAddress); // There is no broadcasting, all messages are for me return true; } @Override public void sendChatMessage(String message) { if (log.isDebugEnabled()) { log.debug(String.format("Sending chat message '%s'", message)); } try { sendToMetaServer(new PacketFactory.SceNetAdhocctlChatPacketC2S(this, message)); } catch (IOException e) { log.warn("Error while sending chat message", e); } } public void displayChatMessage(String nickName, String message) { if (log.isDebugEnabled()) { log.debug(String.format("Displaying chat message from '%s': '%s'", nickName, message)); } chatGUI.addChatMessage(nickName, message); } public static void init() { Settings.getInstance().registerSettingsListener("ProOnline", "emu.enableProOnline", new EnabledSettingsListener()); Settings.getInstance().registerSettingsListener("ProOnline", "network.ProOnline.metaServer", new MetaServerSettingsListener()); Settings.getInstance().registerSettingsListener("ProOnline", "network.ProOnline.metaPort", new MetaServerPortSettingsListener()); } public static void exit() { Settings.getInstance().removeSettingsListener("ProOnline"); if (!isEnabled()) { return; } INetworkAdapter networkAdapter = Modules.sceNetModule.getNetworkAdapter(); if (networkAdapter == null || !(networkAdapter instanceof ProOnlineNetworkAdapter)) { return; } ProOnlineNetworkAdapter proOnline = (ProOnlineNetworkAdapter) networkAdapter; proOnline.exit = true; proOnline.terminatePortManager(); proOnline.waitForFriendFinderToExit(); } @Override public boolean isConnectComplete() { return connectComplete; } public void setConnectComplete(boolean connectComplete) { this.connectComplete = connectComplete; } @Override public void updatePeers() { // Nothing to do } }