//////////////////////////////////////////////////////////////////////////////////////// // // Nestopia - NES/Famicom emulator written in C++ // // Copyright (C) 2003-2008 Martin Freij // // This file is part of Nestopia. // // Nestopia 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 2 of the License, or // (at your option) any later version. // // Nestopia 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 Nestopia; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // //////////////////////////////////////////////////////////////////////////////////////// #include "NstIoLog.hpp" #include "NstIoScreen.hpp" #include "NstSystemDll.hpp" #include "NstResourceString.hpp" #include "NstWindowParam.hpp" #include "NstSystemThread.hpp" #include "NstManagerPaths.hpp" #include "NstDialogNetplay.hpp" #include "NstManagerNetplay.hpp" #include "../kaillera/kailleraclient.h" #include "../core/api/NstApiMachine.hpp" #include "../core/api/NstApiFds.hpp" #include #ifndef WM_THEMECHANGED #define WM_THEMECHANGED WM_NULL #endif namespace Nestopia { namespace Managers { class Netplay::Dll : System::Dll { typedef int (WINAPI *GetVersionFunc)(char*); typedef int (WINAPI *InitFunc)(); typedef int (WINAPI *ShutdownFunc)(); typedef int (WINAPI *SetInfosFunc)(kailleraInfos*); typedef int (WINAPI *SelectServerDialogFunc)(HWND); typedef int (WINAPI *ModifyPlayValuesFunc)(void*,int); typedef int (WINAPI *ChatSendFunc)(char*); typedef int (WINAPI *EndGameFunc)(); InitFunc const Init; ShutdownFunc const Shutdown; public: using System::Dll::operator !; GetVersionFunc const GetVersion; SetInfosFunc const SetInfos; SelectServerDialogFunc const SelectServerDialog; ModifyPlayValuesFunc const ModifyPlayValues; ChatSendFunc const ChatSend; EndGameFunc const EndGame; Dll() : System::Dll (L"kailleraclient.dll"), Init (Fetch< InitFunc >( "_kailleraInit@0" )), Shutdown (Fetch< ShutdownFunc >( "_kailleraShutdown@0" )), GetVersion (Fetch< GetVersionFunc >( "_kailleraGetVersion@4" )), SetInfos (Fetch< SetInfosFunc >( "_kailleraSetInfos@4" )), SelectServerDialog (Fetch< SelectServerDialogFunc >( "_kailleraSelectServerDialog@4" )), ModifyPlayValues (Fetch< ModifyPlayValuesFunc >( "_kailleraModifyPlayValues@8" )), ChatSend (Fetch< ChatSendFunc >( "_kailleraChatSend@4" )), EndGame (Fetch< EndGameFunc >( "_kailleraEndGame@0" )) { if ( GetVersion && Init && Shutdown && SetInfos && SelectServerDialog && ModifyPlayValues && ChatSend && EndGame ) Init(); else Unload(); } ~Dll() { if (*this) Shutdown(); } }; class Netplay::Kaillera : Manager { public: Kaillera(Emulator&,Window::Menu&,const Paths&,Window::Custom&,bool); ~Kaillera(); enum Exception { ERR_LOAD }; void ToggleConnection(); void Chat(); bool Close() const; private: enum { MAX_PLAYERS = 8, MASTER = 1, WM_NST_OPEN_CLIENT = WM_APP + 57, WM_NST_CLOSE_CLIENT = WM_APP + 58, WM_NST_START_GAME = WM_APP + 59 }; class Command { enum { PACKET_TYPE = 0x0F, PACKET_DATA = 0xF0, PACKET_DATA_REGION_PAL = 0x01, PACKET_DATA_ADAPTER_FAMICOM = 0x02, PACKET_DATA_SHIFT = 4, PACKET_STARTUP = 1, PACKET_RESET = 2, PACKET_INSERT_DISK = 3, PACKET_EJECT_DISK = 4, PACKET_INSERT_COIN = 5 }; uint command; struct { Nes::Input::UserData data; Nes::Input::Controllers::VsSystem::PollCallback code; } coinCallback; struct { bool regionPal; bool adapterFamicom; bool unlimSprites; } settings; public: void Begin() { if (Nes::Machine(instance->emulator).Is(Nes::Machine::VS)) { Nes::Input::Controllers::VsSystem::callback.Get( coinCallback.code, coinCallback.data ); Nes::Input::Controllers::VsSystem::callback.Unset(); } else { coinCallback.code = NULL; coinCallback.data = NULL; } settings.regionPal = (Nes::Machine(instance->emulator).GetMode() == Nes::Machine::PAL); settings.adapterFamicom = (Nes::Input(instance->emulator).GetConnectedAdapter() == Nes::Input::ADAPTER_FAMICOM); settings.unlimSprites = Nes::Video(instance->emulator).AreUnlimSpritesEnabled(); Nes::Video(instance->emulator).EnableUnlimSprites( false ); if (instance->network.player == MASTER) { command = PACKET_STARTUP; if (settings.regionPal) command |= uint(PACKET_DATA_REGION_PAL) << PACKET_DATA_SHIFT; if (settings.adapterFamicom) command |= uint(PACKET_DATA_ADAPTER_FAMICOM) << PACKET_DATA_SHIFT; } else { command = 0; } } void Send(Emulator::Command input,uint state) { NST_COMPILE_ASSERT( Emulator::NUM_COMMANDS == 3 ); NST_VERIFY( command == 0 ); if (command == 0 && instance->network.player == MASTER) { switch (input) { case Emulator::COMMAND_RESET: command = PACKET_RESET | (state & 0x1) << PACKET_DATA_SHIFT; break; case Emulator::COMMAND_DISK_INSERT: command = PACKET_INSERT_DISK | state << PACKET_DATA_SHIFT; break; case Emulator::COMMAND_DISK_EJECT: command = PACKET_EJECT_DISK; break; } } } NST_FORCE_INLINE uint GetCode() { if (instance->network.player != MASTER) return 0; if (coinCallback.code) { Nes::Input::Controllers::VsSystem vs; coinCallback.code( coinCallback.data, vs ); if (vs.insertCoin & (Nes::Input::Controllers::VsSystem::COIN_1|Nes::Input::Controllers::VsSystem::COIN_2)) { return PACKET_INSERT_COIN | ( ((vs.insertCoin & Nes::Input::Controllers::VsSystem::COIN_1) ? (0x1U << PACKET_DATA_SHIFT) : 0) | ((vs.insertCoin & Nes::Input::Controllers::VsSystem::COIN_2) ? (0x2U << PACKET_DATA_SHIFT) : 0) ); } } const uint code = command; command = 0; return code; } NST_FORCE_INLINE bool Dispatch(const uint packet,Nes::Input::Controllers& controllers) { if (packet) { const uint data = packet >> PACKET_DATA_SHIFT; switch (packet & PACKET_TYPE) { case PACKET_INSERT_COIN: controllers.vsSystem.insertCoin = ( ((data & 0x1) ? Nes::Input::Controllers::VsSystem::COIN_1 : 0U) | ((data & 0x2) ? Nes::Input::Controllers::VsSystem::COIN_2 : 0U) ); break; case PACKET_RESET: Nes::Machine(instance->emulator).Reset( data & 0x1 ); break; case PACKET_INSERT_DISK: Nes::Fds(instance->emulator).InsertDisk( data / 2, data % 2 ); break; case PACKET_EJECT_DISK: Nes::Fds(instance->emulator).EjectDisk(); break; case PACKET_STARTUP: Nes::Machine(instance->emulator).SetMode( (data & PACKET_DATA_REGION_PAL) ? Nes::Machine::PAL : Nes::Machine::NTSC ); Nes::Input(instance->emulator).ConnectAdapter( (data & PACKET_DATA_ADAPTER_FAMICOM) ? Nes::Input::ADAPTER_FAMICOM : Nes::Input::ADAPTER_NES ); break; default: NST_DEBUG_MSG("unknown netplay package"); break; } return true; } return false; } void End() { if (coinCallback.code) { Nes::Input::Controllers::VsSystem::callback.Set( coinCallback.code, coinCallback.data ); coinCallback.code = NULL; coinCallback.data = NULL; } Nes::Machine(instance->emulator).SetMode( settings.regionPal ? Nes::Machine::PAL : Nes::Machine::NTSC ); Nes::Input(instance->emulator).ConnectAdapter( settings.adapterFamicom ? Nes::Input::ADAPTER_FAMICOM : Nes::Input::ADAPTER_NES ); Nes::Video(instance->emulator).EnableUnlimSprites( settings.unlimSprites ); } }; class Input { struct { Nes::Input::UserData data; Nes::Input::Controllers::Pad::PollCallback code; } pollCallback; public: void Capture() { Nes::Input::Controllers::Pad::callback.Get( pollCallback.code, pollCallback.data ); Nes::Input::Controllers::Pad::callback.Unset(); NST_ASSERT( pollCallback.code ); } NST_FORCE_INLINE uint GetCode() const { uint index = instance->network.player - 1U; if (index < 4) { index = Nes::Input(instance->emulator).GetConnectedController(index) - uint(Nes::Input::PAD1); NST_VERIFY( index < 4 ); if (index < 4) { Nes::Input::Controllers::Pad pad; pollCallback.code( pollCallback.data, pad, index ); return pad.buttons; } } return 0; } NST_FORCE_INLINE void Dispatch(uint port,uint packet,Nes::Input::Controllers& controllers) const { NST_ASSERT( port < 4 ); uint index = Nes::Input(instance->emulator).GetConnectedController(port) - uint(Nes::Input::PAD1); NST_VERIFY( index < 4 ); if (index < 4) controllers.pad[index].buttons = packet; } void Release() const { Nes::Input::Controllers::Pad::callback.Set( pollCallback.code, pollCallback.data ); } }; friend class Command; friend class Input; struct Callbacks; class Client; void Disconnect(); void StartNetwork(System::Thread::Terminator); ibool OnOpenClient (Window::Param&); ibool OnCloseClient (Window::Param&); ibool OnStartGame (Window::Param&); ibool OnEnable (Window::Param&); void OnEmuFrame (Nes::Input::Controllers&); void OnEmuCommand (Emulator::Command,Emulator::Data); void OnEmuEvent (Emulator::Event,Emulator::Data); const Dll dll; Window::Custom& window; Window::Netplay::Chat chat; Window::Netplay dialog; System::Thread thread; Window::MsgHandler::Callback enableCallback; struct { bool connected; Command command; Input input; uint player; uint players; Path game; } network; static Kaillera* instance; public: bool ShouldGoFullscreen() const { return dialog.ShouldGoFullscreen(); } void SaveFile() const { dialog.SaveFile(); } }; struct Netplay::Kaillera::Callbacks { static int WINAPI Start(char* game,int player,int players) { if (game && *game && player > 0 && players > 0 && player-1U < players) { instance->network.game = game; instance->network.player = player; instance->network.players = players; instance->window.Post( WM_NST_START_GAME ); } else { instance->dll.EndGame(); NST_DEBUG_MSG("Kaillera::Start() failed!"); } return 0; } static void WINAPI ClientDrop(char* nick,int playerNum) { static const HeapString player( HeapString() << Resource::String(IDS_TEXT_PLAYER) << ' ' ); static const HeapString droppedOut( HeapString() << ") " << Resource::String(IDS_TEXT_DROPPEDOUT) ); if (nick && *nick) Io::Screen() << player << playerNum << " (" << HeapString().Import(nick) << droppedOut; } static void WINAPI ChatRecieve(char* nick,char* text) { static const HeapString says( HeapString() << ' ' << Resource::String(IDS_TEXT_SAYS) << ": " ); if (nick && *nick && text && *text) Io::Screen() << HeapString().Import(nick) << says << HeapString().Import(text); } }; class Netplay::Kaillera::Client { // Uses a hook for monitoring the Kaillera windows activity. // The bug seems to be located in the Kaillera code so I have // to resolve to some dirty hacks to prevent the message queue // from entering an infinite loop. This will happen if the user // closes the main server list window while others are open. struct Instance { HHOOK hHook; DWORD threadId; Instance() : hHook(NULL) {} }; DWORD visualStyles; static Instance instance; class Callbacks { static NST_NO_INLINE bool IsKaillera(const Window::Generic window) { HeapString name; window.Text() >> name; return ( (name.Length() >= 8 && name(0,8) == L"Kaillera" ) || (name.Length() >= 6 && name(0,6) == L"Anti3D" ) ); } public: static BOOL CALLBACK Destroy(HWND hWnd,LPARAM) { if (IsKaillera( hWnd )) ::SendMessage( hWnd, WM_SYSCOMMAND, SC_CLOSE, 0 ); return true; } static BOOL CALLBACK Find(HWND hWnd,LPARAM lParam) { if (::GetParent( hWnd ) && IsKaillera( hWnd )) { *reinterpret_cast(lParam) = hWnd; return false; } return true; } static BOOL CALLBACK Show(HWND hWnd,LPARAM lParam) { if (IsKaillera( hWnd )) ::ShowWindow( hWnd, lParam ); return true; } }; template static void Enumerate(BOOL (CALLBACK* callback)(HWND,LPARAM),T t) { ::EnumThreadWindows( instance.threadId, callback, LPARAM(t) ); } static bool IsZombie(HWND hWnd) { if (!::GetParent( hWnd )) { hWnd = NULL; Enumerate( Callbacks::Find, &hWnd ); if (hWnd) return true; } return false; } static LRESULT CALLBACK MessageSpy(int iCode,WPARAM wParam,LPARAM lParam) { if (iCode == HC_ACTION) { MSG& msg = *reinterpret_cast(lParam); if (msg.message == WM_CLOSE && IsZombie( msg.hwnd )) msg.message = WM_NULL; } return ::CallNextHookEx( instance.hHook, iCode, wParam, lParam ); } void DisableVisualStyles() { // Kaillera doesn't like XP Visual Styles struct ComCtl32 { static bool IsVersion6() { const System::Dll comctl32( L"comctl32.dll" ); if (DLLGETVERSIONPROC const getVersion = comctl32.Fetch("DllGetVersion")) { DLLVERSIONINFO info; info.cbSize = sizeof(info); return getVersion( &info ) == NOERROR && info.dwMajorVersion >= 6; } return false; } }; visualStyles = 0; static const bool isVersion6 = ComCtl32::IsVersion6(); if (isVersion6) { const System::Dll uxtheme( L"uxtheme.dll" ); typedef DWORD (STDAPICALLTYPE* GetProperty)(); typedef void (STDAPICALLTYPE* SetProperty)(DWORD); if (GetProperty const getProperty = uxtheme.Fetch("GetThemeAppProperties")) { visualStyles = getProperty(); if (visualStyles) { if (SetProperty const setProperty = uxtheme.Fetch("SetThemeAppProperties")) { setProperty( 0 ); Application::Instance::GetMainWindow().Post( WM_THEMECHANGED ); } } } } } void RestoreVisualStyles() { if (visualStyles) { const System::Dll uxtheme( L"uxtheme.dll" ); typedef void (STDAPICALLTYPE* SetProperty)(DWORD); if (SetProperty const setProperty = uxtheme.Fetch("SetThemeAppProperties")) { setProperty( visualStyles ); visualStyles = 0; Application::Instance::GetMainWindow().Post( WM_THEMECHANGED ); } } } public: NST_NO_INLINE Client() { NST_ASSERT( !instance.hHook ); instance.threadId = ::GetCurrentThreadId(); instance.hHook = ::SetWindowsHookEx ( WH_GETMESSAGE, MessageSpy, ::GetModuleHandle(NULL), instance.threadId ); if (!instance.hHook) throw "SetWindowsHookEx() failed!"; Kaillera::instance->window.Post( WM_NST_OPEN_CLIENT ); DisableVisualStyles(); } static void Run() { Kaillera::instance->dll.SelectServerDialog( NULL ); } static void Show() { if (instance.hHook) Enumerate( Callbacks::Show, SW_SHOW ); } static void Hide() { if (instance.hHook) Enumerate( Callbacks::Show, SW_HIDE ); } static void Close() { if (instance.hHook) Enumerate( Callbacks::Destroy, 0 ); } NST_NO_INLINE ~Client() { NST_ASSERT( instance.hHook ); ::UnhookWindowsHookEx( instance.hHook ); instance.hHook = NULL; RestoreVisualStyles(); Kaillera::instance->window.Post( WM_NST_CLOSE_CLIENT ); } }; Netplay::Kaillera* Netplay::Kaillera::instance = NULL; Netplay::Kaillera::Client::Instance Netplay::Kaillera::Client::instance; Netplay::Kaillera::Kaillera ( Emulator& e, Window::Menu& m, const Paths& paths, Window::Custom& w, const bool doFullscreen ) : Manager ( e, m, this, &Kaillera::OnEmuEvent ), window ( w ), chat ( dll.ChatSend ), dialog ( e, paths, doFullscreen ) { if (!dll) throw ERR_LOAD; NST_ASSERT( instance == NULL ); instance = this; } Netplay::Kaillera::~Kaillera() { instance = NULL; } void Netplay::Kaillera::StartNetwork(System::Thread::Terminator) { Client().Run(); } ibool Netplay::Kaillera::OnOpenClient(Window::Param&) { network.input.Capture(); emulator.BeginNetplayMode(); return true; } ibool Netplay::Kaillera::OnCloseClient(Window::Param&) { network.input.Release(); window.Messages().Remove( this ); emulator.EndNetplayMode(); return true; } bool Netplay::Kaillera::Close() const { if (emulator.NetPlayers()) { if (emulator.IsImage()) emulator.Unload(); else Client::Close(); return false; } return true; } ibool Netplay::Kaillera::OnStartGame(Window::Param&) { emulator.StartNetplay ( this, &Kaillera::OnEmuFrame, &Kaillera::OnEmuCommand, network.player, network.players ); Application::Instance::GetMainWindow().Send ( Application::Instance::WM_NST_LAUNCH, Paths::File::GAME|Paths::File::ARCHIVE, dialog.GetPath(network.game.Ptr()) ); if (!emulator.IsOn()) { emulator.StopNetplay(); dll.EndGame(); } return true; } #ifdef NST_MSVC_OPTIMIZE #pragma optimize("t", on) #endif void Netplay::Kaillera::OnEmuFrame(Nes::Input::Controllers& controllers) { controllers.vsSystem.insertCoin = 0; if (network.connected) { uchar packets[MAX_PLAYERS][2] = {{0},{0}}; packets[0][0] = network.input.GetCode(); packets[0][1] = network.command.GetCode(); if (dll.ModifyPlayValues( packets, 2 ) != -1) { network.command.Dispatch( packets[0][1], controllers ); for (uint i=0, n=NST_MIN(4,network.players); i < n; ++i) network.input.Dispatch( i, packets[i][0], controllers ); return; } } network.connected = false; window.PostCommand( IDM_NETPLAY_CONNECTION ); } void Netplay::Kaillera::OnEmuCommand(Emulator::Command command,Emulator::Data data) { if (network.connected) network.command.Send( command, data ); } #ifdef NST_MSVC_OPTIMIZE #pragma optimize("", on) #endif void Netplay::Kaillera::Chat() { chat.Open(); } void Netplay::Kaillera::ToggleConnection() { if (emulator.NetPlayers()) { Close(); } else if (!emulator.IsImage() && dialog.Open()) { static const Window::MsgHandler::Entry messages[] = { { WM_NST_OPEN_CLIENT, &Kaillera::OnOpenClient }, { WM_NST_CLOSE_CLIENT, &Kaillera::OnCloseClient }, { WM_NST_START_GAME, &Kaillera::OnStartGame } }; window.Messages().Add( this, messages ); NST_ASSERT( dialog.GetGamePaths().size() ); String::Heap strings; for (Window::Netplay::GamePaths::const_iterator it(dialog.GetGamePaths().begin()), end(dialog.GetGamePaths().end()); it != end; ++it) strings << it->Target().File() << '\0'; String::Heap name; name << "Nestopia " << Application::Instance::GetVersion(); kailleraInfos info; info.appName = name.Ptr(); info.gameList = strings.Ptr(); info.gameCallback = Callbacks::Start; info.chatReceivedCallback = Callbacks::ChatRecieve; info.clientDroppedCallback = Callbacks::ClientDrop; info.moreInfosCallback = NULL; dll.SetInfos( &info ); thread.Start( System::Thread::Callback(this,&Kaillera::StartNetwork) ); } } ibool Netplay::Kaillera::OnEnable(Window::Param& param) { if (!param.wParam) window.Send( WM_ENABLE, true, 0 ); return true; } void Netplay::Kaillera::OnEmuEvent(const Emulator::Event event,Emulator::Data) { if (emulator.NetPlayers()) { static Window::MsgHandler::Callback old; switch (event) { case Emulator::EVENT_POWER_ON: if (emulator.IsGame()) { Client::Hide(); network.connected = true; network.command.Begin(); menu[IDM_NETPLAY_CHAT].Enable(); if (dialog.ShouldGoFullscreen()) window.SendCommand( IDM_VIEW_SWITCH_SCREEN ); enableCallback = window.Messages()[WM_ENABLE].Replace( this, &Kaillera::OnEnable ); } break; case Emulator::EVENT_POWER_OFF: if (emulator.IsGame()) { network.connected = false; network.command.End(); menu[IDM_NETPLAY_CHAT].Disable(); chat.Close(); if (dialog.ShouldGoFullscreen()) window.SendCommand( IDM_VIEW_SWITCH_SCREEN ); window.Messages()[WM_ENABLE] = enableCallback; Client::Show(); dll.EndGame(); } break; case Emulator::EVENT_UNLOAD: emulator.StopNetplay(); break; } } } Netplay::Netplay ( Emulator& e, const Configuration& cfg, Window::Menu& m, const Paths& p, Window::Custom& w ) : Manager ( e, m, this, &Netplay::OnEmuEvent ), kaillera ( NULL ), window ( w ), paths ( p ), fullscreen ( false ), doFullscreen ( cfg["netplay"]["fullscreen"].Yes() ) { menu[IDM_NETPLAY_CONNECTION].Text() << Resource::String( IDS_MENU_NETPLAY_CONNECT ); menu[IDM_NETPLAY_CHAT].Disable(); const Dll dll; Io::Log log; if (!dll) { log << "Kaillera: file \"kailleraclient.dll\" not found or initialization failed. " "netplay will be disabled!\r\n"; menu[IDM_NETPLAY_CONNECTION].Disable(); } else { Application::Instance::Events::Add( this, &Netplay::OnAppEvent ); static const Window::Menu::CmdHandler::Entry commands[] = { { IDM_NETPLAY_CONNECTION, &Netplay::OnCmdConnection }, { IDM_NETPLAY_CHAT, &Netplay::OnCmdChat } }; menu.Commands().Add( this, commands ); char version[16]; version[0] = '\0'; dll.GetVersion( version ); version[15] = '\0'; if (*version) { log << "Kaillera: found \"kailleraclient.dll\" version " << version << "\r\n"; if (std::strcmp( version, "0.9" )) log << "Kaillera: warning, the DLL file may be incompatible with Nestopia!\r\n"; } else { log << "Kaillera: warning, unknown version of \"kailleraclient.dll\"!\r\n"; } UpdateMenu(); } } Netplay::~Netplay() { Application::Instance::Events::Remove( this ); delete kaillera; } void Netplay::Save(Configuration& cfg,const bool saveGameList) const { cfg["netplay"]["fullscreen"].YesNo() = (kaillera ? kaillera->ShouldGoFullscreen() : doFullscreen); if (kaillera && saveGameList) kaillera->SaveFile(); } bool Netplay::Close() const { return kaillera ? kaillera->Close() : true; } void Netplay::UpdateMenu() const { menu[IDM_NETPLAY_CONNECTION].Enable ( emulator.NetPlayers() || (!fullscreen && !emulator.IsImage()) ); } void Netplay::OnEmuEvent(const Emulator::Event event,const Emulator::Data data) { switch (event) { case Emulator::EVENT_NETPLAY_MODE: menu[IDM_NETPLAY_CONNECTION].Text() << Resource::String( data ? IDS_MENU_NETPLAY_DISCONNECT : IDS_MENU_NETPLAY_CONNECT ); case Emulator::EVENT_LOAD: case Emulator::EVENT_UNLOAD: UpdateMenu(); break; } } void Netplay::OnAppEvent(Application::Instance::Event event,const void*) { switch (event) { case Application::Instance::EVENT_DESKTOP: case Application::Instance::EVENT_FULLSCREEN: fullscreen = (event == Application::Instance::EVENT_FULLSCREEN); UpdateMenu(); break; } } void Netplay::OnCmdConnection(uint) { if (kaillera == NULL) { try { kaillera = new Kaillera( emulator, menu, paths, window, doFullscreen ); } catch (Kaillera::Exception) { return; } } kaillera->ToggleConnection(); } void Netplay::OnCmdChat(uint) { if (kaillera) kaillera->Chat(); } } }