# ai_scanner.cpp

Go to the documentation of this file.

    00001 /* $Id: ai_scanner.cpp 15467 2009-02-13 17:17:34Z yexo $ */
    00002
    00005 #include "../stdafx.h"
    00006 #include "../debug.h"
    00007 #include "../string_func.h"
    00008 #include "../fileio_func.h"
    00009 #include "../fios.h"
    00010 #include "../network/network.h"
    00011 #include "../core/random_func.hpp"
    00012 #include <sys/stat.h>
    00013
    00014 #include <squirrel.h>
    00015 #include "../script/squirrel.hpp"
    00016 #include "../script/squirrel_helper.hpp"
    00017 #include "../script/squirrel_class.hpp"
    00018 #include "ai.hpp"
    00019 #include "ai_info.hpp"
    00020 #include "ai_scanner.hpp"
    00021 #include "api/ai_controller.hpp"
    00022
    00023 void AIScanner::ScanDir(const char *dirname, bool library_scan, bool library_recursive)
    00024 {
    00025   extern bool FiosIsValidFile(const char *path, const struct dirent *ent, struct stat *sb);
    00026   extern bool FiosIsHiddenFile(const struct dirent *ent);
    00027
    00028   char d_name[MAX_PATH];
    00029   char temp_script[1024];
    00030   struct stat sb;
    00031   struct dirent *dirent;
    00032   DIR *dir;
    00033
    00034   dir = ttd_opendir(dirname);
    00035   /* Dir not found, so do nothing */
    00036   if (dir == NULL) return;
    00037
    00038   /* Walk all dirs trying to find a dir in which 'main.nut' exists */
    00039   while ((dirent = readdir(dir)) != NULL) {
    00040     ttd_strlcpy(d_name, FS2OTTD(dirent->d_name), sizeof(d_name));
    00041
    00042     /* Valid file, not '.' or '..', not hidden */
    00043     if (!FiosIsValidFile(dirname, dirent, &sb)) continue;
    00044     if (strcmp(d_name, ".") == 0 || strcmp(d_name, "..") == 0) continue;
    00045     if (FiosIsHiddenFile(dirent) && strncasecmp(d_name, PERSONAL_DIR, strlen(d_name)) != 0) continue;
    00046
    00047     /* Create the full length dirname */
    00048     ttd_strlcpy(temp_script, dirname, sizeof(temp_script));
    00049     ttd_strlcat(temp_script, d_name,  sizeof(temp_script));
    00050
    00051     if (S_ISDIR(sb.st_mode)) {
    00052       /* Libraries are always in a subdirectory of their category, so scan those */
    00053       if (library_scan && !library_recursive) {
    00054         ttd_strlcat(temp_script, PATHSEP, sizeof(temp_script));
    00055         ScanDir(temp_script, library_scan, true);
    00056         continue;
    00057       }
    00058     } else if (S_ISREG(sb.st_mode)) {
    00059       /* Only .tar files are allowed */
    00060       char *ext = strrchr(d_name, '.');
    00061       if (ext == NULL || strcasecmp(ext, ".tar") != 0) continue;
    00062       /* .tar files are only allowed in the root of the library dir */
    00063       if (library_recursive) continue;
    00064
    00065       /* We always expect a directory in the TAR */
    00066       const char *first_dir = FioTarFirstDir(temp_script);
    00067       if (first_dir == NULL) continue;
    00068
    00069       ttd_strlcat(temp_script, PATHSEP, sizeof(temp_script));
    00070       ttd_strlcat(temp_script, first_dir, sizeof(temp_script));
    00071       FioTarAddLink(temp_script, first_dir);
    00072     } else {
    00073       /* Skip any other type of file */
    00074       continue;
    00075     }
    00076
    00077     /* Add an additional / where needed */
    00078     if (temp_script[strlen(temp_script) - 1] != PATHSEPCHAR) ttd_strlcat(temp_script, PATHSEP, sizeof(temp_script));
    00079
    00080     if (!library_scan) {
    00081       char info_script[MAX_PATH];
    00082       ttd_strlcpy(info_script, temp_script, sizeof(info_script));
    00083       ttd_strlcpy(main_script, temp_script, sizeof(main_script));
    00084
    00085       /* Every AI should contain an 'info.nut' and 'main.nut' file; else it is not a valid AI */
    00086       ttd_strlcat(info_script, "info.nut", sizeof(info_script));
    00087       ttd_strlcat(main_script, "main.nut", sizeof(main_script));
    00088       if (!FioCheckFileExists(info_script, AI_DIR) || !FioCheckFileExists(main_script, AI_DIR)) continue;
    00089
    00090       DEBUG(ai, 6, "Loading AI at location '%s'", main_script);
    00091       /* We don't care if one of the other scripst failed to load. */
    00092       this->engine->ResetCrashed();
    00093       this->engine->LoadScript(info_script);
    00094     } else {
    00095       char library_script[MAX_PATH];
    00096       ttd_strlcpy(library_script, temp_script, sizeof(library_script));
    00097       ttd_strlcpy(main_script, temp_script, sizeof(main_script));
    00098
    00099       /* Every library should contain an 'library.nut' and 'main.nut' file; else it is not a valid library */
    00100       ttd_strlcat(library_script, "library.nut", sizeof(library_script));
    00101       ttd_strlcat(main_script, "main.nut", sizeof(main_script));
    00102       if (!FioCheckFileExists(library_script, AI_LIBRARY_DIR) || !FioCheckFileExists(main_script, AI_LIBRARY_DIR)) continue;
    00103
    00104       DEBUG(ai, 6, "Loading AI Library at location '%s'", main_script);
    00105       /* We don't care if one of the other scripst failed to load. */
    00106       this->engine->ResetCrashed();
    00107       this->engine->LoadScript(library_script);
    00108     }
    00109   }
    00110   closedir(dir);
    00111 }
    00112
    00113 void AIScanner::ScanAIDir()
    00114 {
    00115   char buf[MAX_PATH];
    00116   Searchpath sp;
    00117
    00118   extern void ScanForTarFiles();
    00119   ScanForTarFiles();
    00120
    00121   FOR_ALL_SEARCHPATHS(sp) {
    00122     FioAppendDirectory(buf, MAX_PATH, sp, AI_DIR);
    00123     if (FileExists(buf)) this->ScanDir(buf, false);
    00124     FioAppendDirectory(buf, MAX_PATH, sp, AI_LIBRARY_DIR);
    00125     if (FileExists(buf)) this->ScanDir(buf, true);
    00126   }
    00127 }
    00128
    00129 void AIScanner::RescanAIDir()
    00130 {
    00131   this->ScanAIDir();
    00132 }
    00133
    00134 AIScanner::AIScanner() :
    00135   info_dummy(NULL)
    00136 {
    00137   this->engine = new Squirrel();
    00138   this->main_script[0] = '\0';
    00139
    00140   /* Create the AIInfo class, and add the RegisterAI function */
    00141   DefSQClass <AIInfo> SQAIInfo("AIInfo");
    00142   SQAIInfo.PreRegister(engine);
    00143   SQAIInfo.AddConstructor<void (AIInfo::*)(), 1>(engine, "x");
    00144   SQAIInfo.DefSQAdvancedMethod(this->engine, &AIInfo::AddSetting, "AddSetting");
    00145   SQAIInfo.DefSQAdvancedMethod(this->engine, &AIInfo::AddLabels, "AddLabels");
    00146   SQAIInfo.DefSQConst(engine, AICONFIG_RANDOM, "AICONFIG_RANDOM");
    00147   SQAIInfo.DefSQConst(engine, AICONFIG_BOOLEAN, "AICONFIG_BOOLEAN");
    00148   SQAIInfo.PostRegister(engine);
    00149   this->engine->AddMethod("RegisterAI", &AIInfo::Constructor, 2, "tx");
    00150   this->engine->AddMethod("RegisterDummyAI", &AIInfo::DummyConstructor, 2, "tx");
    00151
    00152   /* Create the AILibrary class, and add the RegisterLibrary function */
    00153   this->engine->AddClassBegin("AILibrary");
    00154   this->engine->AddClassEnd();
    00155   this->engine->AddMethod("RegisterLibrary", &AILibrary::Constructor, 2, "tx");
    00156
    00157   /* Mark this class as global pointer */
    00158   this->engine->SetGlobalPointer(this);
    00159
    00160   /* Scan the AI dir for scripts */
    00161   this->ScanAIDir();
    00162
    00163   /* Create the dummy AI */
    00164   this->engine->ResetCrashed();
    00165   extern void AI_CreateAIInfoDummy(HSQUIRRELVM vm);
    00166   AI_CreateAIInfoDummy(this->engine->GetVM());
    00167 }
    00168
    00169 AIScanner::~AIScanner()
    00170 {
    00171   AIInfoList::iterator it = this->info_list.begin();
    00172   for (; it != this->info_list.end(); it++) {
    00173     free((void *)(*it).first);
    00174     delete (*it).second;
    00175   }
    00176   it = this->info_single_list.begin();
    00177   for (; it != this->info_single_list.end(); it++) {
    00178     free((void *)(*it).first);
    00179   }
    00180   AILibraryList::iterator lit = this->library_list.begin();
    00181   for (; lit != this->library_list.end(); lit++) {
    00182     free((void *)(*lit).first);
    00183     delete (*lit).second;
    00184   }
    00185
    00186   delete this->info_dummy;
    00187   delete this->engine;
    00188 }
    00189
    00190 bool AIScanner::ImportLibrary(const char *library, const char *class_name, int version, HSQUIRRELVM vm, AIController *controller)
    00191 {
    00192   /* Internally we store libraries as 'library.version' */
    00193   char library_name[1024];
    00194   snprintf(library_name, sizeof(library_name), "%s.%d", library, version);
    00195   strtolower(library_name);
    00196
    00197   /* Check if the library + version exists */
    00198   AILibraryList::iterator iter = this->library_list.find(library_name);
    00199   if (iter == this->library_list.end()) {
    00200     char error[1024];
    00201
    00202     /* Now see if the version doesn't exist, or the library */
    00203     iter = this->library_list.find(library);
    00204     if (iter == this->library_list.end()) {
    00205       snprintf(error, sizeof(error), "couldn't find library '%s'", library);
    00206     } else {
    00207       snprintf(error, sizeof(error), "couldn't find library '%s' version %d. The latest version available is %d", library, version, (*iter).second->GetVersion());
    00208     }
    00209     sq_throwerror(vm, OTTD2FS(error));
    00210     return false;
    00211   }
    00212
    00213   /* Get the current table/class we belong to */
    00214   HSQOBJECT parent;
    00215   sq_getstackobj(vm, 1, &parent);
    00216
    00217   char fake_class[1024];
    00218   int next_number;
    00219
    00220   if (!controller->LoadedLibrary(library_name, &next_number, &fake_class[0], sizeof(fake_class))) {
    00221     /* Create a new fake internal name */
    00222     snprintf(fake_class, sizeof(fake_class), "_internalNA%d", next_number);
    00223
    00224     /* Load the library in a 'fake' namespace, so we can link it to the name the user requested */
    00225     sq_pushroottable(vm);
    00226     sq_pushstring(vm, OTTD2FS(fake_class), -1);
    00227     sq_newclass(vm, SQFalse);
    00228     /* Load the library */
    00229     if (!Squirrel::LoadScript(vm, (*iter).second->GetMainScript(), false)) {
    00230       char error[1024];
    00231       snprintf(error, sizeof(error), "there was a compile error when importing '%s' version %d", library, version);
    00232       sq_throwerror(vm, OTTD2FS(error));
    00233       return false;
    00234     }
    00235     /* Create the fake class */
    00236     sq_newslot(vm, -3, SQFalse);
    00237     sq_pop(vm, 1);
    00238
    00239     controller->AddLoadedLibrary(library_name, fake_class);
    00240   }
    00241
    00242   /* Find the real class inside the fake class (like 'sets.Vector') */
    00243   sq_pushroottable(vm);
    00244   sq_pushstring(vm, OTTD2FS(fake_class), -1);
    00245   if (SQ_FAILED(sq_get(vm, -2))) {
    00246     sq_throwerror(vm, _SC("internal error assigning library class"));
    00247     return false;
    00248   }
    00249   sq_pushstring(vm, OTTD2FS((*iter).second->GetInstanceName()), -1);
    00250   if (SQ_FAILED(sq_get(vm, -2))) {
    00251     char error[1024];
    00252     snprintf(error, sizeof(error), "unable to find class '%s' in the library '%s' version %d", (*iter).second->GetInstanceName(), library, version);
    00253     sq_throwerror(vm, OTTD2FS(error));
    00254     return false;
    00255   }
    00256   HSQOBJECT obj;
    00257   sq_getstackobj(vm, -1, &obj);
    00258   sq_pop(vm, 3);
    00259
    00260   if (StrEmpty(class_name)) {
    00261     sq_pushobject(vm, obj);
    00262     return true;
    00263   }
    00264
    00265   /* Now link the name the user wanted to our 'fake' class */
    00266   sq_pushobject(vm, parent);
    00267   sq_pushstring(vm, OTTD2FS(class_name), -1);
    00268   sq_pushobject(vm, obj);
    00269   sq_newclass(vm, SQTrue);
    00270   sq_newslot(vm, -3, SQFalse);
    00271   sq_pop(vm, 1);
    00272
    00273   sq_pushobject(vm, obj);
    00274   return true;
    00275 }
    00276
    00277 void AIScanner::RegisterLibrary(AILibrary *library)
    00278 {
    00279   char library_name[1024];
    00280   snprintf(library_name, sizeof(library_name), "%s.%s.%d", library->GetCategory(), library->GetInstanceName(), library->GetVersion());
    00281   strtolower(library_name);
    00282
    00283   if (this->library_list.find(library_name) != this->library_list.end()) {
    00284     /* This AI was already registered */
    00285 #ifdef WIN32
    00286     /* Windows doesn't care about the case */
    00287     if (strcasecmp(this->library_list[library_name]->GetMainScript(), library->GetMainScript()) == 0) {
    00288 #else
    00289     if (strcmp(this->library_list[library_name]->GetMainScript(), library->GetMainScript()) == 0) {
    00290 #endif
    00291       delete library;
    00292       return;
    00293     }
    00294
    00295     DEBUG(ai, 0, "Registering two libraries with the same name and version");
    00296     DEBUG(ai, 0, "  1: %s", this->library_list[library_name]->GetMainScript());
    00297     DEBUG(ai, 0, "  2: %s", library->GetMainScript());
    00298     DEBUG(ai, 0, "The first is taking precedence.");
    00299
    00300     delete library;
    00301     return;
    00302   }
    00303
    00304   this->library_list[strdup(library_name)] = library;
    00305 }
    00306
    00307 void AIScanner::RegisterAI(AIInfo *info)
    00308 {
    00309   char ai_name[1024];
    00310   snprintf(ai_name, sizeof(ai_name), "%s.%d", info->GetInstanceName(), info->GetVersion());
    00311   strtolower(ai_name);
    00312
    00313   /* Check if GetShortName follows the rules */
    00314   if (strlen(info->GetShortName()) != 4) {
    00315     DEBUG(ai, 0, "The AI '%s' returned a string from GetShortName() which is not four characaters. Unable to load the AI.", info->GetInstanceName());
    00316     delete info;
    00317     return;
    00318   }
    00319
    00320   if (this->info_list.find(ai_name) != this->info_list.end()) {
    00321     /* This AI was already registered */
    00322 #ifdef WIN32
    00323     /* Windows doesn't care about the case */
    00324     if (strcasecmp(this->info_list[ai_name]->GetMainScript(), info->GetMainScript()) == 0) {
    00325 #else
    00326     if (strcmp(this->info_list[ai_name]->GetMainScript(), info->GetMainScript()) == 0) {
    00327 #endif
    00328       delete info;
    00329       return;
    00330     }
    00331
    00332     DEBUG(ai, 0, "Registering two AIs with the same name and version");
    00333     DEBUG(ai, 0, "  1: %s", this->info_list[ai_name]->GetMainScript());
    00334     DEBUG(ai, 0, "  2: %s", info->GetMainScript());
    00335     DEBUG(ai, 0, "The first is taking precedence.");
    00336
    00337     delete info;
    00338     return;
    00339   }
    00340
    00341   this->info_list[strdup(ai_name)] = info;
    00342
    00343   /* Add the AI to the 'unique' AI list, where only the highest version of the
    00344    *  AI is registered. */
    00345   snprintf(ai_name, sizeof(ai_name), "%s", info->GetInstanceName());
    00346   strtolower(ai_name);
    00347   if (this->info_single_list.find(ai_name) == this->info_single_list.end()) {
    00348     this->info_single_list[strdup(ai_name)] = info;
    00349   } else if (this->info_single_list[ai_name]->GetVersion() < info->GetVersion()) {
    00350     this->info_single_list[ai_name] = info;
    00351   }
    00352 }
    00353
    00354 AIInfo *AIScanner::SelectRandomAI()
    00355 {
    00356   if (this->info_single_list.size() == 0) {
    00357     DEBUG(ai, 0, "No suitable AI found, loading 'dummy' AI.");
    00358     return this->info_dummy;
    00359   }
    00360
    00361   /* Find a random AI */
    00362   uint pos;
    00363   if (_networking) pos = InteractiveRandomRange((uint16)this->info_single_list.size());
    00364   else             pos =            RandomRange((uint16)this->info_single_list.size());
    00365
    00366   /* Find the Nth item from the array */
    00367   AIInfoList::iterator it = this->info_single_list.begin();
    00368   for (; pos > 0; pos--) it++;
    00369   AIInfoList::iterator first_it = it;
    00370   return (*it).second;
    00371 }
    00372
    00373 AIInfo *AIScanner::FindInfo(const char *nameParam, int versionParam)
    00374 {
    00375   if (this->info_list.size() == 0) return NULL;
    00376   if (nameParam == NULL) return NULL;
    00377
    00378   char ai_name[1024];
    00379   ttd_strlcpy(ai_name, nameParam, sizeof(ai_name));
    00380   strtolower(ai_name);
    00381
    00382   AIInfo *info = NULL;
    00383   int version = -1;
    00384
    00385   if (versionParam == -1) {
    00386     /* We want to load the latest version of this AI; so find it */
    00387     if (this->info_single_list.find(ai_name) != this->info_single_list.end()) return this->info_single_list[ai_name];
    00388
    00389     /* If we didn't find a match AI, maybe the user included a version */
    00390     char *e = strrchr(ai_name, '.');
    00391     if (e == NULL) return NULL;
    00392     *e = '\0';
    00393     e++;
    00394     versionParam = atoi(e);
    00395     /* Fall-through, like we were calling this function with a version */
    00396   }
    00397
    00398   /* Try to find a direct 'name.version' match */
    00399   char ai_name_tmp[1024];
    00400   snprintf(ai_name_tmp, sizeof(ai_name_tmp), "%s.%d", ai_name, versionParam);
    00401   strtolower(ai_name_tmp);
    00402   if (this->info_list.find(ai_name_tmp) != this->info_list.end()) return this->info_list[ai_name_tmp];
    00403
    00404   /* See if there is a compatible AI which goes by that name, with the highest
    00405    *  version which allows loading the requested version */
    00406   AIInfoList::iterator it = this->info_list.begin();
    00407   for (; it != this->info_list.end(); it++) {
    00408     char ai_name_compare[1024];
    00409     snprintf(ai_name_compare, sizeof(ai_name_compare), "%s", (*it).second->GetInstanceName());
    00410     strtolower(ai_name_compare);
    00411
    00412     if (strcasecmp(ai_name, ai_name_compare) == 0 && (*it).second->CanLoadFromVersion(versionParam)) {
    00413       version = (*it).second->GetVersion();
    00414       info = (*it).second;
    00415     }
    00416   }
    00417
    00418   return info;
    00419 }
    00420
    00421 char *AIScanner::GetAIConsoleList(char *p, const char *last)
    00422 {
    00423   p += seprintf(p, last, "List of AIs:\n");
    00424   AIInfoList::iterator it = this->info_list.begin();
    00425   for (; it != this->info_list.end(); it++) {
    00426     AIInfo *i = (*it).second;
    00427     p += seprintf(p, last, "%10s (v%d): %s\n", i->GetInstanceName(), i->GetVersion(), i->GetDescription());
    00428   }
    00429   p += seprintf(p, last, "\n");
    00430
    00431   return p;
    00432 }
    00433
    00434 #if defined(ENABLE_NETWORK)
    00435 #include "../network/network_content.h"
    00436 #include "../md5.h"
    00437 #include "../tar_type.h"
    00438
    00440 struct AIFileChecksumCreator : FileScanner {
    00441   byte md5sum[16];
    00442
    00447   AIFileChecksumCreator()
    00448   {
    00449     memset(this->md5sum, 0, sizeof(this->md5sum));
    00450   }
    00451
    00452   /* Add the file and calculate the md5 sum. */
    00453   virtual bool AddFile(const char *filename, size_t basepath_length)
    00454   {
    00455     Md5 checksum;
    00456     uint8 buffer[1024];
    00457     size_t len, size;
    00458     byte tmp_md5sum[16];
    00459
    00460     /* Open the file ... */
    00461     FILE *f = FioFOpenFile(filename, "rb", DATA_DIR, &size);
    00462     if (f == NULL) return false;
    00463
    00464     /* ... calculate md5sum... */
    00465     while ((len = fread(buffer, 1, (size > sizeof(buffer)) ? sizeof(buffer) : size, f)) != 0 && size != 0) {
    00466       size -= len;
    00467       checksum.Append(buffer, len);
    00468     }
    00469     checksum.Finish(tmp_md5sum);
    00470
    00471     FioFCloseFile(f);
    00472
    00473     /* ... and xor it to the overall md5sum. */
    00474     for (uint i = 0; i < sizeof(md5sum); i++) this->md5sum[i] ^= tmp_md5sum[i];
    00475
    00476     return true;
    00477   }
    00478 };
    00479
    00488 static bool IsSameAI(const ContentInfo *ci, bool md5sum, AIFileInfo *info)
    00489 {
    00490   uint32 id = 0;
    00491   const char *str = info->GetShortName();
    00492   for (int j = 0; j < 4 && *str != '\0'; j++, str++) id |= *str << (8 * j);
    00493
    00494   if (id != ci->unique_id) return false;
    00495   if (!md5sum) return true;
    00496
    00497   AIFileChecksumCreator checksum;
    00498   char path[MAX_PATH];
    00499   strecpy(path, info->GetMainScript(), lastof(path));
    00500   /* There'll always be at least 2 path separator characters in an AI's
    00501    * main script name as the search algorithm requires the main script to
    00502    * be in a subdirectory of the AI directory; so ai/<path>/main.nut. */
    00503   *strrchr(path, PATHSEPCHAR) = '\0';
    00504   *strrchr(path, PATHSEPCHAR) = '\0';
    00505   TarList::iterator iter = _tar_list.find(path);
    00506
    00507   if (iter != _tar_list.end()) {
    00508     /* The main script is in a tar file, so find all files that
    00509      * are in the same tar and add them to the MD5 checksumming. */
    00510     TarFileList::iterator tar;
    00511     FOR_ALL_TARS(tar) {
    00512       /* Not in the same tar. */
    00513       if (tar->second.tar_filename != iter->first) continue;
    00514
    00515       /* Check the extension. */
    00516       const char *ext = strrchr(tar->first.c_str(), '.');
    00517       if (ext == NULL || strcasecmp(ext, ".nut") != 0) continue;
    00518
    00519       /* Create the full path name, */
    00520       seprintf(path, lastof(path), "%s%c%s", tar->second.tar_filename, PATHSEPCHAR, tar->first.c_str());
    00521       checksum.AddFile(path, 0);
    00522     }
    00523   } else {
    00524     /* Add the path sep char back when searching a directory, so we are
    00525      * in the actual directory. */
    00526     path[strlen(path)] = PATHSEPCHAR;
    00527     checksum.Scan(".nut", path);
    00528   }
    00529
    00530   return memcmp(ci->md5sum, checksum.md5sum, sizeof(ci->md5sum)) == 0;
    00531 }
    00532
    00539 bool AIScanner::HasAI(const ContentInfo *ci, bool md5sum)
    00540 {
    00541   switch (ci->type) {
    00542     case CONTENT_TYPE_AI:
    00543       for (AIInfoList::iterator it = this->info_list.begin(); it != this->info_list.end(); it++) {
    00544         if (IsSameAI(ci, md5sum, (*it).second)) return true;
    00545       }
    00546       return false;
    00547
    00548     case CONTENT_TYPE_AI_LIBRARY:
    00549       for (AILibraryList::iterator it = this->library_list.begin(); it != this->library_list.end(); it++) {
    00550         if (IsSameAI(ci, md5sum, (*it).second)) return true;
    00551       }
    00552       return false;
    00553
    00554     default:
    00555       NOT_REACHED();
    00556   }
    00557 }
    00558
    00565 /*static */ bool AI::HasAI(const ContentInfo *ci, bool md5sum)
    00566 {
    00567   return AI::ai_scanner->HasAI(ci, md5sum);
    00568 }
    00569
    00570 #endif /* ENABLE_NETWORK */