2 Copyright (c) 2001, Loki software, inc.
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
8 Redistributions of source code must retain the above copyright notice, this list
9 of conditions and the following disclaimer.
11 Redistributions in binary form must reproduce the above copyright notice, this
12 list of conditions and the following disclaimer in the documentation and/or
13 other materials provided with the distribution.
15 Neither the name of Loki software nor the names of its contributors may be used
16 to endorse or promote products derived from this software without specific prior
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
20 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
23 DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 //-----------------------------------------------------------------------------
34 // monitoring window for running BSP processes (and possibly various other stuff)
37 #include "globaldefs.h"
43 #include "string/string.h"
44 #include "stream/stringstream.h"
46 #include "gtkutil/messagebox.h"
49 #include "preferences.h"
52 #include "mainframe.h"
55 void message_flush(message_info_t *self)
57 Sys_Print(self->msg_level, self->m_buffer, self->m_length);
61 void message_print(message_info_t *self, const char *characters, std::size_t length)
63 const char *end = characters + length;
64 while (characters != end) {
65 std::size_t space = message_info_t::bufsize - 1 - self->m_length;
69 std::size_t size = std::min(space, std::size_t(end - characters));
70 memcpy(self->m_buffer + self->m_length, characters, size);
71 self->m_length += size;
79 #include <uilib/uilib.h>
84 // a flag we have set to true when using an external BSP plugin
85 // the resulting code with that is a bit dirty, cleaner solution would be to seperate the succession of commands from the listening loop
86 // (in two seperate classes probably)
89 // EIdle: we are not listening
90 // DoMonitoringLoop will change state to EBeginStep
91 // EBeginStep: the socket is up for listening, we are expecting incoming connection
92 // incoming connection will change state to EWatching
93 // EWatching: we have a connection, monitor it
94 // connection closed will see if we start a new step (EBeginStep) or launch Quake3 and end (EIdle)
95 enum EWatchBSPState { EIdle, EBeginStep, EWatching } m_eState;
96 socket_t *m_pListenSocket;
97 socket_t *m_pInSocket;
100 // used to timeout EBeginStep
102 std::size_t m_iCurrentStep;
103 // name of the map so we can run the engine
105 // buffer we use in push mode to receive data directly from the network
106 xmlParserInputBufferPtr m_xmlInputBuffer;
107 xmlParserCtxtPtr m_xmlParserCtxt;
109 // call this to switch the set listening mode
110 bool SetupListening();
112 // start a new EBeginStep
115 // the xml and sax parser state
116 char m_xmlBuf[MAX_NETMESSAGE];
117 bool m_bNeedCtxtInit;
118 message_info_t m_message_info;
124 m_bBSPPlugin = false;
125 m_pListenSocket = NULL;
128 m_pTimer = g_timer_new();
130 m_xmlInputBuffer = NULL;
131 m_bNeedCtxtInit = true;
139 g_timer_destroy(m_pTimer);
142 bool HasBSPPlugin() const
143 { return m_bBSPPlugin; }
145 // called regularly to keep listening
146 void RoutineProcessing();
148 // start a monitoring loop with the following steps
149 void DoMonitoringLoop(GPtrArray *pCmd, const char *sBSPName);
151 void EndMonitoringLoop()
155 string_release(m_sBSPName, string_length(m_sBSPName));
159 g_ptr_array_free(m_pCmd, TRUE);
164 // close everything - may be called from the outside to abort the process
167 // start a listening loop for an external process, possibly a BSP plugin
168 void ExternalListen();
171 CWatchBSP *g_pWatchBSP;
173 // watch the BSP process through network connections
174 // true: trigger the BSP steps one by one and monitor them through the network
175 // false: create a BAT / .sh file and execute it. don't bother monitoring it.
176 bool g_WatchBSP_Enabled = true;
177 // do we stop the compilation process if we come accross a leak?
178 bool g_WatchBSP_LeakStop = true;
179 bool g_WatchBSP_RunQuake = false;
180 // store prefs setting for automatic sleep mode activation
181 bool g_WatchBSP_DoSleep = true;
182 // timeout when beginning a step (in seconds)
183 // if we don't get a connection quick enough we assume something failed and go back to idling
184 int g_WatchBSP_Timeout = 10;
187 void Build_constructPreferences(PreferencesPage &page)
189 ui::CheckButton monitorbsp = page.appendCheckBox("", "Enable Build Process Monitoring", g_WatchBSP_Enabled);
190 ui::CheckButton leakstop = page.appendCheckBox("", "Stop Compilation on Leak", g_WatchBSP_LeakStop);
191 ui::CheckButton runengine = page.appendCheckBox("", "Run Engine After Compile", g_WatchBSP_RunQuake);
192 ui::CheckButton sleep = page.appendCheckBox("", "Sleep When Running the Engine", g_WatchBSP_DoSleep);
193 Widget_connectToggleDependency(leakstop, monitorbsp);
194 Widget_connectToggleDependency(runengine, monitorbsp);
195 Widget_connectToggleDependency(sleep, runengine);
198 void Build_constructPage(PreferenceGroup &group)
200 PreferencesPage page(group.createPage("Build", "Build Preferences"));
201 Build_constructPreferences(page);
204 void Build_registerPreferencesPage()
206 PreferencesDialog_addSettingsPage(makeCallbackF(Build_constructPage));
209 #include "preferencesystem.h"
210 #include "stringio.h"
212 void BuildMonitor_Construct()
214 g_pWatchBSP = new CWatchBSP();
216 g_WatchBSP_Enabled = !string_equal(g_pGameDescription->getKeyValue("no_bsp_monitor"), "1");
218 GlobalPreferenceSystem().registerPreference("WatchBSP", make_property_string(g_WatchBSP_Enabled));
219 GlobalPreferenceSystem().registerPreference("RunQuake2Run", make_property_string(g_WatchBSP_RunQuake));
220 GlobalPreferenceSystem().registerPreference("LeakStop", make_property_string(g_WatchBSP_LeakStop));
221 GlobalPreferenceSystem().registerPreference("SleepMode", make_property_string(g_WatchBSP_DoSleep));
223 Build_registerPreferencesPage();
226 void BuildMonitor_Destroy()
231 CWatchBSP *GetWatchBSP()
236 void BuildMonitor_Run(GPtrArray *commands, const char *mapName)
238 GetWatchBSP()->DoMonitoringLoop(commands, mapName);
242 // Static functions for the SAX callbacks -------------------------------------------------------
244 // utility for saxStartElement below
245 static void abortStream(message_info_t *data)
247 GetWatchBSP()->EndMonitoringLoop();
248 // tell there has been an error
250 if ( GetWatchBSP()->HasBSPPlugin() ) {
251 g_BSPFrontendTable.m_pfnEndListen( 2 );
254 // yeah this doesn't look good.. but it's needed so that everything will be ignored until the stream goes out
255 data->ignore_depth = -1;
259 #include "stream_version.h"
261 static void saxStartElement(message_info_t *data, const xmlChar *name, const xmlChar **attrs)
264 globalOutputStream() << "<" << name;
266 for ( const xmlChar** p = attrs; *p != 0; p += 2 )
268 globalOutputStream() << " " << p[0] << "=" << makeQuoted( p[1] );
271 globalOutputStream() << ">\n";
274 if (data->ignore_depth == 0) {
275 if (data->pGeometry != 0) {
277 data->pGeometry->saxStartElement(data, name, attrs);
279 if (strcmp(reinterpret_cast<const char *>( name ), "q3map_feedback") == 0) {
280 // check the correct version
281 // old q3map don't send a version attribute
282 // the ones we support .. send Q3MAP_STREAM_VERSION
283 if (!attrs[0] || !attrs[1] || (strcmp(reinterpret_cast<const char *>( attrs[0] ), "version") != 0)) {
286 << "No stream version given in the feedback stream, this is an old q3map version.\n"
287 "Please turn off monitored compiling if you still wish to use this q3map executable\n";
290 } else if (strcmp(reinterpret_cast<const char *>( attrs[1] ), Q3MAP_STREAM_VERSION) != 0) {
292 globalErrorStream() <<
293 "This version of Radiant reads version " Q3MAP_STREAM_VERSION " debug streams, I got an incoming connection with version "
294 << reinterpret_cast<const char *>( attrs[1] ) << "\n"
295 "Please make sure your versions of Radiant and q3map are matching.\n";
300 // we don't treat locally
301 else if (strcmp(reinterpret_cast<const char *>( name ), "message") == 0) {
302 int msg_level = atoi(reinterpret_cast<const char *>( attrs[1] ));
303 if (msg_level != data->msg_level) {
305 data->msg_level = msg_level;
307 } else if (strcmp(reinterpret_cast<const char *>( name ), "polyline") == 0) {
308 // polyline has a particular status .. right now we only use it for leakfile ..
309 data->geometry_depth = data->recurse;
310 data->pGeometry = &g_pointfile;
311 data->pGeometry->saxStartElement(data, name, attrs);
312 } else if (strcmp(reinterpret_cast<const char *>( name ), "select") == 0) {
313 CSelectMsg *pSelect = new CSelectMsg();
314 data->geometry_depth = data->recurse;
315 data->pGeometry = pSelect;
316 data->pGeometry->saxStartElement(data, name, attrs);
317 } else if (strcmp(reinterpret_cast<const char *>( name ), "pointmsg") == 0) {
318 CPointMsg *pPoint = new CPointMsg();
319 data->geometry_depth = data->recurse;
320 data->pGeometry = pPoint;
321 data->pGeometry->saxStartElement(data, name, attrs);
322 } else if (strcmp(reinterpret_cast<const char *>( name ), "windingmsg") == 0) {
323 CWindingMsg *pWinding = new CWindingMsg();
324 data->geometry_depth = data->recurse;
325 data->pGeometry = pWinding;
326 data->pGeometry->saxStartElement(data, name, attrs);
328 globalErrorStream() << "Warning: ignoring unrecognized node in XML stream ("
329 << reinterpret_cast<const char *>( name ) << ")\n";
330 // we don't recognize this node, jump over it
331 // (NOTE: the ignore mechanism is a bit screwed, only works when starting an ignore at the highest level)
332 data->ignore_depth = data->recurse;
339 static void saxEndElement(message_info_t *data, const xmlChar *name)
342 globalOutputStream() << "<" << name << "/>\n";
346 // we are out of an ignored chunk
347 if (data->recurse == data->ignore_depth) {
348 data->ignore_depth = 0;
351 if (data->pGeometry != 0) {
352 data->pGeometry->saxEndElement(data, name);
353 // we add the object to the debug window
354 if (data->geometry_depth == data->recurse) {
355 g_DbgDlg.Push(data->pGeometry);
359 if (data->recurse == data->stop_depth) {
362 globalOutputStream() << "Received error msg .. shutting down..\n";
364 GetWatchBSP()->EndMonitoringLoop();
365 // tell there has been an error
367 if ( GetWatchBSP()->HasBSPPlugin() ) {
368 g_BSPFrontendTable.m_pfnEndListen( 2 );
375 class MessageOutputStream : public TextOutputStream {
376 message_info_t *m_data;
378 MessageOutputStream(message_info_t *data) : m_data(data)
382 std::size_t write(const char *buffer, std::size_t length)
384 if (m_data->pGeometry != 0) {
385 m_data->pGeometry->saxCharacters(m_data, reinterpret_cast<const xmlChar *>( buffer ), int(length));
387 if (m_data->ignore_depth == 0) {
388 // output the message using the level
389 message_print(m_data, buffer, length);
390 // if this message has error level flag, we mark the depth to stop the compilation when we get out
391 // we don't set the msg level if we don't stop on leak
392 if (m_data->msg_level == 3) {
393 m_data->stop_depth = m_data->recurse - 1;
403 inline MessageOutputStream &operator<<(MessageOutputStream &ostream, const T &t)
405 return ostream_write(ostream, t);
408 static void saxCharacters(message_info_t *data, const xmlChar *ch, int len)
410 MessageOutputStream ostream(data);
411 ostream << StringRange(reinterpret_cast<const char *>( ch ), reinterpret_cast<const char *>( ch + len ));
414 static void saxComment(void *ctx, const xmlChar *msg)
416 globalOutputStream() << "XML comment: " << reinterpret_cast<const char *>( msg ) << "\n";
419 static void saxWarning(void *ctx, const char *msg, ...)
421 char saxMsgBuffer[4096];
425 vsprintf(saxMsgBuffer, msg, args);
427 globalOutputStream() << "XML warning: " << saxMsgBuffer << "\n";
430 static void saxError(void *ctx, const char *msg, ...)
432 char saxMsgBuffer[4096];
436 vsprintf(saxMsgBuffer, msg, args);
438 globalErrorStream() << "XML error: " << saxMsgBuffer << "\n";
441 static void saxFatal(void *ctx, const char *msg, ...)
448 vsprintf(buffer, msg, args);
450 globalErrorStream() << "XML fatal error: " << buffer << "\n";
453 static xmlSAXHandler saxParser = {
454 0, /* internalSubset */
455 0, /* isStandalone */
456 0, /* hasInternalSubset */
457 0, /* hasExternalSubset */
458 0, /* resolveEntity */
461 0, /* notationDecl */
462 0, /* attributeDecl */
464 0, /* unparsedEntityDecl */
465 0, /* setDocumentLocator */
466 0, /* startDocument */
468 (startElementSAXFunc) saxStartElement, /* startElement */
469 (endElementSAXFunc) saxEndElement, /* endElement */
471 (charactersSAXFunc) saxCharacters, /* characters */
472 0, /* ignorableWhitespace */
473 0, /* processingInstruction */
474 (commentSAXFunc) saxComment, /* comment */
475 (warningSAXFunc) saxWarning, /* warning */
476 (errorSAXFunc) saxError, /* error */
477 (fatalErrorSAXFunc) saxFatal, /* fatalError */
488 // ------------------------------------------------------------------------------------------------
493 static gint watchbsp_routine(gpointer data)
495 reinterpret_cast<CWatchBSP *>( data )->RoutineProcessing();
499 void CWatchBSP::Reset()
502 Net_Disconnect(m_pInSocket);
505 if (m_pListenSocket) {
506 Net_Disconnect(m_pListenSocket);
507 m_pListenSocket = NULL;
509 if (m_xmlInputBuffer) {
510 xmlFreeParserInputBuffer(m_xmlInputBuffer);
511 m_xmlInputBuffer = NULL;
515 g_source_remove(s_routine_id);
519 bool CWatchBSP::SetupListening()
522 if (m_pListenSocket) {
523 globalOutputStream() << "ERROR: m_pListenSocket != NULL in CWatchBSP::SetupListening\n";
527 globalOutputStream() << "Setting up\n";
529 m_pListenSocket = Net_ListenSocket(39000);
530 if (m_pListenSocket == NULL) {
533 globalOutputStream() << "Listening...\n";
537 void CWatchBSP::DoEBeginStep()
540 if (SetupListening() == false) {
541 const char *msg = "Failed to get a listening socket on port 39000.\nTry running with Build monitoring disabled if you can't fix this.\n";
542 globalOutputStream() << msg;
543 ui::alert(MainFrame_getWindow(), msg, "Build monitoring", ui::alert_type::OK, ui::alert_icon::Error);
546 // set the timer for timeouts and step cancellation
547 g_timer_reset(m_pTimer);
548 g_timer_start(m_pTimer);
551 globalOutputStream() << "=== running build command ===\n"
552 << static_cast<const char *>(g_ptr_array_index(m_pCmd, m_iCurrentStep) ) << "\n";
554 if (!Q_Exec(NULL, (char *) g_ptr_array_index(m_pCmd, m_iCurrentStep), NULL, true, false)) {
555 StringOutputStream msg(256);
556 msg << "Failed to execute the following command: ";
557 msg << reinterpret_cast<const char *>(g_ptr_array_index(m_pCmd, m_iCurrentStep) );
558 msg << "\nCheck that the file exists and that you don't run out of system resources.\n";
559 globalOutputStream() << msg.c_str();
560 ui::alert(MainFrame_getWindow(), msg.c_str(), "Build monitoring", ui::alert_type::OK,
561 ui::alert_icon::Error);
564 // re-initialise the debug window
565 if (m_iCurrentStep == 0) {
569 m_eState = EBeginStep;
570 s_routine_id = g_timeout_add(25, watchbsp_routine, this);
575 const char *ENGINE_ATTRIBUTE = "engine_win32";
576 const char *MP_ENGINE_ATTRIBUTE = "mp_engine_win32";
577 #elif GDEF_OS_LINUX || GDEF_OS_BSD
578 const char *ENGINE_ATTRIBUTE = "engine_linux";
579 const char *MP_ENGINE_ATTRIBUTE = "mp_engine_linux";
581 const char *ENGINE_ATTRIBUTE = "engine_macos";
582 const char *MP_ENGINE_ATTRIBUTE = "mp_engine_macos";
584 #error "unsupported platform"
587 class RunEngineConfiguration {
589 const char *executable;
590 const char *mp_executable;
593 RunEngineConfiguration() :
594 executable(g_pGameDescription->getRequiredKeyValue(ENGINE_ATTRIBUTE)),
595 mp_executable(g_pGameDescription->getKeyValue(MP_ENGINE_ATTRIBUTE))
597 do_sp_mp = !string_empty(mp_executable);
601 inline void GlobalGameDescription_string_write_mapparameter(StringOutputStream &string, const char *mapname)
603 if (g_pGameDescription->mGameType == "q2"
604 || g_pGameDescription->mGameType == "heretic2") {
605 string << ". +exec radiant.cfg +map " << mapname;
607 string << "+set sv_pure 0 ";
608 // TTimo: a check for vm_* but that's all fine
609 //cmdline = "+set sv_pure 0 +set vm_ui 0 +set vm_cgame 0 +set vm_game 0 ";
610 const char *fs_game = gamename_get();
611 if (!string_equal(fs_game, basegame_get())) {
612 string << "+set fs_game " << fs_game << " ";
614 if (g_pGameDescription->mGameType == "wolf") {
615 //|| g_pGameDescription->mGameType == "et")
616 if (string_equal(gamemode_get(), "mp")) {
618 string << "+devmap " << mapname;
621 string << "+set nextmap \"spdevmap " << mapname << "\"";
624 string << "+devmap " << mapname;
630 void CWatchBSP::RoutineProcessing()
634 // timeout: if we don't get an incoming connection fast enough, go back to idle
635 if (g_timer_elapsed(m_pTimer, NULL) > g_WatchBSP_Timeout) {
636 ui::alert(MainFrame_getWindow(),
637 "The connection timed out, assuming the build process failed\nMake sure you are using a networked version of Q3Map?\nOtherwise you need to disable BSP Monitoring in prefs.",
638 "BSP process monitoring", ui::alert_type::OK);
641 if ( m_bBSPPlugin ) {
642 // status == 1 : didn't get the connection
643 g_BSPFrontendTable.m_pfnEndListen( 1 );
650 if (!m_pListenSocket) {
652 << "ERROR: m_pListenSocket == NULL in CWatchBSP::RoutineProcessing EBeginStep state\n";
656 // we are not connected yet, accept any incoming connection
657 m_pInSocket = Net_Accept(m_pListenSocket);
659 globalOutputStream() << "Connected.\n";
660 // prepare the message info struct for diving in
661 memset(&m_message_info, 0, sizeof(message_info_t));
662 // a dumb flag to make sure we init the push parser context when first getting a msg
663 m_bNeedCtxtInit = true;
664 m_eState = EWatching;
671 globalErrorStream() << "ERROR: m_pInSocket == NULL in CWatchBSP::RoutineProcessing EWatching state\n";
676 int ret = Net_Wait(m_pInSocket, 0, 0);
678 globalOutputStream() << "WARNING: SOCKET_ERROR in CWatchBSP::RoutineProcessing\n";
679 globalOutputStream() << "Terminating the connection.\n";
685 // the socket has been identified, there's something (message or disconnection)
686 // see if there's anything in input
687 ret = Net_Receive(m_pInSocket, &msg);
689 // unsigned int size = msg.size; //++timo just a check
690 strcpy(m_xmlBuf, NMSG_ReadString(&msg));
691 if (m_bNeedCtxtInit) {
692 m_xmlParserCtxt = NULL;
693 m_xmlParserCtxt = xmlCreatePushParserCtxt(&saxParser, &m_message_info, m_xmlBuf,
694 static_cast<int>( strlen(m_xmlBuf)), NULL);
696 if (m_xmlParserCtxt == NULL) {
697 globalErrorStream() << "Failed to create the XML parser (incoming stream began with: "
698 << m_xmlBuf << ")\n";
701 m_bNeedCtxtInit = false;
703 xmlParseChunk(m_xmlParserCtxt, m_xmlBuf, static_cast<int>( strlen(m_xmlBuf)), 0);
706 message_flush(&m_message_info);
707 // error or connection closed/reset
708 // NOTE: if we get an error down the XML stream we don't reach here
709 Net_Disconnect(m_pInSocket);
711 globalOutputStream() << "Connection closed.\n";
713 if ( m_bBSPPlugin ) {
715 // let the BSP plugin know that the job is done
716 g_BSPFrontendTable.m_pfnEndListen( 0 );
720 // move to next step or finish
722 if (m_iCurrentStep < m_pCmd->len) {
725 // launch the engine .. OMG
726 if (g_WatchBSP_RunQuake) {
728 // do we enter sleep mode before?
729 if ( g_WatchBSP_DoSleep ) {
730 globalOutputStream() << "Going into sleep mode..\n";
731 g_pParentWnd->OnSleep();
734 globalOutputStream() << "Running engine...\n";
735 StringOutputStream cmd(256);
736 // build the command line
737 cmd << EnginePath_get();
738 // this is game dependant
740 RunEngineConfiguration engineConfig;
742 if (engineConfig.do_sp_mp) {
743 if (string_equal(gamemode_get(), "mp")) {
744 cmd << engineConfig.mp_executable;
746 cmd << engineConfig.executable;
749 cmd << engineConfig.executable;
752 StringOutputStream cmdline;
754 GlobalGameDescription_string_write_mapparameter(cmdline, m_sBSPName);
756 globalOutputStream() << cmd.c_str() << " " << cmdline.c_str() << "\n";
759 if (!Q_Exec(cmd.c_str(), (char *) cmdline.c_str(), EnginePath_get(), false, false)) {
760 StringOutputStream msg;
761 msg << "Failed to execute the following command: " << cmd.c_str() << cmdline.c_str();
762 globalOutputStream() << msg.c_str();
763 ui::alert(MainFrame_getWindow(), msg.c_str(), "Build monitoring", ui::alert_type::OK,
764 ui::alert_icon::Error);
778 GPtrArray *str_ptr_array_clone(GPtrArray *array)
780 GPtrArray *cloned = g_ptr_array_sized_new(array->len);
781 for (guint i = 0; i < array->len; ++i) {
782 g_ptr_array_add(cloned, g_strdup((char *) g_ptr_array_index(array, i)));
787 void CWatchBSP::DoMonitoringLoop(GPtrArray *pCmd, const char *sBSPName)
789 m_sBSPName = string_clone(sBSPName);
790 if (m_eState != EIdle) {
791 globalOutputStream() << "WatchBSP got a monitoring request while not idling...\n";
792 // prompt the user, should we cancel the current process and go ahead?
793 if (ui::alert(MainFrame_getWindow(),
794 "I am already monitoring a Build process.\nDo you want me to override and start a new compilation?",
795 "Build process monitoring", ui::alert_type::YESNO) == ui::alert_response::YES) {
796 // disconnect and set EIdle state
800 m_pCmd = str_ptr_array_clone(pCmd);
805 void CWatchBSP::ExternalListen()
811 // the part of the watchbsp interface we export to plugins
812 // NOTE: in the long run, the whole watchbsp.cpp interface needs to go out and be handled at the BSP plugin level
813 // for now we provide something really basic and limited, the essential is to have something that works fine and fast (for 1.1 final)
816 // open the listening socket
817 GetWatchBSP()->ExternalListen();