/ [w3loader] / ghost / src / ghost.cpp
To checkout: svn checkout http://svn.gnu.org.ua/sources/w3loader/ghost/src/ghost.cpp
Puszcza

Annotation of /ghost/src/ghost.cpp

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5 - (hide annotations)
Tue Jun 2 19:44:46 2020 UTC (17 months, 4 weeks ago) by zombieteam
File size: 54734 byte(s)
kick when save game
1 zombieteam 2 /*
2    
3     Copyright [2008] [Trevor Hogan]
4    
5     Licensed under the Apache License, Version 2.0 (the "License");
6     you may not use this file except in compliance with the License.
7     You may obtain a copy of the License at
8    
9     http://www.apache.org/licenses/LICENSE-2.0
10    
11     Unless required by applicable law or agreed to in writing, software
12     distributed under the License is distributed on an "AS IS" BASIS,
13     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14     See the License for the specific language governing permissions and
15     limitations under the License.
16    
17     CODE PORTED FROM THE ORIGINAL GHOST PROJECT: http://ghost.pwner.org/
18    
19     */
20    
21     #include "ghost.h"
22     #include "util.h"
23     #include "crc32.h"
24     #include "sha1.h"
25     #include "csvparser.h"
26     #include "config.h"
27     #include "language.h"
28     #include "socket.h"
29     #include "ghostdb.h"
30     #include "ghostdbsqlite.h"
31     #include "ghostdbmysql.h"
32     #include "bnet.h"
33     #include "map.h"
34     #include "packed.h"
35     #include "savegame.h"
36     #include "gameplayer.h"
37     #include "gameprotocol.h"
38     #include "gpsprotocol.h"
39     #include "game_base.h"
40     #include "game.h"
41     #include "game_admin.h"
42    
43     #include <signal.h>
44     #include <stdlib.h>
45    
46     #ifdef WIN32
47     #include <ws2tcpip.h> // for WSAIoctl
48     #endif
49    
50     #define __STORMLIB_SELF__
51     #include <stormlib/StormLib.h>
52    
53     /*
54    
55     #include "ghost.h"
56     #include "util.h"
57     #include "crc32.h"
58     #include "sha1.h"
59     #include "csvparser.h"
60     #include "config.h"
61     #include "language.h"
62     #include "socket.h"
63     #include "commandpacket.h"
64     #include "ghostdb.h"
65     #include "ghostdbsqlite.h"
66     #include "ghostdbmysql.h"
67     #include "bncsutilinterface.h"
68     #include "warden.h"
69     #include "bnlsprotocol.h"
70     #include "bnlsclient.h"
71     #include "bnetprotocol.h"
72     #include "bnet.h"
73     #include "map.h"
74     #include "packed.h"
75     #include "savegame.h"
76     #include "replay.h"
77     #include "gameslot.h"
78     #include "gameplayer.h"
79     #include "gameprotocol.h"
80     #include "gpsprotocol.h"
81     #include "game_base.h"
82     #include "game.h"
83     #include "game_admin.h"
84     #include "stats.h"
85     #include "statsdota.h"
86     #include "sqlite3.h"
87    
88     */
89    
90     #ifdef WIN32
91     #include <windows.h>
92     #include <winsock.h>
93     #endif
94    
95     #include <time.h>
96    
97     #ifndef WIN32
98     #include <sys/time.h>
99     #endif
100    
101     #ifdef __APPLE__
102     #include <mach/mach_time.h>
103     #endif
104    
105     string gCFGFile;
106     string gLogFile;
107     uint32_t gLogMethod;
108     ofstream *gLog = NULL;
109     CGHost *gGHost = NULL;
110    
111     uint32_t GetTime( )
112     {
113     return GetTicks( ) / 1000;
114     }
115    
116     uint32_t GetTicks( )
117     {
118     #ifdef WIN32
119     // don't use GetTickCount anymore because it's not accurate enough (~16ms resolution)
120     // don't use QueryPerformanceCounter anymore because it isn't guaranteed to be strictly increasing on some systems and thus requires "smoothing" code
121     // use timeGetTime instead, which typically has a high resolution (5ms or more) but we request a lower resolution on startup
122    
123     return timeGetTime( );
124     #elif __APPLE__
125     uint64_t current = mach_absolute_time( );
126     static mach_timebase_info_data_t info = { 0, 0 };
127     // get timebase info
128     if( info.denom == 0 )
129     mach_timebase_info( &info );
130     uint64_t elapsednano = current * ( info.numer / info.denom );
131     // convert ns to ms
132     return elapsednano / 1e6;
133     #else
134     uint32_t ticks;
135     struct timespec t;
136     clock_gettime( CLOCK_MONOTONIC, &t );
137     ticks = t.tv_sec * 1000;
138     ticks += t.tv_nsec / 1000000;
139     return ticks;
140     #endif
141     }
142    
143     void SignalCatcher2( int s )
144     {
145     CONSOLE_Print( "[!!!] caught signal " + UTIL_ToString( s ) + ", exiting NOW" );
146    
147     if( gGHost )
148     {
149     if( gGHost->m_Exiting )
150     exit( 1 );
151     else
152     gGHost->m_Exiting = true;
153     }
154     else
155     exit( 1 );
156     }
157    
158     void SignalCatcher( int s )
159     {
160     // signal( SIGABRT, SignalCatcher2 );
161     signal( SIGINT, SignalCatcher2 );
162    
163     CONSOLE_Print( "[!!!] caught signal " + UTIL_ToString( s ) + ", exiting nicely" );
164    
165     if( gGHost )
166     gGHost->m_ExitingNice = true;
167     else
168     exit( 1 );
169     }
170    
171     void CONSOLE_Print( string message )
172     {
173     cout << message << endl;
174    
175     // logging
176    
177     if( !gLogFile.empty( ) )
178     {
179     if( gLogMethod == 1 )
180     {
181     ofstream Log;
182     Log.open( gLogFile.c_str( ), ios :: app );
183    
184     if( !Log.fail( ) )
185     {
186     time_t Now = time( NULL );
187     string Time = asctime( localtime( &Now ) );
188    
189     // erase the newline
190    
191     Time.erase( Time.size( ) - 1 );
192     Log << "[" << Time << "] " << message << endl;
193     Log.close( );
194     }
195     }
196     else if( gLogMethod == 2 )
197     {
198     if( gLog && !gLog->fail( ) )
199     {
200     time_t Now = time( NULL );
201     string Time = asctime( localtime( &Now ) );
202    
203     // erase the newline
204    
205     Time.erase( Time.size( ) - 1 );
206     *gLog << "[" << Time << "] " << message << endl;
207     gLog->flush( );
208     }
209     }
210     }
211     }
212    
213     void DEBUG_Print( string message )
214     {
215     cout << message << endl;
216     }
217    
218     void DEBUG_Print( BYTEARRAY b )
219     {
220     cout << "{ ";
221    
222     for( unsigned int i = 0; i < b.size( ); i++ )
223     cout << hex << (int)b[i] << " ";
224    
225     cout << "}" << endl;
226     }
227    
228     //
229     // main
230     //
231    
232     int main( int argc, char **argv )
233     {
234     gCFGFile = "ghost.cfg";
235    
236     if( argc > 1 && argv[1] )
237     gCFGFile = argv[1];
238    
239     // read config file
240    
241     CConfig CFG;
242     CFG.Read( "default.cfg" );
243     CFG.Read( gCFGFile );
244     gLogFile = CFG.GetString( "bot_log", string( ) );
245     gLogMethod = CFG.GetInt( "bot_logmethod", 1 );
246    
247     if( !gLogFile.empty( ) )
248     {
249     if( gLogMethod == 1 )
250     {
251     // log method 1: open, append, and close the log for every message
252     // this works well on Linux but poorly on Windows, particularly as the log file grows in size
253     // the log file can be edited/moved/deleted while GHost++ is running
254     }
255     else if( gLogMethod == 2 )
256     {
257     // log method 2: open the log on startup, flush the log for every message, close the log on shutdown
258     // the log file CANNOT be edited/moved/deleted while GHost++ is running
259    
260     gLog = new ofstream( );
261     gLog->open( gLogFile.c_str( ), ios :: app );
262     }
263     }
264    
265     CONSOLE_Print( "[GHOST] starting up" );
266    
267     if( !gLogFile.empty( ) )
268     {
269     if( gLogMethod == 1 )
270     CONSOLE_Print( "[GHOST] using log method 1, logging is enabled and [" + gLogFile + "] will not be locked" );
271     else if( gLogMethod == 2 )
272     {
273     if( gLog->fail( ) )
274     CONSOLE_Print( "[GHOST] using log method 2 but unable to open [" + gLogFile + "] for appending, logging is disabled" );
275     else
276     CONSOLE_Print( "[GHOST] using log method 2, logging is enabled and [" + gLogFile + "] is now locked" );
277     }
278     }
279     else
280     CONSOLE_Print( "[GHOST] no log file specified, logging is disabled" );
281    
282     // catch SIGABRT and SIGINT
283    
284     // signal( SIGABRT, SignalCatcher );
285     signal( SIGINT, SignalCatcher );
286    
287     #ifndef WIN32
288     // disable SIGPIPE since some systems like OS X don't define MSG_NOSIGNAL
289    
290     signal( SIGPIPE, SIG_IGN );
291     #endif
292    
293     #ifdef WIN32
294     // initialize timer resolution
295     // attempt to set the resolution as low as possible from 1ms to 5ms
296    
297     unsigned int TimerResolution = 0;
298    
299     for( unsigned int i = 1; i <= 5; i++ )
300     {
301     if( timeBeginPeriod( i ) == TIMERR_NOERROR )
302     {
303     TimerResolution = i;
304     break;
305     }
306     else if( i < 5 )
307     CONSOLE_Print( "[GHOST] error setting Windows timer resolution to " + UTIL_ToString( i ) + " milliseconds, trying a higher resolution" );
308     else
309     {
310     CONSOLE_Print( "[GHOST] error setting Windows timer resolution" );
311     return 1;
312     }
313     }
314    
315     CONSOLE_Print( "[GHOST] using Windows timer with resolution " + UTIL_ToString( TimerResolution ) + " milliseconds" );
316     #elif __APPLE__
317     // not sure how to get the resolution
318     #else
319     // print the timer resolution
320    
321     struct timespec Resolution;
322    
323     if( clock_getres( CLOCK_MONOTONIC, &Resolution ) == -1 )
324     CONSOLE_Print( "[GHOST] error getting monotonic timer resolution" );
325     else
326     CONSOLE_Print( "[GHOST] using monotonic timer with resolution " + UTIL_ToString( (double)( Resolution.tv_nsec / 1000 ), 2 ) + " microseconds" );
327     #endif
328    
329     #ifdef WIN32
330     // initialize winsock
331    
332     CONSOLE_Print( "[GHOST] starting winsock" );
333     WSADATA wsadata;
334    
335     if( WSAStartup( MAKEWORD( 2, 2 ), &wsadata ) != 0 )
336     {
337     CONSOLE_Print( "[GHOST] error starting winsock" );
338     return 1;
339     }
340    
341     // increase process priority
342    
343     CONSOLE_Print( "[GHOST] setting process priority to \"above normal\"" );
344     SetPriorityClass( GetCurrentProcess( ), ABOVE_NORMAL_PRIORITY_CLASS );
345     #endif
346    
347     // initialize ghost
348    
349     gGHost = new CGHost( &CFG );
350    
351     while( 1 )
352     {
353     // block for 50ms on all sockets - if you intend to perform any timed actions more frequently you should change this
354     // that said it's likely we'll loop more often than this due to there being data waiting on one of the sockets but there aren't any guarantees
355    
356     if( gGHost->Update( 50000 ) )
357     break;
358     }
359    
360     // shutdown ghost
361    
362     CONSOLE_Print( "[GHOST] shutting down" );
363     delete gGHost;
364     gGHost = NULL;
365    
366     #ifdef WIN32
367     // shutdown winsock
368    
369     CONSOLE_Print( "[GHOST] shutting down winsock" );
370     WSACleanup( );
371    
372     // shutdown timer
373    
374     timeEndPeriod( TimerResolution );
375     #endif
376    
377     if( gLog )
378     {
379     if( !gLog->fail( ) )
380     gLog->close( );
381    
382     delete gLog;
383     }
384    
385     return 0;
386     }
387    
388     //
389     // CGHost
390     //
391    
392     CGHost :: CGHost( CConfig *CFG )
393     {
394     m_UDPSocket = new CUDPSocket( );
395     m_UDPSocket->SetBroadcastTarget( CFG->GetString( "udp_broadcasttarget", string( ) ) );
396     m_UDPSocket->SetDontRoute( CFG->GetInt( "udp_dontroute", 0 ) == 0 ? false : true );
397     m_ReconnectSocket = NULL;
398     m_GPSProtocol = new CGPSProtocol( );
399     m_CRC = new CCRC32( );
400     m_CRC->Initialize( );
401     m_SHA = new CSHA1( );
402     m_CurrentGame = NULL;
403     string DBType = CFG->GetString( "db_type", "sqlite3" );
404     CONSOLE_Print( "[GHOST] opening primary database" );
405    
406     if( DBType == "mysql" )
407     {
408     #ifdef GHOST_MYSQL
409     m_DB = new CGHostDBMySQL( CFG );
410     #else
411     CONSOLE_Print( "[GHOST] warning - this binary was not compiled with MySQL database support, using SQLite database instead" );
412     m_DB = new CGHostDBSQLite( CFG );
413     #endif
414     }
415     else
416     m_DB = new CGHostDBSQLite( CFG );
417    
418     CONSOLE_Print( "[GHOST] opening secondary (local) database" );
419     m_DBLocal = new CGHostDBSQLite( CFG );
420    
421     // get a list of local IP addresses
422     // this list is used elsewhere to determine if a player connecting to the bot is local or not
423    
424     CONSOLE_Print( "[GHOST] attempting to find local IP addresses" );
425    
426     #ifdef WIN32
427     // use a more reliable Windows specific method since the portable method doesn't always work properly on Windows
428     // code stolen from: http://tangentsoft.net/wskfaq/examples/getifaces.html
429    
430     SOCKET sd = WSASocket( AF_INET, SOCK_DGRAM, 0, 0, 0, 0 );
431    
432     if( sd == SOCKET_ERROR )
433     CONSOLE_Print( "[GHOST] error finding local IP addresses - failed to create socket (error code " + UTIL_ToString( WSAGetLastError( ) ) + ")" );
434     else
435     {
436     INTERFACE_INFO InterfaceList[20];
437     unsigned long nBytesReturned;
438    
439     if( WSAIoctl( sd, SIO_GET_INTERFACE_LIST, 0, 0, &InterfaceList, sizeof(InterfaceList), &nBytesReturned, 0, 0 ) == SOCKET_ERROR )
440     CONSOLE_Print( "[GHOST] error finding local IP addresses - WSAIoctl failed (error code " + UTIL_ToString( WSAGetLastError( ) ) + ")" );
441     else
442     {
443     int nNumInterfaces = nBytesReturned / sizeof(INTERFACE_INFO);
444    
445     for( int i = 0; i < nNumInterfaces; i++ )
446     {
447     sockaddr_in *pAddress;
448     pAddress = (sockaddr_in *)&(InterfaceList[i].iiAddress);
449     CONSOLE_Print( "[GHOST] local IP address #" + UTIL_ToString( i + 1 ) + " is [" + string( inet_ntoa( pAddress->sin_addr ) ) + "]" );
450     m_LocalAddresses.push_back( UTIL_CreateByteArray( (uint32_t)pAddress->sin_addr.s_addr, false ) );
451     }
452     }
453    
454     closesocket( sd );
455     }
456     #else
457     // use a portable method
458    
459     char HostName[255];
460    
461     if( gethostname( HostName, 255 ) == SOCKET_ERROR )
462     CONSOLE_Print( "[GHOST] error finding local IP addresses - failed to get local hostname" );
463     else
464     {
465     CONSOLE_Print( "[GHOST] local hostname is [" + string( HostName ) + "]" );
466     struct hostent *HostEnt = gethostbyname( HostName );
467    
468     if( !HostEnt )
469     CONSOLE_Print( "[GHOST] error finding local IP addresses - gethostbyname failed" );
470     else
471     {
472     for( int i = 0; HostEnt->h_addr_list[i] != NULL; i++ )
473     {
474     struct in_addr Address;
475     memcpy( &Address, HostEnt->h_addr_list[i], sizeof(struct in_addr) );
476     CONSOLE_Print( "[GHOST] local IP address #" + UTIL_ToString( i + 1 ) + " is [" + string( inet_ntoa( Address ) ) + "]" );
477     m_LocalAddresses.push_back( UTIL_CreateByteArray( (uint32_t)Address.s_addr, false ) );
478     }
479     }
480     }
481     #endif
482    
483     m_Language = NULL;
484     m_Exiting = false;
485     m_ExitingNice = false;
486     m_Enabled = true;
487     m_Version = "17.0";
488     m_HostCounter = 1;
489     m_AutoHostMaximumGames = CFG->GetInt( "autohost_maxgames", 0 );
490     m_AutoHostAutoStartPlayers = CFG->GetInt( "autohost_startplayers", 0 );
491     m_AutoHostGameName = CFG->GetString( "autohost_gamename", string( ) );
492     m_AutoHostOwner = CFG->GetString( "autohost_owner", string( ) );
493     m_LastAutoHostTime = GetTime( );
494     m_AutoHostMatchMaking = false;
495     m_AutoHostMinimumScore = 0.0;
496     m_AutoHostMaximumScore = 0.0;
497     m_AllGamesFinished = false;
498     m_AllGamesFinishedTime = 0;
499     m_TFT = CFG->GetInt( "bot_tft", 1 ) == 0 ? false : true;
500    
501     if( m_TFT )
502     CONSOLE_Print( "[GHOST] acting as Warcraft III: The Frozen Throne" );
503     else
504     CONSOLE_Print( "[GHOST] acting as Warcraft III: Reign of Chaos" );
505    
506     m_HostPort = CFG->GetInt( "bot_hostport", 6112 );
507     m_Reconnect = CFG->GetInt( "bot_reconnect", 1 ) == 0 ? false : true;
508     m_ReconnectPort = CFG->GetInt( "bot_reconnectport", 6114 );
509     m_DefaultMap = CFG->GetString( "bot_defaultmap", "map" );
510     m_AdminGameCreate = CFG->GetInt( "admingame_create", 0 ) == 0 ? false : true;
511     m_AdminGamePort = CFG->GetInt( "admingame_port", 6113 );
512     m_AdminGamePassword = CFG->GetString( "admingame_password", string( ) );
513     m_AdminGameMap = CFG->GetString( "admingame_map", string( ) );
514     m_LANWar3Version = CFG->GetInt( "lan_war3version", 24 );
515     m_ReplayWar3Version = CFG->GetInt( "replay_war3version", 24 );
516     m_ReplayBuildNumber = CFG->GetInt( "replay_buildnumber", 6059 );
517     SetConfigs( CFG );
518    
519     // load the battle.net connections
520     // we're just loading the config data and creating the CBNET classes here, the connections are established later (in the Update function)
521    
522     for( uint32_t i = 1; i < 10; i++ )
523     {
524     string Prefix;
525    
526     if( i == 1 )
527     Prefix = "bnet_";
528     else
529     Prefix = "bnet" + UTIL_ToString( i ) + "_";
530    
531     string Server = CFG->GetString( Prefix + "server", string( ) );
532     string ServerAlias = CFG->GetString( Prefix + "serveralias", string( ) );
533     string CDKeyROC = CFG->GetString( Prefix + "cdkeyroc", string( ) );
534     string CDKeyTFT = CFG->GetString( Prefix + "cdkeytft", string( ) );
535     string CountryAbbrev = CFG->GetString( Prefix + "countryabbrev", "USA" );
536     string Country = CFG->GetString( Prefix + "country", "United States" );
537     string Locale = CFG->GetString( Prefix + "locale", "system" );
538     uint32_t LocaleID;
539    
540     if( Locale == "system" )
541     {
542     #ifdef WIN32
543     LocaleID = GetUserDefaultLangID( );
544     #else
545     LocaleID = 1033;
546     #endif
547     }
548     else
549     LocaleID = UTIL_ToUInt32( Locale );
550    
551     string UserName = CFG->GetString( Prefix + "username", string( ) );
552     string UserPassword = CFG->GetString( Prefix + "password", string( ) );
553     string FirstChannel = CFG->GetString( Prefix + "firstchannel", "The Void" );
554     string RootAdmin = CFG->GetString( Prefix + "rootadmin", string( ) );
555     string BNETCommandTrigger = CFG->GetString( Prefix + "commandtrigger", "!" );
556    
557     if( BNETCommandTrigger.empty( ) )
558     BNETCommandTrigger = "!";
559    
560     bool HoldFriends = CFG->GetInt( Prefix + "holdfriends", 1 ) == 0 ? false : true;
561     bool HoldClan = CFG->GetInt( Prefix + "holdclan", 1 ) == 0 ? false : true;
562     bool PublicCommands = CFG->GetInt( Prefix + "publiccommands", 1 ) == 0 ? false : true;
563     string BNLSServer = CFG->GetString( Prefix + "bnlsserver", string( ) );
564     int BNLSPort = CFG->GetInt( Prefix + "bnlsport", 9367 );
565     int BNLSWardenCookie = CFG->GetInt( Prefix + "bnlswardencookie", 0 );
566     unsigned char War3Version = CFG->GetInt( Prefix + "custom_war3version", 24 );
567     BYTEARRAY EXEVersion = UTIL_ExtractNumbers( CFG->GetString( Prefix + "custom_exeversion", string( ) ), 4 );
568     BYTEARRAY EXEVersionHash = UTIL_ExtractNumbers( CFG->GetString( Prefix + "custom_exeversionhash", string( ) ), 4 );
569     string PasswordHashType = CFG->GetString( Prefix + "custom_passwordhashtype", string( ) );
570     string PVPGNRealmName = CFG->GetString( Prefix + "custom_pvpgnrealmname", "PvPGN Realm" );
571     uint32_t MaxMessageLength = CFG->GetInt( Prefix + "custom_maxmessagelength", 200 );
572 zombieteam 3 string War3Path = UTIL_AddPathSeperator( CFG->GetString( Prefix + "war3path", m_Warcraft3Path ) );
573 zombieteam 2
574     if( Server.empty( ) )
575     break;
576    
577     if( CDKeyROC.empty( ) )
578     {
579     CONSOLE_Print( "[GHOST] missing " + Prefix + "cdkeyroc, skipping this battle.net connection" );
580     continue;
581     }
582    
583     if( m_TFT && CDKeyTFT.empty( ) )
584     {
585     CONSOLE_Print( "[GHOST] missing " + Prefix + "cdkeytft, skipping this battle.net connection" );
586     continue;
587     }
588    
589     if( UserName.empty( ) )
590     {
591     CONSOLE_Print( "[GHOST] missing " + Prefix + "username, skipping this battle.net connection" );
592     continue;
593     }
594    
595     if( UserPassword.empty( ) )
596     {
597     CONSOLE_Print( "[GHOST] missing " + Prefix + "password, skipping this battle.net connection" );
598     continue;
599     }
600    
601     CONSOLE_Print( "[GHOST] found battle.net connection #" + UTIL_ToString( i ) + " for server [" + Server + "]" );
602    
603     if( Locale == "system" )
604     {
605     #ifdef WIN32
606     CONSOLE_Print( "[GHOST] using system locale of " + UTIL_ToString( LocaleID ) );
607     #else
608     CONSOLE_Print( "[GHOST] unable to get system locale, using default locale of 1033" );
609     #endif
610     }
611    
612 zombieteam 3 m_BNETs.push_back( new CBNET( this, Server, ServerAlias, BNLSServer, (uint16_t)BNLSPort, (uint32_t)BNLSWardenCookie, CDKeyROC, CDKeyTFT, CountryAbbrev, Country, LocaleID, UserName, UserPassword, FirstChannel, RootAdmin, BNETCommandTrigger[0], HoldFriends, HoldClan, PublicCommands, War3Version, EXEVersion, EXEVersionHash, PasswordHashType, PVPGNRealmName, MaxMessageLength, i, War3Path ) );
613 zombieteam 2 }
614    
615     if( m_BNETs.empty( ) )
616     CONSOLE_Print( "[GHOST] warning - no battle.net connections found in config file" );
617    
618     // extract common.j and blizzard.j from War3Patch.mpq if we can
619     // these two files are necessary for calculating "map_crc" when loading maps so we make sure to do it before loading the default map
620     // see CMap :: Load for more information
621    
622     ExtractScripts( );
623    
624     // load the default maps (note: make sure to run ExtractScripts first)
625    
626     if( m_DefaultMap.size( ) < 4 || m_DefaultMap.substr( m_DefaultMap.size( ) - 4 ) != ".cfg" )
627     {
628     m_DefaultMap += ".cfg";
629     CONSOLE_Print( "[GHOST] adding \".cfg\" to default map -> new default is [" + m_DefaultMap + "]" );
630     }
631    
632     CConfig MapCFG;
633     MapCFG.Read( m_MapCFGPath + m_DefaultMap );
634     m_Map = new CMap( this, &MapCFG, m_MapCFGPath + m_DefaultMap );
635    
636     if( !m_AdminGameMap.empty( ) )
637     {
638     if( m_AdminGameMap.size( ) < 4 || m_AdminGameMap.substr( m_AdminGameMap.size( ) - 4 ) != ".cfg" )
639     {
640     m_AdminGameMap += ".cfg";
641     CONSOLE_Print( "[GHOST] adding \".cfg\" to default admin game map -> new default is [" + m_AdminGameMap + "]" );
642     }
643    
644     CONSOLE_Print( "[GHOST] trying to load default admin game map" );
645     CConfig AdminMapCFG;
646     AdminMapCFG.Read( m_MapCFGPath + m_AdminGameMap );
647     m_AdminMap = new CMap( this, &AdminMapCFG, m_MapCFGPath + m_AdminGameMap );
648    
649     if( !m_AdminMap->GetValid( ) )
650     {
651     CONSOLE_Print( "[GHOST] default admin game map isn't valid, using hardcoded admin game map instead" );
652     delete m_AdminMap;
653     m_AdminMap = new CMap( this );
654     }
655     }
656     else
657     {
658     CONSOLE_Print( "[GHOST] using hardcoded admin game map" );
659     m_AdminMap = new CMap( this );
660     }
661    
662     m_AutoHostMap = new CMap( *m_Map );
663     m_SaveGame = new CSaveGame( );
664    
665     // load the iptocountry data
666    
667     LoadIPToCountryData( );
668    
669     // create the admin game
670    
671     if( m_AdminGameCreate )
672     {
673     CONSOLE_Print( "[GHOST] creating admin game" );
674     m_AdminGame = new CAdminGame( this, m_AdminMap, NULL, m_AdminGamePort, 0, "GHost++ Admin Game", m_AdminGamePassword );
675    
676     if( m_AdminGamePort == m_HostPort )
677     CONSOLE_Print( "[GHOST] warning - admingame_port and bot_hostport are set to the same value, you won't be able to host any games" );
678     }
679     else
680     m_AdminGame = NULL;
681    
682     if( m_BNETs.empty( ) && !m_AdminGame )
683     CONSOLE_Print( "[GHOST] warning - no battle.net connections found and no admin game created" );
684    
685     #ifdef GHOST_MYSQL
686     CONSOLE_Print( "[GHOST] GHost++ Version " + m_Version + " (with MySQL support)" );
687     #else
688     CONSOLE_Print( "[GHOST] GHost++ Version " + m_Version + " (without MySQL support)" );
689     #endif
690     }
691    
692     CGHost :: ~CGHost( )
693     {
694     delete m_UDPSocket;
695     delete m_ReconnectSocket;
696    
697     for( vector<CTCPSocket *> :: iterator i = m_ReconnectSockets.begin( ); i != m_ReconnectSockets.end( ); i++ )
698     delete *i;
699    
700     delete m_GPSProtocol;
701     delete m_CRC;
702     delete m_SHA;
703    
704     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
705     delete *i;
706    
707     delete m_CurrentGame;
708     delete m_AdminGame;
709    
710     for( vector<CBaseGame *> :: iterator i = m_Games.begin( ); i != m_Games.end( ); i++ )
711     delete *i;
712    
713     delete m_DB;
714     delete m_DBLocal;
715    
716     // warning: we don't delete any entries of m_Callables here because we can't be guaranteed that the associated threads have terminated
717     // this is fine if the program is currently exiting because the OS will clean up after us
718     // but if you try to recreate the CGHost object within a single session you will probably leak resources!
719    
720     if( !m_Callables.empty( ) )
721     CONSOLE_Print( "[GHOST] warning - " + UTIL_ToString( m_Callables.size( ) ) + " orphaned callables were leaked (this is not an error)" );
722    
723     delete m_Language;
724     delete m_Map;
725     delete m_AdminMap;
726     delete m_AutoHostMap;
727     delete m_SaveGame;
728     }
729    
730     bool CGHost :: Update( long usecBlock )
731     {
732     // todotodo: do we really want to shutdown if there's a database error? is there any way to recover from this?
733    
734     if( m_DB->HasError( ) )
735     {
736     CONSOLE_Print( "[GHOST] database error - " + m_DB->GetError( ) );
737     return true;
738     }
739    
740     if( m_DBLocal->HasError( ) )
741     {
742     CONSOLE_Print( "[GHOST] local database error - " + m_DBLocal->GetError( ) );
743     return true;
744     }
745    
746     // try to exit nicely if requested to do so
747    
748     if( m_ExitingNice )
749     {
750     if( !m_BNETs.empty( ) )
751     {
752     CONSOLE_Print( "[GHOST] deleting all battle.net connections in preparation for exiting nicely" );
753    
754     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
755     delete *i;
756    
757     m_BNETs.clear( );
758     }
759    
760     if( m_CurrentGame )
761     {
762     CONSOLE_Print( "[GHOST] deleting current game in preparation for exiting nicely" );
763     delete m_CurrentGame;
764     m_CurrentGame = NULL;
765     }
766    
767     if( m_AdminGame )
768     {
769     CONSOLE_Print( "[GHOST] deleting admin game in preparation for exiting nicely" );
770     delete m_AdminGame;
771     m_AdminGame = NULL;
772     }
773    
774     if( m_Games.empty( ) )
775     {
776     if( !m_AllGamesFinished )
777     {
778     CONSOLE_Print( "[GHOST] all games finished, waiting 60 seconds for threads to finish" );
779     CONSOLE_Print( "[GHOST] there are " + UTIL_ToString( m_Callables.size( ) ) + " threads in progress" );
780     m_AllGamesFinished = true;
781     m_AllGamesFinishedTime = GetTime( );
782     }
783     else
784     {
785     if( m_Callables.empty( ) )
786     {
787     CONSOLE_Print( "[GHOST] all threads finished, exiting nicely" );
788     m_Exiting = true;
789     }
790     else if( GetTime( ) - m_AllGamesFinishedTime >= 60 )
791     {
792     CONSOLE_Print( "[GHOST] waited 60 seconds for threads to finish, exiting anyway" );
793     CONSOLE_Print( "[GHOST] there are " + UTIL_ToString( m_Callables.size( ) ) + " threads still in progress which will be terminated" );
794     m_Exiting = true;
795     }
796     }
797     }
798     }
799    
800     // update callables
801    
802     for( vector<CBaseCallable *> :: iterator i = m_Callables.begin( ); i != m_Callables.end( ); )
803     {
804     if( (*i)->GetReady( ) )
805     {
806     m_DB->RecoverCallable( *i );
807     delete *i;
808     i = m_Callables.erase( i );
809     }
810     else
811     i++;
812     }
813    
814     // create the GProxy++ reconnect listener
815    
816     if( m_Reconnect )
817     {
818     if( !m_ReconnectSocket )
819     {
820     m_ReconnectSocket = new CTCPServer( );
821    
822     if( m_ReconnectSocket->Listen( m_BindAddress, m_ReconnectPort ) )
823     CONSOLE_Print( "[GHOST] listening for GProxy++ reconnects on port " + UTIL_ToString( m_ReconnectPort ) );
824     else
825     {
826     CONSOLE_Print( "[GHOST] error listening for GProxy++ reconnects on port " + UTIL_ToString( m_ReconnectPort ) );
827     delete m_ReconnectSocket;
828     m_ReconnectSocket = NULL;
829     m_Reconnect = false;
830     }
831     }
832     else if( m_ReconnectSocket->HasError( ) )
833     {
834     CONSOLE_Print( "[GHOST] GProxy++ reconnect listener error (" + m_ReconnectSocket->GetErrorString( ) + ")" );
835     delete m_ReconnectSocket;
836     m_ReconnectSocket = NULL;
837     m_Reconnect = false;
838     }
839     }
840    
841     unsigned int NumFDs = 0;
842    
843     // take every socket we own and throw it in one giant select statement so we can block on all sockets
844    
845     int nfds = 0;
846     fd_set fd;
847     fd_set send_fd;
848     FD_ZERO( &fd );
849     FD_ZERO( &send_fd );
850    
851     // 1. all battle.net sockets
852    
853     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
854     NumFDs += (*i)->SetFD( &fd, &send_fd, &nfds );
855    
856     // 2. the current game's server and player sockets
857    
858     if( m_CurrentGame )
859     NumFDs += m_CurrentGame->SetFD( &fd, &send_fd, &nfds );
860    
861     // 3. the admin game's server and player sockets
862    
863     if( m_AdminGame )
864     NumFDs += m_AdminGame->SetFD( &fd, &send_fd, &nfds );
865    
866     // 4. all running games' player sockets
867    
868     for( vector<CBaseGame *> :: iterator i = m_Games.begin( ); i != m_Games.end( ); i++ )
869     NumFDs += (*i)->SetFD( &fd, &send_fd, &nfds );
870    
871     // 5. the GProxy++ reconnect socket(s)
872    
873     if( m_Reconnect && m_ReconnectSocket )
874     {
875     m_ReconnectSocket->SetFD( &fd, &send_fd, &nfds );
876     NumFDs++;
877     }
878    
879     for( vector<CTCPSocket *> :: iterator i = m_ReconnectSockets.begin( ); i != m_ReconnectSockets.end( ); i++ )
880     {
881     (*i)->SetFD( &fd, &send_fd, &nfds );
882     NumFDs++;
883     }
884    
885     // before we call select we need to determine how long to block for
886     // previously we just blocked for a maximum of the passed usecBlock microseconds
887     // however, in an effort to make game updates happen closer to the desired latency setting we now use a dynamic block interval
888     // note: we still use the passed usecBlock as a hard maximum
889    
890     for( vector<CBaseGame *> :: iterator i = m_Games.begin( ); i != m_Games.end( ); i++ )
891     {
892     if( (*i)->GetNextTimedActionTicks( ) * 1000 < usecBlock )
893     usecBlock = (*i)->GetNextTimedActionTicks( ) * 1000;
894     }
895    
896     // always block for at least 1ms just in case something goes wrong
897     // this prevents the bot from sucking up all the available CPU if a game keeps asking for immediate updates
898     // it's a bit ridiculous to include this check since, in theory, the bot is programmed well enough to never make this mistake
899     // however, considering who programmed it, it's worthwhile to do it anyway
900    
901     if( usecBlock < 1000 )
902     usecBlock = 1000;
903    
904     struct timeval tv;
905     tv.tv_sec = 0;
906     tv.tv_usec = usecBlock;
907    
908     struct timeval send_tv;
909     send_tv.tv_sec = 0;
910     send_tv.tv_usec = 0;
911    
912     #ifdef WIN32
913     select( 1, &fd, NULL, NULL, &tv );
914     select( 1, NULL, &send_fd, NULL, &send_tv );
915     #else
916     select( nfds + 1, &fd, NULL, NULL, &tv );
917     select( nfds + 1, NULL, &send_fd, NULL, &send_tv );
918     #endif
919    
920     if( NumFDs == 0 )
921     {
922     // we don't have any sockets (i.e. we aren't connected to battle.net maybe due to a lost connection and there aren't any games running)
923     // select will return immediately and we'll chew up the CPU if we let it loop so just sleep for 50ms to kill some time
924    
925     MILLISLEEP( 50 );
926     }
927    
928     bool AdminExit = false;
929     bool BNETExit = false;
930    
931     // update current game
932    
933     if( m_CurrentGame )
934     {
935     if( m_CurrentGame->Update( &fd, &send_fd ) )
936     {
937     CONSOLE_Print( "[GHOST] deleting current game [" + m_CurrentGame->GetGameName( ) + "]" );
938     delete m_CurrentGame;
939     m_CurrentGame = NULL;
940    
941     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
942     {
943     (*i)->QueueGameUncreate( );
944     (*i)->QueueEnterChat( );
945     }
946     }
947     else if( m_CurrentGame )
948     m_CurrentGame->UpdatePost( &send_fd );
949     }
950    
951     // update admin game
952    
953     if( m_AdminGame )
954     {
955     if( m_AdminGame->Update( &fd, &send_fd ) )
956     {
957     CONSOLE_Print( "[GHOST] deleting admin game" );
958     delete m_AdminGame;
959     m_AdminGame = NULL;
960     AdminExit = true;
961     }
962     else if( m_AdminGame )
963     m_AdminGame->UpdatePost( &send_fd );
964     }
965    
966     // update running games
967    
968     for( vector<CBaseGame *> :: iterator i = m_Games.begin( ); i != m_Games.end( ); )
969     {
970     if( (*i)->Update( &fd, &send_fd ) )
971     {
972     CONSOLE_Print( "[GHOST] deleting game [" + (*i)->GetGameName( ) + "]" );
973     EventGameDeleted( *i );
974     delete *i;
975     i = m_Games.erase( i );
976     }
977     else
978     {
979     (*i)->UpdatePost( &send_fd );
980     i++;
981     }
982     }
983    
984     // update battle.net connections
985    
986     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
987     {
988     if( (*i)->Update( &fd, &send_fd ) )
989     BNETExit = true;
990     }
991    
992     // update GProxy++ reliable reconnect sockets
993    
994     if( m_Reconnect && m_ReconnectSocket )
995     {
996     CTCPSocket *NewSocket = m_ReconnectSocket->Accept( &fd );
997    
998     if( NewSocket )
999     m_ReconnectSockets.push_back( NewSocket );
1000     }
1001    
1002     for( vector<CTCPSocket *> :: iterator i = m_ReconnectSockets.begin( ); i != m_ReconnectSockets.end( ); )
1003     {
1004     if( (*i)->HasError( ) || !(*i)->GetConnected( ) || GetTime( ) - (*i)->GetLastRecv( ) >= 10 )
1005     {
1006     delete *i;
1007     i = m_ReconnectSockets.erase( i );
1008     continue;
1009     }
1010    
1011     (*i)->DoRecv( &fd );
1012     string *RecvBuffer = (*i)->GetBytes( );
1013     BYTEARRAY Bytes = UTIL_CreateByteArray( (unsigned char *)RecvBuffer->c_str( ), RecvBuffer->size( ) );
1014    
1015     // a packet is at least 4 bytes
1016    
1017     if( Bytes.size( ) >= 4 )
1018     {
1019     if( Bytes[0] == GPS_HEADER_CONSTANT )
1020     {
1021     // bytes 2 and 3 contain the length of the packet
1022    
1023     uint16_t Length = UTIL_ByteArrayToUInt16( Bytes, false, 2 );
1024    
1025     if( Length >= 4 )
1026     {
1027     if( Bytes.size( ) >= Length )
1028     {
1029     if( Bytes[1] == CGPSProtocol :: GPS_RECONNECT && Length == 13 )
1030     {
1031     unsigned char PID = Bytes[4];
1032     uint32_t ReconnectKey = UTIL_ByteArrayToUInt32( Bytes, false, 5 );
1033     uint32_t LastPacket = UTIL_ByteArrayToUInt32( Bytes, false, 9 );
1034    
1035     // look for a matching player in a running game
1036    
1037     CGamePlayer *Match = NULL;
1038    
1039     for( vector<CBaseGame *> :: iterator j = m_Games.begin( ); j != m_Games.end( ); j++ )
1040     {
1041     if( (*j)->GetGameLoaded( ) )
1042     {
1043     CGamePlayer *Player = (*j)->GetPlayerFromPID( PID );
1044    
1045     if( Player && Player->GetGProxy( ) && Player->GetGProxyReconnectKey( ) == ReconnectKey )
1046     {
1047     Match = Player;
1048     break;
1049     }
1050     }
1051     }
1052    
1053     if( Match )
1054     {
1055     // reconnect successful!
1056    
1057     *RecvBuffer = RecvBuffer->substr( Length );
1058     Match->EventGProxyReconnect( *i, LastPacket );
1059     i = m_ReconnectSockets.erase( i );
1060     continue;
1061     }
1062     else
1063     {
1064     (*i)->PutBytes( m_GPSProtocol->SEND_GPSS_REJECT( REJECTGPS_NOTFOUND ) );
1065     (*i)->DoSend( &send_fd );
1066     delete *i;
1067     i = m_ReconnectSockets.erase( i );
1068     continue;
1069     }
1070     }
1071     else
1072     {
1073     (*i)->PutBytes( m_GPSProtocol->SEND_GPSS_REJECT( REJECTGPS_INVALID ) );
1074     (*i)->DoSend( &send_fd );
1075     delete *i;
1076     i = m_ReconnectSockets.erase( i );
1077     continue;
1078     }
1079     }
1080     }
1081     else
1082     {
1083     (*i)->PutBytes( m_GPSProtocol->SEND_GPSS_REJECT( REJECTGPS_INVALID ) );
1084     (*i)->DoSend( &send_fd );
1085     delete *i;
1086     i = m_ReconnectSockets.erase( i );
1087     continue;
1088     }
1089     }
1090     else
1091     {
1092     (*i)->PutBytes( m_GPSProtocol->SEND_GPSS_REJECT( REJECTGPS_INVALID ) );
1093     (*i)->DoSend( &send_fd );
1094     delete *i;
1095     i = m_ReconnectSockets.erase( i );
1096     continue;
1097     }
1098     }
1099    
1100     (*i)->DoSend( &send_fd );
1101     i++;
1102     }
1103    
1104     // autohost
1105    
1106     if( !m_AutoHostGameName.empty( ) && m_AutoHostMaximumGames != 0 && m_AutoHostAutoStartPlayers != 0 && GetTime( ) - m_LastAutoHostTime >= 30 )
1107     {
1108     // copy all the checks from CGHost :: CreateGame here because we don't want to spam the chat when there's an error
1109     // instead we fail silently and try again soon
1110    
1111     if( !m_ExitingNice && m_Enabled && !m_CurrentGame && m_Games.size( ) < m_MaxGames && m_Games.size( ) < m_AutoHostMaximumGames )
1112     {
1113     if( m_AutoHostMap->GetValid( ) )
1114     {
1115     string GameName = m_AutoHostGameName + " #" + UTIL_ToString( m_HostCounter );
1116    
1117     if( GameName.size( ) <= 31 )
1118     {
1119     CreateGame( m_AutoHostMap, GAME_PUBLIC, false, GameName, m_AutoHostOwner, m_AutoHostOwner, m_AutoHostServer, false );
1120    
1121     if( m_CurrentGame )
1122     {
1123     m_CurrentGame->SetAutoStartPlayers( m_AutoHostAutoStartPlayers );
1124    
1125     if( m_AutoHostMatchMaking )
1126     {
1127     if( !m_Map->GetMapMatchMakingCategory( ).empty( ) )
1128     {
1129     if( !( m_Map->GetMapOptions( ) & MAPOPT_FIXEDPLAYERSETTINGS ) )
1130     CONSOLE_Print( "[GHOST] autohostmm - map_matchmakingcategory [" + m_Map->GetMapMatchMakingCategory( ) + "] found but matchmaking can only be used with fixed player settings, matchmaking disabled" );
1131     else
1132     {
1133     CONSOLE_Print( "[GHOST] autohostmm - map_matchmakingcategory [" + m_Map->GetMapMatchMakingCategory( ) + "] found, matchmaking enabled" );
1134    
1135     m_CurrentGame->SetMatchMaking( true );
1136     m_CurrentGame->SetMinimumScore( m_AutoHostMinimumScore );
1137     m_CurrentGame->SetMaximumScore( m_AutoHostMaximumScore );
1138     }
1139     }
1140     else
1141     CONSOLE_Print( "[GHOST] autohostmm - map_matchmakingcategory not found, matchmaking disabled" );
1142     }
1143     }
1144     }
1145     else
1146     {
1147     CONSOLE_Print( "[GHOST] stopped auto hosting, next game name [" + GameName + "] is too long (the maximum is 31 characters)" );
1148     m_AutoHostGameName.clear( );
1149     m_AutoHostOwner.clear( );
1150     m_AutoHostServer.clear( );
1151     m_AutoHostMaximumGames = 0;
1152     m_AutoHostAutoStartPlayers = 0;
1153     m_AutoHostMatchMaking = false;
1154     m_AutoHostMinimumScore = 0.0;
1155     m_AutoHostMaximumScore = 0.0;
1156     }
1157     }
1158     else
1159     {
1160     CONSOLE_Print( "[GHOST] stopped auto hosting, map config file [" + m_AutoHostMap->GetCFGFile( ) + "] is invalid" );
1161     m_AutoHostGameName.clear( );
1162     m_AutoHostOwner.clear( );
1163     m_AutoHostServer.clear( );
1164     m_AutoHostMaximumGames = 0;
1165     m_AutoHostAutoStartPlayers = 0;
1166     m_AutoHostMatchMaking = false;
1167     m_AutoHostMinimumScore = 0.0;
1168     m_AutoHostMaximumScore = 0.0;
1169     }
1170     }
1171    
1172     m_LastAutoHostTime = GetTime( );
1173     }
1174    
1175     return m_Exiting || AdminExit || BNETExit;
1176     }
1177    
1178     void CGHost :: EventBNETConnecting( CBNET *bnet )
1179     {
1180     if( m_AdminGame )
1181     m_AdminGame->SendAllChat( m_Language->ConnectingToBNET( bnet->GetServer( ) ) );
1182    
1183     if( m_CurrentGame )
1184     m_CurrentGame->SendAllChat( m_Language->ConnectingToBNET( bnet->GetServer( ) ) );
1185     }
1186    
1187     void CGHost :: EventBNETConnected( CBNET *bnet )
1188     {
1189     if( m_AdminGame )
1190     m_AdminGame->SendAllChat( m_Language->ConnectedToBNET( bnet->GetServer( ) ) );
1191    
1192     if( m_CurrentGame )
1193     m_CurrentGame->SendAllChat( m_Language->ConnectedToBNET( bnet->GetServer( ) ) );
1194     }
1195    
1196     void CGHost :: EventBNETDisconnected( CBNET *bnet )
1197     {
1198     if( m_AdminGame )
1199     m_AdminGame->SendAllChat( m_Language->DisconnectedFromBNET( bnet->GetServer( ) ) );
1200    
1201     if( m_CurrentGame )
1202     m_CurrentGame->SendAllChat( m_Language->DisconnectedFromBNET( bnet->GetServer( ) ) );
1203     }
1204    
1205     void CGHost :: EventBNETLoggedIn( CBNET *bnet )
1206     {
1207     if( m_AdminGame )
1208     m_AdminGame->SendAllChat( m_Language->LoggedInToBNET( bnet->GetServer( ) ) );
1209    
1210     if( m_CurrentGame )
1211     m_CurrentGame->SendAllChat( m_Language->LoggedInToBNET( bnet->GetServer( ) ) );
1212     }
1213    
1214     void CGHost :: EventBNETGameRefreshed( CBNET *bnet )
1215     {
1216     if( m_AdminGame )
1217     m_AdminGame->SendAllChat( m_Language->BNETGameHostingSucceeded( bnet->GetServer( ) ) );
1218    
1219     if( m_CurrentGame )
1220     m_CurrentGame->EventGameRefreshed( bnet->GetServer( ) );
1221     }
1222    
1223     void CGHost :: EventBNETGameRefreshFailed( CBNET *bnet )
1224     {
1225     if( m_CurrentGame )
1226     {
1227     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1228     {
1229     (*i)->QueueChatCommand( m_Language->UnableToCreateGameTryAnotherName( bnet->GetServer( ), m_CurrentGame->GetGameName( ) ) );
1230    
1231     if( (*i)->GetServer( ) == m_CurrentGame->GetCreatorServer( ) )
1232     (*i)->QueueChatCommand( m_Language->UnableToCreateGameTryAnotherName( bnet->GetServer( ), m_CurrentGame->GetGameName( ) ), m_CurrentGame->GetCreatorName( ), true );
1233     }
1234    
1235     if( m_AdminGame )
1236     m_AdminGame->SendAllChat( m_Language->BNETGameHostingFailed( bnet->GetServer( ), m_CurrentGame->GetGameName( ) ) );
1237    
1238     m_CurrentGame->SendAllChat( m_Language->UnableToCreateGameTryAnotherName( bnet->GetServer( ), m_CurrentGame->GetGameName( ) ) );
1239    
1240     // we take the easy route and simply close the lobby if a refresh fails
1241     // it's possible at least one refresh succeeded and therefore the game is still joinable on at least one battle.net (plus on the local network) but we don't keep track of that
1242     // we only close the game if it has no players since we support game rehosting (via !priv and !pub in the lobby)
1243    
1244     if( m_CurrentGame->GetNumHumanPlayers( ) == 0 )
1245     m_CurrentGame->SetExiting( true );
1246    
1247     m_CurrentGame->SetRefreshError( true );
1248     }
1249     }
1250    
1251     void CGHost :: EventBNETConnectTimedOut( CBNET *bnet )
1252     {
1253     if( m_AdminGame )
1254     m_AdminGame->SendAllChat( m_Language->ConnectingToBNETTimedOut( bnet->GetServer( ) ) );
1255    
1256     if( m_CurrentGame )
1257     m_CurrentGame->SendAllChat( m_Language->ConnectingToBNETTimedOut( bnet->GetServer( ) ) );
1258     }
1259    
1260     void CGHost :: EventBNETWhisper( CBNET *bnet, string user, string message )
1261     {
1262     if( m_AdminGame )
1263     {
1264     m_AdminGame->SendAdminChat( "[W: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1265    
1266     if( m_CurrentGame )
1267     m_CurrentGame->SendLocalAdminChat( "[W: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1268    
1269     for( vector<CBaseGame *> :: iterator i = m_Games.begin( ); i != m_Games.end( ); i++ )
1270     (*i)->SendLocalAdminChat( "[W: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1271     }
1272     }
1273    
1274     void CGHost :: EventBNETChat( CBNET *bnet, string user, string message )
1275     {
1276     if( m_AdminGame )
1277     {
1278     m_AdminGame->SendAdminChat( "[L: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1279    
1280     if( m_CurrentGame )
1281     m_CurrentGame->SendLocalAdminChat( "[L: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1282    
1283     for( vector<CBaseGame *> :: iterator i = m_Games.begin( ); i != m_Games.end( ); i++ )
1284     (*i)->SendLocalAdminChat( "[L: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1285     }
1286     }
1287    
1288     void CGHost :: EventBNETEmote( CBNET *bnet, string user, string message )
1289     {
1290     if( m_AdminGame )
1291     {
1292     m_AdminGame->SendAdminChat( "[E: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1293    
1294     if( m_CurrentGame )
1295     m_CurrentGame->SendLocalAdminChat( "[E: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1296    
1297     for( vector<CBaseGame *> :: iterator i = m_Games.begin( ); i != m_Games.end( ); i++ )
1298     (*i)->SendLocalAdminChat( "[E: " + bnet->GetServerAlias( ) + "] [" + user + "] " + message );
1299     }
1300     }
1301    
1302     void CGHost :: EventGameDeleted( CBaseGame *game )
1303     {
1304     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1305     {
1306     (*i)->QueueChatCommand( m_Language->GameIsOver( game->GetDescription( ) ) );
1307    
1308     if( (*i)->GetServer( ) == game->GetCreatorServer( ) )
1309     (*i)->QueueChatCommand( m_Language->GameIsOver( game->GetDescription( ) ), game->GetCreatorName( ), true );
1310     }
1311     }
1312    
1313     void CGHost :: ReloadConfigs( )
1314     {
1315     CConfig CFG;
1316     CFG.Read( "default.cfg" );
1317     CFG.Read( gCFGFile );
1318     SetConfigs( &CFG );
1319     }
1320    
1321     void CGHost :: SetConfigs( CConfig *CFG )
1322     {
1323     // this doesn't set EVERY config value since that would potentially require reconfiguring the battle.net connections
1324     // it just set the easily reloadable values
1325    
1326     m_LanguageFile = CFG->GetString( "bot_language", "language.cfg" );
1327     delete m_Language;
1328     m_Language = new CLanguage( m_LanguageFile );
1329     m_Warcraft3Path = UTIL_AddPathSeperator( CFG->GetString( "bot_war3path", "C:\\Program Files\\Warcraft III\\" ) );
1330     m_BindAddress = CFG->GetString( "bot_bindaddress", string( ) );
1331     m_ReconnectWaitTime = CFG->GetInt( "bot_reconnectwaittime", 3 );
1332     m_MaxGames = CFG->GetInt( "bot_maxgames", 5 );
1333     string BotCommandTrigger = CFG->GetString( "bot_commandtrigger", "!" );
1334    
1335     if( BotCommandTrigger.empty( ) )
1336     BotCommandTrigger = "!";
1337    
1338     m_CommandTrigger = BotCommandTrigger[0];
1339     m_MapCFGPath = UTIL_AddPathSeperator( CFG->GetString( "bot_mapcfgpath", string( ) ) );
1340     m_SaveGamePath = UTIL_AddPathSeperator( CFG->GetString( "bot_savegamepath", string( ) ) );
1341     m_MapPath = UTIL_AddPathSeperator( CFG->GetString( "bot_mappath", string( ) ) );
1342     m_SaveReplays = CFG->GetInt( "bot_savereplays", 0 ) == 0 ? false : true;
1343     m_ReplayPath = UTIL_AddPathSeperator( CFG->GetString( "bot_replaypath", string( ) ) );
1344     m_VirtualHostName = CFG->GetString( "bot_virtualhostname", "|cFF4080C0GHost" );
1345     m_HideIPAddresses = CFG->GetInt( "bot_hideipaddresses", 0 ) == 0 ? false : true;
1346     m_CheckMultipleIPUsage = CFG->GetInt( "bot_checkmultipleipusage", 1 ) == 0 ? false : true;
1347    
1348     if( m_VirtualHostName.size( ) > 15 )
1349     {
1350     m_VirtualHostName = "|cFF4080C0GHost";
1351     CONSOLE_Print( "[GHOST] warning - bot_virtualhostname is longer than 15 characters, using default virtual host name" );
1352     }
1353    
1354     m_SpoofChecks = CFG->GetInt( "bot_spoofchecks", 2 );
1355     m_RequireSpoofChecks = CFG->GetInt( "bot_requirespoofchecks", 0 ) == 0 ? false : true;
1356     m_ReserveAdmins = CFG->GetInt( "bot_reserveadmins", 1 ) == 0 ? false : true;
1357     m_RefreshMessages = CFG->GetInt( "bot_refreshmessages", 0 ) == 0 ? false : true;
1358     m_AutoLock = CFG->GetInt( "bot_autolock", 0 ) == 0 ? false : true;
1359     m_AutoSave = CFG->GetInt( "bot_autosave", 0 ) == 0 ? false : true;
1360     m_AllowDownloads = CFG->GetInt( "bot_allowdownloads", 0 );
1361     m_PingDuringDownloads = CFG->GetInt( "bot_pingduringdownloads", 0 ) == 0 ? false : true;
1362     m_MaxDownloaders = CFG->GetInt( "bot_maxdownloaders", 3 );
1363     m_MaxDownloadSpeed = CFG->GetInt( "bot_maxdownloadspeed", 100 );
1364     m_LCPings = CFG->GetInt( "bot_lcpings", 1 ) == 0 ? false : true;
1365     m_AutoKickPing = CFG->GetInt( "bot_autokickping", 400 );
1366     m_BanMethod = CFG->GetInt( "bot_banmethod", 1 );
1367     m_IPBlackListFile = CFG->GetString( "bot_ipblacklistfile", "ipblacklist.txt" );
1368     m_LobbyTimeLimit = CFG->GetInt( "bot_lobbytimelimit", 10 );
1369     m_Latency = CFG->GetInt( "bot_latency", 100 );
1370     m_SyncLimit = CFG->GetInt( "bot_synclimit", 50 );
1371     m_VoteKickAllowed = CFG->GetInt( "bot_votekickallowed", 1 ) == 0 ? false : true;
1372     m_VoteKickPercentage = CFG->GetInt( "bot_votekickpercentage", 100 );
1373    
1374     if( m_VoteKickPercentage > 100 )
1375     {
1376     m_VoteKickPercentage = 100;
1377     CONSOLE_Print( "[GHOST] warning - bot_votekickpercentage is greater than 100, using 100 instead" );
1378     }
1379    
1380     m_MOTDFile = CFG->GetString( "bot_motdfile", "motd.txt" );
1381     m_GameLoadedFile = CFG->GetString( "bot_gameloadedfile", "gameloaded.txt" );
1382     m_GameOverFile = CFG->GetString( "bot_gameoverfile", "gameover.txt" );
1383     m_LocalAdminMessages = CFG->GetInt( "bot_localadminmessages", 1 ) == 0 ? false : true;
1384     m_TCPNoDelay = CFG->GetInt( "tcp_nodelay", 0 ) == 0 ? false : true;
1385     m_MatchMakingMethod = CFG->GetInt( "bot_matchmakingmethod", 1 );
1386 zombieteam 5 m_KickGameSaver = CFG->GetInt( "bot_kickgamesaver", 0 ) == 0 ? false : true;
1387 zombieteam 2 }
1388    
1389     void CGHost :: ExtractScripts( )
1390     {
1391     string PatchMPQFileName = m_Warcraft3Path + "War3Patch.mpq";
1392     HANDLE PatchMPQ;
1393    
1394     if( SFileOpenArchive( PatchMPQFileName.c_str( ), 0, MPQ_OPEN_FORCE_MPQ_V1, &PatchMPQ ) )
1395     {
1396     CONSOLE_Print( "[GHOST] loading MPQ file [" + PatchMPQFileName + "]" );
1397     HANDLE SubFile;
1398    
1399     // common.j
1400    
1401     if( SFileOpenFileEx( PatchMPQ, "Scripts\\common.j", 0, &SubFile ) )
1402     {
1403     uint32_t FileLength = SFileGetFileSize( SubFile, NULL );
1404    
1405     if( FileLength > 0 && FileLength != 0xFFFFFFFF )
1406     {
1407     char *SubFileData = new char[FileLength];
1408     DWORD BytesRead = 0;
1409    
1410     if( SFileReadFile( SubFile, SubFileData, FileLength, &BytesRead ) )
1411     {
1412     CONSOLE_Print( "[GHOST] extracting Scripts\\common.j from MPQ file to [" + m_MapCFGPath + "common.j]" );
1413     UTIL_FileWrite( m_MapCFGPath + "common.j", (unsigned char *)SubFileData, BytesRead );
1414     }
1415     else
1416     CONSOLE_Print( "[GHOST] warning - unable to extract Scripts\\common.j from MPQ file" );
1417    
1418     delete [] SubFileData;
1419     }
1420    
1421     SFileCloseFile( SubFile );
1422     }
1423     else
1424     CONSOLE_Print( "[GHOST] couldn't find Scripts\\common.j in MPQ file" );
1425    
1426     // blizzard.j
1427    
1428     if( SFileOpenFileEx( PatchMPQ, "Scripts\\blizzard.j", 0, &SubFile ) )
1429     {
1430     uint32_t FileLength = SFileGetFileSize( SubFile, NULL );
1431    
1432     if( FileLength > 0 && FileLength != 0xFFFFFFFF )
1433     {
1434     char *SubFileData = new char[FileLength];
1435     DWORD BytesRead = 0;
1436    
1437     if( SFileReadFile( SubFile, SubFileData, FileLength, &BytesRead ) )
1438     {
1439     CONSOLE_Print( "[GHOST] extracting Scripts\\blizzard.j from MPQ file to [" + m_MapCFGPath + "blizzard.j]" );
1440     UTIL_FileWrite( m_MapCFGPath + "blizzard.j", (unsigned char *)SubFileData, BytesRead );
1441     }
1442     else
1443     CONSOLE_Print( "[GHOST] warning - unable to extract Scripts\\blizzard.j from MPQ file" );
1444    
1445     delete [] SubFileData;
1446     }
1447    
1448     SFileCloseFile( SubFile );
1449     }
1450     else
1451     CONSOLE_Print( "[GHOST] couldn't find Scripts\\blizzard.j in MPQ file" );
1452    
1453     SFileCloseArchive( PatchMPQ );
1454     }
1455     else
1456     CONSOLE_Print( "[GHOST] warning - unable to load MPQ file [" + PatchMPQFileName + "] - error code " + UTIL_ToString( GetLastError( ) ) );
1457     }
1458    
1459     void CGHost :: LoadIPToCountryData( )
1460     {
1461     ifstream in;
1462     in.open( "ip-to-country.csv" );
1463    
1464     if( in.fail( ) )
1465     CONSOLE_Print( "[GHOST] warning - unable to read file [ip-to-country.csv], iptocountry data not loaded" );
1466     else
1467     {
1468     CONSOLE_Print( "[GHOST] started loading [ip-to-country.csv]" );
1469    
1470     // the begin and commit statements are optimizations
1471     // we're about to insert ~4 MB of data into the database so if we allow the database to treat each insert as a transaction it will take a LONG time
1472     // todotodo: handle begin/commit failures a bit more gracefully
1473    
1474     if( !m_DBLocal->Begin( ) )
1475     CONSOLE_Print( "[GHOST] warning - failed to begin local database transaction, iptocountry data not loaded" );
1476     else
1477     {
1478     unsigned char Percent = 0;
1479     string Line;
1480     string IP1;
1481     string IP2;
1482     string Country;
1483     CSVParser parser;
1484    
1485     // get length of file for the progress meter
1486    
1487     in.seekg( 0, ios :: end );
1488     uint32_t FileLength = in.tellg( );
1489     in.seekg( 0, ios :: beg );
1490    
1491     while( !in.eof( ) )
1492     {
1493     getline( in, Line );
1494    
1495     if( Line.empty( ) )
1496     continue;
1497    
1498     parser << Line;
1499     parser >> IP1;
1500     parser >> IP2;
1501     parser >> Country;
1502     m_DBLocal->FromAdd( UTIL_ToUInt32( IP1 ), UTIL_ToUInt32( IP2 ), Country );
1503    
1504     // it's probably going to take awhile to load the iptocountry data (~10 seconds on my 3.2 GHz P4 when using SQLite3)
1505     // so let's print a progress meter just to keep the user from getting worried
1506    
1507     unsigned char NewPercent = (unsigned char)( (float)in.tellg( ) / FileLength * 100 );
1508    
1509     if( NewPercent != Percent && NewPercent % 10 == 0 )
1510     {
1511     Percent = NewPercent;
1512     CONSOLE_Print( "[GHOST] iptocountry data: " + UTIL_ToString( Percent ) + "% loaded" );
1513     }
1514     }
1515    
1516     if( !m_DBLocal->Commit( ) )
1517     CONSOLE_Print( "[GHOST] warning - failed to commit local database transaction, iptocountry data not loaded" );
1518     else
1519     CONSOLE_Print( "[GHOST] finished loading [ip-to-country.csv]" );
1520     }
1521    
1522     in.close( );
1523     }
1524     }
1525    
1526     void CGHost :: CreateGame( CMap *map, unsigned char gameState, bool saveGame, string gameName, string ownerName, string creatorName, string creatorServer, bool whisper )
1527     {
1528     if( !m_Enabled )
1529     {
1530     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1531     {
1532     if( (*i)->GetServer( ) == creatorServer )
1533     (*i)->QueueChatCommand( m_Language->UnableToCreateGameDisabled( gameName ), creatorName, whisper );
1534     }
1535    
1536     if( m_AdminGame )
1537     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameDisabled( gameName ) );
1538    
1539     return;
1540     }
1541    
1542     if( gameName.size( ) > 31 )
1543     {
1544     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1545     {
1546     if( (*i)->GetServer( ) == creatorServer )
1547     (*i)->QueueChatCommand( m_Language->UnableToCreateGameNameTooLong( gameName ), creatorName, whisper );
1548     }
1549    
1550     if( m_AdminGame )
1551     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameNameTooLong( gameName ) );
1552    
1553     return;
1554     }
1555    
1556     if( !map->GetValid( ) )
1557     {
1558     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1559     {
1560     if( (*i)->GetServer( ) == creatorServer )
1561     (*i)->QueueChatCommand( m_Language->UnableToCreateGameInvalidMap( gameName ), creatorName, whisper );
1562     }
1563    
1564     if( m_AdminGame )
1565     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameInvalidMap( gameName ) );
1566    
1567     return;
1568     }
1569    
1570     if( saveGame )
1571     {
1572     if( !m_SaveGame->GetValid( ) )
1573     {
1574     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1575     {
1576     if( (*i)->GetServer( ) == creatorServer )
1577     (*i)->QueueChatCommand( m_Language->UnableToCreateGameInvalidSaveGame( gameName ), creatorName, whisper );
1578     }
1579    
1580     if( m_AdminGame )
1581     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameInvalidSaveGame( gameName ) );
1582    
1583     return;
1584     }
1585    
1586     string MapPath1 = m_SaveGame->GetMapPath( );
1587     string MapPath2 = map->GetMapPath( );
1588     transform( MapPath1.begin( ), MapPath1.end( ), MapPath1.begin( ), (int(*)(int))tolower );
1589     transform( MapPath2.begin( ), MapPath2.end( ), MapPath2.begin( ), (int(*)(int))tolower );
1590    
1591     if( MapPath1 != MapPath2 )
1592     {
1593     CONSOLE_Print( "[GHOST] path mismatch, saved game path is [" + MapPath1 + "] but map path is [" + MapPath2 + "]" );
1594    
1595     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1596     {
1597     if( (*i)->GetServer( ) == creatorServer )
1598     (*i)->QueueChatCommand( m_Language->UnableToCreateGameSaveGameMapMismatch( gameName ), creatorName, whisper );
1599     }
1600    
1601     if( m_AdminGame )
1602     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameSaveGameMapMismatch( gameName ) );
1603    
1604     return;
1605     }
1606    
1607     if( m_EnforcePlayers.empty( ) )
1608     {
1609     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1610     {
1611     if( (*i)->GetServer( ) == creatorServer )
1612     (*i)->QueueChatCommand( m_Language->UnableToCreateGameMustEnforceFirst( gameName ), creatorName, whisper );
1613     }
1614    
1615     if( m_AdminGame )
1616     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameMustEnforceFirst( gameName ) );
1617    
1618     return;
1619     }
1620     }
1621    
1622     if( m_CurrentGame )
1623     {
1624     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1625     {
1626     if( (*i)->GetServer( ) == creatorServer )
1627     (*i)->QueueChatCommand( m_Language->UnableToCreateGameAnotherGameInLobby( gameName, m_CurrentGame->GetDescription( ) ), creatorName, whisper );
1628     }
1629    
1630     if( m_AdminGame )
1631     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameAnotherGameInLobby( gameName, m_CurrentGame->GetDescription( ) ) );
1632    
1633     return;
1634     }
1635    
1636     if( m_Games.size( ) >= m_MaxGames )
1637     {
1638     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1639     {
1640     if( (*i)->GetServer( ) == creatorServer )
1641     (*i)->QueueChatCommand( m_Language->UnableToCreateGameMaxGamesReached( gameName, UTIL_ToString( m_MaxGames ) ), creatorName, whisper );
1642     }
1643    
1644     if( m_AdminGame )
1645     m_AdminGame->SendAllChat( m_Language->UnableToCreateGameMaxGamesReached( gameName, UTIL_ToString( m_MaxGames ) ) );
1646    
1647     return;
1648     }
1649    
1650     CONSOLE_Print( "[GHOST] creating game [" + gameName + "]" );
1651    
1652     if( saveGame )
1653     m_CurrentGame = new CGame( this, map, m_SaveGame, m_HostPort, gameState, gameName, ownerName, creatorName, creatorServer );
1654     else
1655     m_CurrentGame = new CGame( this, map, NULL, m_HostPort, gameState, gameName, ownerName, creatorName, creatorServer );
1656    
1657     // todotodo: check if listening failed and report the error to the user
1658    
1659     if( m_SaveGame )
1660     {
1661     m_CurrentGame->SetEnforcePlayers( m_EnforcePlayers );
1662     m_EnforcePlayers.clear( );
1663     }
1664    
1665     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1666     {
1667     if( whisper && (*i)->GetServer( ) == creatorServer )
1668     {
1669     // note that we send this whisper only on the creator server
1670    
1671     if( gameState == GAME_PRIVATE )
1672     (*i)->QueueChatCommand( m_Language->CreatingPrivateGame( gameName, ownerName ), creatorName, whisper );
1673     else if( gameState == GAME_PUBLIC )
1674     (*i)->QueueChatCommand( m_Language->CreatingPublicGame( gameName, ownerName ), creatorName, whisper );
1675     }
1676     else
1677     {
1678     // note that we send this chat message on all other bnet servers
1679    
1680     if( gameState == GAME_PRIVATE )
1681     (*i)->QueueChatCommand( m_Language->CreatingPrivateGame( gameName, ownerName ) );
1682     else if( gameState == GAME_PUBLIC )
1683     (*i)->QueueChatCommand( m_Language->CreatingPublicGame( gameName, ownerName ) );
1684     }
1685    
1686     if( saveGame )
1687     (*i)->QueueGameCreate( gameState, gameName, string( ), map, m_SaveGame, m_CurrentGame->GetHostCounter( ) );
1688     else
1689     (*i)->QueueGameCreate( gameState, gameName, string( ), map, NULL, m_CurrentGame->GetHostCounter( ) );
1690     }
1691    
1692     if( m_AdminGame )
1693     {
1694     if( gameState == GAME_PRIVATE )
1695     m_AdminGame->SendAllChat( m_Language->CreatingPrivateGame( gameName, ownerName ) );
1696     else if( gameState == GAME_PUBLIC )
1697     m_AdminGame->SendAllChat( m_Language->CreatingPublicGame( gameName, ownerName ) );
1698     }
1699    
1700     // if we're creating a private game we don't need to send any game refresh messages so we can rejoin the chat immediately
1701     // unfortunately this doesn't work on PVPGN servers because they consider an enterchat message to be a gameuncreate message when in a game
1702     // so don't rejoin the chat if we're using PVPGN
1703    
1704     if( gameState == GAME_PRIVATE )
1705     {
1706     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1707     {
1708     if( (*i)->GetPasswordHashType( ) != "pvpgn" )
1709     (*i)->QueueEnterChat( );
1710     }
1711     }
1712    
1713     // hold friends and/or clan members
1714    
1715     for( vector<CBNET *> :: iterator i = m_BNETs.begin( ); i != m_BNETs.end( ); i++ )
1716     {
1717     if( (*i)->GetHoldFriends( ) )
1718     (*i)->HoldFriends( m_CurrentGame );
1719    
1720     if( (*i)->GetHoldClan( ) )
1721     (*i)->HoldClan( m_CurrentGame );
1722     }
1723     }

Send suggestions and bug reports to Sergey Poznyakoff
ViewVC Help
Powered by ViewVC 1.1.20