core/GameDataFetcher.cpp
author Borja Ferrer <faluco@alliedmods.net>
Fri Jul 04 19:04:33 2008 +0000 (2008-07-04)
changeset 2155 3f1fd7ab3d86
parent 2150 ef9e2b861ddf
child 2186 c991c043022d
permissions -rw-r--r--
fixed amb1801, IsFakeClient() wasn't working for TF2 causing false results and crashes
     1 /**
     2 * vim: set ts=4 :
     3 * =============================================================================
     4 * SourceMod
     5 * Copyright (C) 2004-2008 AlliedModders LLC.  All rights reserved.
     6 * =============================================================================
     7 *
     8 * This program is free software; you can redistribute it and/or modify it under
     9 * the terms of the GNU General Public License, version 3.0, as published by the
    10 * Free Software Foundation.
    11 * 
    12 * This program is distributed in the hope that it will be useful, but WITHOUT
    13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    14 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
    15 * details.
    16 *
    17 * You should have received a copy of the GNU General Public License along with
    18 * this program.  If not, see <http://www.gnu.org/licenses/>.
    19 *
    20 * As a special exception, AlliedModders LLC gives you permission to link the
    21 * code of this program (as well as its derivative works) to "Half-Life 2," the
    22 * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
    23 * by the Valve Corporation.  You must obey the GNU General Public License in
    24 * all respects for all other code used.  Additionally, AlliedModders LLC grants
    25 * this exception to all derivative works.  AlliedModders LLC defines further
    26 * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
    27 * or <http://www.sourcemod.net/license.php>.
    28 *
    29 * Version: $Id$
    30 */
    31 
    32 #include "GameDataFetcher.h"
    33 #include "bitbuf.h"
    34 
    35 #ifdef PLATFORM_WINDOWS
    36 #include <winsock2.h>
    37 #else
    38 #include <unistd.h>
    39 #include <sys/types.h>
    40 #include <sys/socket.h>
    41 #include <netinet/in.h>
    42 #include <netdb.h>
    43 #include <arpa/inet.h>
    44 
    45 #define INVALID_SOCKET -1
    46 #define closesocket close
    47 #endif
    48 
    49 #include "sh_vector.h"
    50 #include "sh_string.h"
    51 #include "sm_version.h"
    52 
    53 #ifdef ORANGEBOX_BUILD
    54 #include "convar_sm_ob.h"
    55 #else
    56 #include "convar_sm.h"
    57 #endif
    58 
    59 #include "sourcemm_api.h"
    60 #include "time.h"
    61 #include "TimerSys.h"
    62 #include "compat_wrappers.h"
    63 
    64 #define QUERY_MAX_LENGTH 1024
    65 
    66 BuildMD5ableBuffer g_MD5Builder;
    67 FetcherThread g_FetchThread;
    68 
    69 FILE *logfile = NULL;
    70 
    71 bool g_disableGameDataUpdate = false;
    72 bool g_restartAfterUpdate = false;
    73 
    74 void FetcherThread::RunThread( IThreadHandle *pHandle )
    75 {
    76 	char lock_path[PLATFORM_MAX_PATH];
    77 	g_SourceMod.BuildPath(Path_SM, lock_path, sizeof(lock_path), "data/temp");
    78 	g_LibSys.CreateFolder(lock_path);
    79 
    80 	g_SourceMod.BuildPath(Path_SM, lock_path, sizeof(lock_path), "data/temp/gamedata.lock");
    81 
    82 	g_Logger.LogMessage("Starting experimental gamedata update fetcher... please report problems to bugs.alliedmods.net");
    83 
    84 	char log_path[PLATFORM_MAX_PATH];
    85 	g_SourceMod.BuildPath(Path_SM, log_path, sizeof(log_path), "logs/gamedata");
    86 
    87 	g_LibSys.CreateFolder(log_path);
    88 
    89 	time_t t;
    90 	GetAdjustedTime(&t);
    91 	tm *curtime = localtime(&t);
    92 
    93 	g_SourceMod.BuildPath(Path_SM, log_path, sizeof(log_path), "logs/gamedata/L%02d%02d.log", curtime->tm_mon + 1, curtime->tm_mday);
    94 
    95 	logfile = fopen(log_path, "a");
    96 
    97 	if (!logfile)
    98 	{
    99 		g_Logger.LogError("Failed to create GameData log file");
   100 		return;
   101 	}
   102 
   103 	//Create a blank lock file
   104 	FILE *fp = fopen(lock_path, "w");
   105 	if (fp)
   106 	{
   107 		fclose(fp);
   108 	}
   109 
   110 	/* Create a new socket for this connection */
   111 	int socketDescriptor = ConnectSocket();
   112 
   113 	if (socketDescriptor == INVALID_SOCKET)
   114 	{
   115 		fclose(logfile);
   116 		unlink(lock_path);
   117 		return;
   118 	}
   119 
   120 	char query[QUERY_MAX_LENGTH];
   121 
   122 	/* Check for updated gamedata files */
   123 	int len = BuildGameDataQuery(query, QUERY_MAX_LENGTH);
   124 
   125 	if (len == 0)
   126 	{
   127 		g_Logger.LogToOpenFile(logfile, "Query Writing failed");
   128 
   129 		closesocket(socketDescriptor);
   130 		fclose(logfile);
   131 		unlink(lock_path);
   132 		return;
   133 	}
   134 
   135 	if (g_disableGameDataUpdate)
   136 	{
   137 		g_Logger.LogMessage("Skipping GameData Query due to DisableAutoUpdate being set to true");
   138 
   139 		closesocket(socketDescriptor);
   140 		fclose(logfile);
   141 		unlink(lock_path);
   142 		return;
   143 	}
   144 
   145 	int sent = NonBlockingSend(socketDescriptor, query, len);
   146 
   147 	g_Logger.LogToOpenFile(logfile, "Sent Query!");
   148 
   149 	if (sent == 0)
   150 	{
   151 		g_Logger.LogToOpenFile(logfile, "Failed to send data");
   152 
   153 		closesocket(socketDescriptor);
   154 		fclose(logfile);
   155 		unlink(lock_path);
   156 		return;
   157 	}
   158 
   159 	ProcessGameDataQuery(socketDescriptor);
   160 
   161 	/* And we're done! */
   162 	closesocket(socketDescriptor);
   163 	fclose(logfile);
   164 	unlink(lock_path);
   165 }
   166 
   167 void FetcherThread::OnTerminate( IThreadHandle *pHandle, bool cancel )
   168 {
   169 	//delete this;
   170 }
   171 
   172 int FetcherThread::BuildGameDataQuery( char *buffer, int maxlen )
   173 {
   174 	char gamedata_path[PLATFORM_MAX_PATH];
   175 	g_SourceMod.BuildPath(Path_SM, gamedata_path, sizeof(gamedata_path), "gamedata");
   176 
   177 	IDirectory *dir = g_LibSys.OpenDirectory(gamedata_path);
   178 
   179 	if (dir == NULL)
   180 	{
   181 		return 0;
   182 	}
   183 
   184 	bf_write Writer = bf_write("GameDataQuery", buffer, maxlen);
   185 
   186 	Writer.WriteByte('A'); //Generic Header char
   187 	Writer.WriteByte('G'); //G for gamedata query, or green, like my hat.
   188 
   189 	short build[4] = { SVN_FILE_VERSION };
   190 
   191 	Writer.WriteBytes(&build[0], 8);
   192 
   193 	Writer.WriteByte(0); // Initialize the file counter - Index 10
   194 
   195 	while (dir->MoreFiles())
   196 	{
   197 		if (dir->IsEntryFile())
   198 		{
   199 			const char *name = dir->GetEntryName();
   200 			size_t len = strlen(name);
   201 			if (len >= 4
   202 				&& strcmp(&name[len-4], ".txt") == 0)
   203 			{
   204 				char file[PLATFORM_MAX_PATH];
   205 
   206 				g_LibSys.PathFormat(file, sizeof(file), "%s/%s", gamedata_path, name);
   207 
   208 				SMCStates states;
   209 				if (g_TextParser.ParseFile_SMC(file, &g_MD5Builder, &states) == SMCError_Okay)
   210 				{
   211 					unsigned char *md5 = g_MD5Builder.GetMD5();
   212 					if (md5 != NULL)
   213 					{
   214 						(uint8_t)buffer[10]++; //Increment the file counter
   215 						Writer.WriteBytes(md5, 16);
   216 
   217 						g_Logger.LogToOpenFile(logfile, "%s - \"%s\"", file, g_MD5Builder.GetMD5String());
   218 
   219 						FileData *data = new FileData();
   220 						data->filename = new SourceHook::String(file);
   221 						memcpy(data->checksum, g_MD5Builder.GetMD5String(), 33);
   222 						filenames.push_back(data);
   223 					}
   224 					else
   225 					{
   226 						g_Logger.LogToOpenFile(logfile, "%s no md5?", file);
   227 					}
   228 				}
   229 				else
   230 				{
   231 					g_Logger.LogToOpenFile(logfile, "%s failed!", file);
   232 				}
   233 			}
   234 		}
   235 
   236 		dir->NextEntry();
   237 	}
   238 
   239 	return Writer.GetNumBytesWritten();
   240 }
   241 
   242 int FetcherThread::ConnectSocket()
   243 {
   244 	struct protoent *ptrp;
   245 	ptrp = getprotobyname("tcp");
   246 
   247 #ifdef WIN32
   248 	WSADATA wsaData;
   249 	WSAStartup(0x0101, &wsaData);
   250 #endif
   251 
   252 	if (ptrp == NULL) 
   253 	{
   254 		g_Logger.LogToOpenFile(logfile, "Failed to find TCP");
   255 		return INVALID_SOCKET;
   256 	}
   257 
   258 	int socketDescriptor = socket(AF_INET, SOCK_STREAM, ptrp->p_proto);
   259 
   260 	if (socketDescriptor == INVALID_SOCKET)
   261 	{
   262 		//bugger aye?
   263 		g_Logger.LogToOpenFile(logfile, "Failed to create a new socket");
   264 		closesocket(socketDescriptor);
   265 		return INVALID_SOCKET;
   266 	}
   267 
   268 	struct hostent *he;
   269 	struct sockaddr_in local_addr;
   270 
   271 	local_addr.sin_family = AF_INET;
   272 	local_addr.sin_port = htons((u_short)6500);
   273 
   274 	he = gethostbyname("hayate.alliedmods.net");
   275 
   276 	if (!he)
   277 	{
   278 		if ((local_addr.sin_addr.s_addr = inet_addr("hayate.alliedmods.net")) == INADDR_NONE)
   279 		{
   280 			g_Logger.LogToOpenFile(logfile, "Couldnt locate address");
   281 			closesocket(socketDescriptor);
   282 			return INVALID_SOCKET;
   283 		}
   284 	} 
   285 	else 
   286 	{
   287 		memcpy(&local_addr.sin_addr, (struct in_addr *)he->h_addr, he->h_length);
   288 	}
   289 
   290 	if (connect(socketDescriptor, (struct sockaddr *) &local_addr, sizeof(local_addr)) < 0)
   291 	{
   292 		g_Logger.LogToOpenFile(logfile, "Couldn't connect");
   293 		closesocket(socketDescriptor);
   294 		return INVALID_SOCKET;
   295 	}
   296 
   297 	return socketDescriptor;
   298 }
   299 
   300 void FetcherThread::ProcessGameDataQuery(int socketDescriptor)
   301 {
   302 	char buffer[50];
   303 
   304 	g_Logger.LogToOpenFile(logfile, "Waiting for reply!");
   305 
   306 	//Read in the header bytes
   307 	int returnLen = NonBlockingRecv(socketDescriptor, buffer, 12);
   308 
   309 	g_Logger.LogToOpenFile(logfile, "Recv Completed");
   310 
   311 	if (returnLen == 0)
   312 	{
   313 		g_Logger.LogToOpenFile(logfile, ",but it failed.");
   314 		/* Timeout or fail? */
   315 		return;
   316 	}
   317 
   318 	g_Logger.LogToOpenFile(logfile, "Received Header!");
   319 
   320 	bf_read Reader = bf_read("GameDataQuery", buffer, 12);
   321 
   322 	if (Reader.ReadByte() != 'A' || Reader.ReadByte() != 'G')
   323 	{
   324 		g_Logger.LogToOpenFile(logfile, "Invalid Query to handle");
   325 		return;
   326 	}
   327 
   328 	UpdateStatus updateStatus = (UpdateStatus)Reader.ReadByte();
   329 
   330 	short build[4] = {0,0,0,0};
   331 
   332 	build[0] = Reader.ReadShort();
   333 	build[1] = Reader.ReadShort();
   334 	build[2] = Reader.ReadShort();
   335 	build[3] = Reader.ReadShort();
   336 
   337 	g_Logger.LogToOpenFile(logfile, "Update Status: %i - Latest %i.%i.%i.%i", updateStatus, build[0], build[1], build[2], build[3]);
   338 
   339 	HandleUpdateStatus(updateStatus, build);
   340 
   341 	int changedFiles = Reader.ReadByte();
   342 
   343 	g_Logger.LogToOpenFile(logfile, "Files to download: %i", changedFiles);
   344 
   345 	for (int i=0; i<changedFiles; i++)
   346 	{
   347 		//Read in the file index and byte count
   348 		returnLen = NonBlockingRecv(socketDescriptor, buffer, 5);
   349 
   350 		if (returnLen == 0)
   351 		{
   352 			/* Timeout or fail? */
   353 			return;
   354 		}
   355 
   356 		Reader.StartReading(buffer, 5);
   357 
   358 		int index = Reader.ReadByte();
   359 		int tempLen = Reader.ReadUBitLong(32);
   360 
   361 		g_Logger.LogToOpenFile(logfile, "File index %i and length %i", index, tempLen);
   362 
   363 		void *memPtr;
   364 		memtable->CreateMem(tempLen+1, &memPtr);
   365 
   366 		//Read the contents of our file into the memtable
   367 		returnLen = NonBlockingRecv(socketDescriptor, (char *)memPtr, tempLen);
   368 
   369 		g_Logger.LogToOpenFile(logfile, "Recieved %i bytes", returnLen);
   370 
   371 		if (returnLen == 0)
   372 		{
   373 			/* Timeout or fail? */
   374 			return;
   375 		}
   376 
   377 		((unsigned char *)memPtr)[tempLen] = '\0';
   378 
   379 		FileData *data = filenames.at(index);
   380 		const char* filename;
   381 		if (data != NULL)
   382 		{
   383 			filename = data->filename->c_str();
   384 
   385 			FILE *fp = fopen(filename, "w");
   386 
   387 			if (fp)
   388 			{
   389 				fprintf(fp, (const char *)memPtr);
   390 				fclose(fp);
   391 			}
   392 		}
   393 		else
   394 		{
   395 			filename = "";
   396 		}
   397 
   398 		memtable->Reset();
   399 
   400 		g_Logger.LogToOpenFile(logfile, "Updated File %s", filename);
   401 	}
   402 
   403 	g_Logger.LogToOpenFile(logfile, "File Downloads Completed!");
   404 
   405 	bool needsRestart = false;
   406 
   407 	if (changedFiles > 0)
   408 	{
   409 		needsRestart = true;
   410 		g_Logger.LogMessage("New GameData Files have been downloaded to your gamedata directory. Please restart your server for these to take effect");
   411 	}
   412 
   413 	//Read changed file count
   414 	returnLen = NonBlockingRecv(socketDescriptor, buffer, 1);
   415 
   416 	if (returnLen == 0)
   417 	{
   418 		/* Timeout or fail? */
   419 		g_Logger.LogToOpenFile(logfile, "Failed to receive unknown count");
   420 		return;
   421 	}
   422 
   423 	Reader.StartReading(buffer, 1);
   424 
   425 	changedFiles = Reader.ReadByte();
   426 
   427 	if (changedFiles == 0)
   428 	{
   429 		g_Logger.LogToOpenFile(logfile, "No unknown files. We're all done");
   430 		return;
   431 	}
   432 
   433 	char *changedFileIndexes = new char[changedFiles];
   434 	
   435 	g_Logger.LogToOpenFile(logfile, "%i Files were unknown", changedFiles);
   436 	
   437 	returnLen = NonBlockingRecv(socketDescriptor, changedFileIndexes, changedFiles);
   438 
   439 	if (returnLen == 0)
   440 	{
   441 		/* Timeout or fail? */
   442 		g_Logger.LogToOpenFile(logfile, "Failed to receive unknown list");
   443 		return;
   444 	}
   445 
   446 	Reader.StartReading(changedFileIndexes, changedFiles);
   447 
   448 	for (int i=0; i<changedFiles; i++)
   449 	{
   450 		int index = Reader.ReadByte();
   451 		char fileName[30];
   452 
   453 		FileData *data = filenames.at(index);
   454 		const char* pathname;
   455 		if (data != NULL)
   456 		{
   457 			pathname = data->filename->c_str();
   458 		}
   459 		else
   460 		{
   461 			pathname = "";
   462 		}
   463 
   464 		g_LibSys.GetFileFromPath(fileName, sizeof(fileName), pathname);
   465 
   466 		g_Logger.LogToOpenFile(logfile, "Unknown File %i : %s", index, fileName);
   467 	}
   468 
   469 	delete [] changedFileIndexes;
   470 
   471 	if (needsRestart && g_restartAfterUpdate)
   472 	{
   473 		g_Logger.LogMessage("Automatically restarting server after a successful gamedata update!");
   474 		engine->ServerCommand("quit\n");
   475 	}
   476 }
   477 
   478 int FetcherThread::NonBlockingRecv( int socketDescriptor, char *buffer, int len )
   479 {
   480 	fd_set fds;
   481 	struct timeval tv;
   482 
   483 	/* Create a 10 Second Timeout */
   484 	tv.tv_sec = 10;
   485 	tv.tv_usec = 0;
   486 
   487 	/* Add our socket to a socket set */
   488 	FD_ZERO(&fds);
   489 	FD_SET(socketDescriptor, &fds);
   490 
   491 	/* Wait max of 10 seconds for recv to become available */
   492 	select(socketDescriptor+1, &fds, NULL, NULL, &tv);
   493 
   494 	int bytesReceived = 0;
   495 
   496 	/* Is there a limit on how much we can receive? Some site said 1024 bytes, which will be well short of a file */
   497 	if (FD_ISSET(socketDescriptor, &fds))
   498 	{
   499 		bytesReceived = recv(socketDescriptor, buffer, len, 0);
   500 	}
   501 
   502 	if (bytesReceived == 0 || bytesReceived == -1)
   503 	{
   504 		return 0;
   505 	}
   506 
   507 	if (bytesReceived < len)
   508 	{
   509 		return bytesReceived + NonBlockingRecv(socketDescriptor, buffer+bytesReceived, len-bytesReceived);
   510 	}
   511 
   512 	return bytesReceived;	
   513 }
   514 
   515 int FetcherThread::NonBlockingSend( int socketDescriptor, char *buffer, int len )
   516 {
   517 	fd_set fds;
   518 	struct timeval tv;
   519 
   520 	tv.tv_sec = 10;
   521 	tv.tv_usec = 0;
   522 
   523 	FD_ZERO(&fds);
   524 	FD_SET(socketDescriptor, &fds);
   525 
   526 	select(socketDescriptor+1, NULL, &fds, NULL, &tv);
   527 
   528 	int sentBytes = 0;
   529 
   530 	if (FD_ISSET(socketDescriptor, &fds))
   531 	{
   532 		sentBytes = send(socketDescriptor, buffer, len, 0);
   533 	}
   534 
   535 	if (sentBytes == 0 || sentBytes == -1)
   536 	{
   537 		return 0;
   538 	}
   539 
   540 	if (sentBytes < len)
   541 	{
   542 		return sentBytes + NonBlockingSend(socketDescriptor, buffer+sentBytes, len-sentBytes);
   543 	}
   544 
   545 	return sentBytes;
   546 }
   547 
   548 void FetcherThread::HandleUpdateStatus( UpdateStatus status, short version[4] )
   549 {
   550 	switch (status)
   551 	{
   552 		case Update_Unknown:
   553 		case Update_Current:
   554 		{
   555 			break;
   556 		}
   557 
   558 		case Update_NewBuild:
   559 		{
   560 			g_Logger.LogMessage("SourceMod Update: A new SVN build is available from sourcemod.net");
   561 			g_Logger.LogMessage("Current Version: %i.%i.%i.%i Available: %i.%i.%i.%i", version[0], version[1], version[2], version[3], version[0], version[1], version[2], version[3]);
   562 			break;
   563 		}
   564 
   565 		case Update_MinorAvailable:
   566 		{
   567 			g_Logger.LogMessage("SourceMod Update: An incremental minor release of SourceMod is now available from sourcemod.net");
   568 			g_Logger.LogMessage("Current Version: %i.%i.%i Available: %i.%i.%i", version[0], version[1], version[2], version[0], version[1], version[2]);
   569 			break;
   570 		}
   571 
   572 		case Update_MajorAvailable:
   573 		{
   574 			g_Logger.LogMessage("SourceMod Update: An major release of SourceMod is now available from sourcemod.net");
   575 			g_Logger.LogMessage("Current Version: %i.%i.%i Available: %i.%i.%i", version[0], version[1], version[2], version[0], version[1], version[2]);
   576 			break;
   577 		}
   578 
   579 		case Update_CriticalAvailable:
   580 		{
   581 			g_Logger.LogError("SourceMod Update: A new critical release of SourceMod is now available from sourcemod.net. It is strongly recommended that you update");
   582 			g_Logger.LogMessage("Current Version: %i.%i.%i.%i Available: %i.%i.%i.%i", version[0], version[1], version[2], version[3], version[0], version[1], version[2], version[3]);
   583 			break;
   584 		}
   585 	}
   586 }
   587 
   588 bool g_blockGameDataLoad = false;
   589 
   590 class InitFetch : public SMGlobalClass
   591 {
   592 public:
   593 	void OnSourceModAllInitialized_Post()
   594 	{
   595 		char lock_path[PLATFORM_MAX_PATH];
   596 		g_SourceMod.BuildPath(Path_SM, lock_path, sizeof(lock_path), "data/temp/gamedata.lock");
   597 
   598 		if (g_LibSys.IsPathFile(lock_path) && g_LibSys.PathExists(lock_path))
   599 		{
   600 			g_Logger.LogError("sourcemod/data/temp/gamedata.lock file detected. This is most likely due to a crash during GameData updating - Blocking GameData loading");
   601 			g_Logger.LogError("If this error persists delete the file manually");
   602 			g_blockGameDataLoad = true;
   603 		}
   604 
   605 		ThreadParams fetchThreadParams = ThreadParams();
   606 		fetchThreadParams.prio = ThreadPrio_Low;
   607 		g_pThreader->MakeThread(&g_FetchThread, &fetchThreadParams);
   608 	}
   609 
   610 	ConfigResult OnSourceModConfigChanged(const char *key, 
   611 		const char *value, 
   612 		ConfigSource source,
   613 		char *error, 
   614 		size_t maxlength)
   615 	{
   616 		if (strcmp(key, "DisableAutoUpdate") == 0)
   617 		{
   618 			if (strcmp(value, "yes") == 0)
   619 			{
   620 				g_disableGameDataUpdate = true;
   621 				return ConfigResult_Accept;
   622 			}
   623 			else if (strcmp(value, "no") == 0)
   624 			{
   625 				g_disableGameDataUpdate = false;
   626 				return ConfigResult_Accept;
   627 			}
   628 
   629 			return ConfigResult_Reject;
   630 		}
   631 
   632 		if (strcmp(key, "ForceRestartAfterUpdate") == 0)
   633 		{
   634 			if (strcmp(value, "yes") == 0)
   635 			{
   636 				g_restartAfterUpdate = true;
   637 				return ConfigResult_Accept;
   638 			}
   639 			else if (strcmp(value, "no") == 0)
   640 			{
   641 				g_restartAfterUpdate = false;
   642 				return ConfigResult_Accept;
   643 			}
   644 
   645 			return ConfigResult_Reject;
   646 		}
   647 
   648 		return ConfigResult_Ignore;
   649 	}
   650 } g_InitFetch;
   651 
   652 CON_COMMAND(sm_gamedata_md5, "Checks the MD5 sum for a given gamedata file")
   653 {
   654 #if !defined ORANGEBOX_BUILD
   655 	CCommand args;
   656 #endif
   657 
   658 	if (args.ArgC() < 2)
   659 	{
   660 		g_SMAPI->ConPrint("Usage: sm_gamedata_md5 <file>\n");
   661 		return;
   662 	}
   663 
   664 	const char *file = args.Arg(1);
   665 	if (!file || file[0] == '\0')
   666 	{
   667 		g_SMAPI->ConPrint("Usage: sm_gamedata_md5 <file>\n");
   668 		return;
   669 	}
   670 
   671 	SourceHook::CVector<FileData *>::iterator iter = g_FetchThread.filenames.begin();
   672 
   673 	FileData *curData;
   674 
   675 	while (iter != g_FetchThread.filenames.end())
   676 	{
   677 		curData = (*iter);
   678 
   679 		char fileName[30];
   680 
   681 		g_LibSys.GetFileFromPath(fileName, sizeof(fileName), curData->filename->c_str());
   682 
   683 		if (strcmpi(fileName, file) == 0)
   684 		{
   685 			g_SMAPI->ConPrintf("MD5 Sum: %s\n", curData->checksum);
   686 			return;
   687 		}
   688 
   689 		iter++;
   690 	}
   691 
   692 	g_SMAPI->ConPrint("File not found!\n");
   693 }