tron@2186: /* $Id$ */ tron@2186: rubidium@9111: /** @file gfxinit.cpp Initializing of the (GRF) graphics. */ belugas@6179: truelight@0: #include "stdafx.h" Darkvater@1891: #include "openttd.h" tron@1299: #include "debug.h" tron@2340: #include "gfxinit.h" tron@1349: #include "spritecache.h" rubidium@10039: #include "fileio_func.h" rubidium@7805: #include "fios.h" pasky@463: #include "newgrf.h" dominik@961: #include "md5.h" tron@2159: #include "variables.h" peter1138@5156: #include "fontcache.h" rubidium@8123: #include "gfx_func.h" rubidium@8213: #include "core/alloc_func.hpp" rubidium@8236: #include "core/bitmath_func.hpp" rubidium@10037: #include "core/smallvec_type.hpp" tron@4472: #include rubidium@8270: #include "settings_type.h" rubidium@9994: #include "string_func.h" rubidium@10037: #include "ini_type.h" truelight@0: rubidium@8264: #include "table/sprites.h" rubidium@10062: #include "table/palette_convert.h" rubidium@8264: rubidium@10037: /** The currently used palette */ rubidium@9995: PaletteType _use_palette = PAL_AUTODETECT; rubidium@10062: /** Whether the given NewGRFs must get a palette remap or not. */ rubidium@10062: bool _palette_remap_grf[MAX_FILE_SLOTS]; rubidium@10062: /** Palette map to go from the !_use_palette to the _use_palette */ rubidium@10062: const byte *_palette_remap = NULL; rubidium@10062: /** Palette map to go from the _use_palette to the !_use_palette */ rubidium@10062: const byte *_palette_reverse_remap = NULL; rubidium@10062: rubidium@10037: char _ini_graphics_set[32]; rubidium@9989: rubidium@10037: /** Structure holding filename and MD5 information about a single file */ rubidium@6248: struct MD5File { rubidium@10037: const char *filename; ///< filename rubidium@10037: uint8 hash[16]; ///< md5 sum of the file rubidium@10037: const char *missing_warning; ///< warning when this file is missing rubidium@6248: }; dominik@961: rubidium@10037: /** Types of graphics in the base graphics set */ rubidium@10037: enum GraphicsFileType { rubidium@10037: GFT_BASE, ///< Base sprites for all climates rubidium@10037: GFT_LOGOS, ///< Logos, landscape icons and original terrain generator sprites rubidium@10037: GFT_ARCTIC, ///< Landscape replacement sprites for arctic rubidium@10037: GFT_TROPICAL, ///< Landscape replacement sprites for tropical rubidium@10037: GFT_TOYLAND, ///< Landscape replacement sprites for toyland rubidium@10037: GFT_EXTRA, ///< Extra sprites that were not part of the original sprites rubidium@10037: MAX_GFT ///< We are looking for this amount of GRFs rubidium@10037: }; rubidium@10037: rubidium@10037: /** Information about a single graphics set. */ rubidium@9994: struct GraphicsSet { rubidium@9994: const char *name; ///< The name of the graphics set rubidium@9994: const char *description; ///< Description of the graphics set rubidium@10037: uint32 shortname; ///< Four letter short variant of the name rubidium@10037: uint32 version; ///< The version of this graphics set rubidium@9995: PaletteType palette; ///< Palette of this graphics set rubidium@10037: rubidium@10037: MD5File files[MAX_GFT]; ///< All GRF files part of this set rubidium@9994: uint found_grfs; ///< Number of the GRFs that could be found rubidium@10037: rubidium@10037: GraphicsSet *next; ///< The next graphics set in this list rubidium@10037: rubidium@10037: /** Free everything we allocated */ rubidium@10037: ~GraphicsSet() rubidium@10037: { rubidium@10037: free((void*)this->name); rubidium@10037: free((void*)this->description); rubidium@10037: for (uint i = 0; i < MAX_GFT; i++) { rubidium@10037: free((void*)this->files[i].filename); rubidium@10037: free((void*)this->files[i].missing_warning); rubidium@10037: } rubidium@10037: rubidium@10037: delete this->next; rubidium@10037: } rubidium@6248: }; truelight@0: rubidium@10037: /** All graphics sets currently available */ rubidium@10037: static GraphicsSet *_available_graphics_sets = NULL; rubidium@10037: /** The one and only graphics set that is currently being used. */ rubidium@10037: static const GraphicsSet *_used_graphics_set = NULL; rubidium@9994: dominik@961: #include "table/files.h" truelight@0: #include "table/landscape_sprite.h" truelight@0: celestar@2187: static const SpriteID * const _landscape_spriteindexes[] = { truelight@0: _landscape_spriteindexes_1, truelight@0: _landscape_spriteindexes_2, truelight@0: _landscape_spriteindexes_3, truelight@0: }; truelight@0: rubidium@7841: static uint LoadGrfFile(const char *filename, uint load_index, int file_index) truelight@0: { tron@2342: uint load_index_org = load_index; truelight@6908: uint sprite_id = 0; truelight@0: truelight@0: FioOpenFile(file_index, filename); darkvater@365: Darkvater@5380: DEBUG(sprite, 2, "Reading grf-file '%s'", filename); truelight@0: truelight@6908: while (LoadNextSprite(load_index, file_index, sprite_id)) { truelight@0: load_index++; truelight@6908: sprite_id++; celestar@2187: if (load_index >= MAX_SPRITES) { glx@9470: usererror("Too many sprites. Recompile with higher MAX_SPRITES value or remove some custom GRF files."); truelight@0: } truelight@0: } Darkvater@5380: DEBUG(sprite, 2, "Currently %i sprites are loaded", load_index); truelight@0: darkvater@365: return load_index - load_index_org; darkvater@365: } darkvater@365: tron@2342: rubidium@7772: void LoadSpritesIndexed(int file_index, uint *sprite_id, const SpriteID *index_tbl) rubidium@7772: { rubidium@7772: uint start; rubidium@7772: while ((start = *index_tbl++) != END) { rubidium@7772: uint end = *index_tbl++; rubidium@7772: peter1138@8432: do { peter1138@8432: bool b = LoadNextSprite(start, file_index, *sprite_id); peter1138@8432: assert(b); peter1138@8432: (*sprite_id)++; peter1138@8432: } while (++start <= end); rubidium@7772: } rubidium@7772: } rubidium@7772: tron@2342: static void LoadGrfIndexed(const char* filename, const SpriteID* index_tbl, int file_index) darkvater@365: { truelight@6908: uint sprite_id = 0; dominik@614: darkvater@365: FioOpenFile(file_index, filename); darkvater@365: Darkvater@5380: DEBUG(sprite, 2, "Reading indexed grf-file '%s'", filename); truelight@0: rubidium@7772: LoadSpritesIndexed(file_index, &sprite_id, index_tbl); truelight@184: } truelight@0: truelight@0: rubidium@7841: /** rubidium@7841: * Calculate and check the MD5 hash of the supplied filename. rubidium@7841: * @param file filename and expected MD5 hash for the given filename. rubidium@7841: * @return true if the checksum is correct. rubidium@7841: */ rubidium@7841: static bool FileMD5(const MD5File file) dominik@614: { truelight@7574: size_t size; truelight@7574: FILE *f = FioFOpenFile(file.filename, "rb", DATA_DIR, &size); bjarni@5482: truelight@862: if (f != NULL) { skidd13@8133: Md5 checksum; skidd13@8133: uint8 buffer[1024]; skidd13@8133: uint8 digest[16]; tron@2028: size_t len; tron@2028: truelight@7574: while ((len = fread(buffer, 1, (size > sizeof(buffer)) ? sizeof(buffer) : size, f)) != 0 && size != 0) { truelight@7574: size -= len; skidd13@8133: checksum.Append(buffer, len); truelight@7574: } dominik@961: truelight@7592: FioFCloseFile(f); tron@1019: skidd13@8133: checksum.Finish(digest); rubidium@7841: return memcmp(file.hash, digest, sizeof(file.hash)) == 0; dominik@961: } else { // file not found dominik@961: return false; tron@1019: } dominik@961: } dominik@961: rubidium@7841: /** rubidium@9994: * Determine the graphics pack that has to be used. rubidium@9994: * The one with the most correct files wins. rubidium@9994: */ peter1138@10048: static bool DetermineGraphicsPack() rubidium@9994: { peter1138@10048: if (_used_graphics_set != NULL) return true; rubidium@9994: rubidium@10037: const GraphicsSet *best = _available_graphics_sets; rubidium@10037: for (const GraphicsSet *c = _available_graphics_sets; c != NULL; c = c->next) { rubidium@10037: if (best->found_grfs < c->found_grfs || rubidium@10062: (best->found_grfs == c->found_grfs && ( rubidium@10062: (best->shortname == c->shortname && best->version < c->version) || rubidium@10062: (best->palette != _use_palette && c->palette == _use_palette)))) { rubidium@10037: best = c; rubidium@10037: } rubidium@9994: } rubidium@9994: rubidium@10037: _used_graphics_set = best; peter1138@10048: return _used_graphics_set != NULL; rubidium@9994: } rubidium@9994: rubidium@10077: extern void UpdateNewGRFConfigPalette(); rubidium@10077: rubidium@9994: /** rubidium@7841: * Determine the palette that has to be used. rubidium@9994: * - forced palette via command line -> leave it that way rubidium@9994: * - otherwise -> palette based on the graphics pack rubidium@7841: */ rubidium@7841: static void DeterminePalette() dominik@961: { peter1138@10048: assert(_used_graphics_set != NULL); rubidium@10062: if (_use_palette >= MAX_PAL) _use_palette = _used_graphics_set->palette; rubidium@7841: rubidium@10062: switch (_use_palette) { rubidium@10062: case PAL_DOS: rubidium@10062: _palette_remap = _palmap_w2d; rubidium@10062: _palette_reverse_remap = _palmap_d2w; rubidium@10062: break; rubidium@10062: rubidium@10062: case PAL_WINDOWS: rubidium@10062: _palette_remap = _palmap_d2w; rubidium@10062: _palette_reverse_remap = _palmap_w2d; rubidium@10062: break; rubidium@10062: rubidium@10062: default: rubidium@10062: NOT_REACHED(); rubidium@10062: } rubidium@10077: rubidium@10077: UpdateNewGRFConfigPalette(); dominik@614: } dominik@614: rubidium@7841: /** rubidium@7841: * Checks whether the MD5 checksums of the files are correct. rubidium@7841: * rubidium@7841: * @note Also checks sample.cat and other required non-NewGRF GRFs for corruption. rubidium@7841: */ rubidium@7841: void CheckExternalFiles() rubidium@7841: { rubidium@7841: DeterminePalette(); rubidium@7841: rubidium@10062: DEBUG(grf, 1, "Using the %s base graphics set with the %s palette", _used_graphics_set->name, _use_palette == PAL_DOS ? "DOS" : "Windows"); rubidium@10062: rubidium@7841: static const size_t ERROR_MESSAGE_LENGTH = 128; rubidium@10037: char error_msg[ERROR_MESSAGE_LENGTH * (MAX_GFT + 1)]; rubidium@7841: error_msg[0] = '\0'; rubidium@7841: char *add_pos = error_msg; rubidium@10299: const char *last = lastof(error_msg); rubidium@7841: rubidium@10037: for (uint i = 0; i < lengthof(_used_graphics_set->files); i++) { rubidium@10037: if (!FileMD5(_used_graphics_set->files[i])) { rubidium@10299: add_pos += seprintf(add_pos, last, "Your '%s' file is corrupted or missing! %s\n", _used_graphics_set->files[i].filename, _used_graphics_set->files[i].missing_warning); rubidium@7841: } rubidium@7841: } rubidium@7841: rubidium@9994: bool sound = false; rubidium@9994: for (uint i = 0; !sound && i < lengthof(_sound_sets); i++) { rubidium@9994: sound = FileMD5(_sound_sets[i]); rubidium@9994: } rubidium@9994: rubidium@9994: if (!sound) { rubidium@10299: add_pos += seprintf(add_pos, last, "Your 'sample.cat' file is corrupted or missing! You can find 'sample.cat' on your Transport Tycoon Deluxe CD-ROM.\n"); rubidium@7841: } rubidium@7841: rubidium@7841: if (add_pos != error_msg) ShowInfoF(error_msg); rubidium@7841: } rubidium@7841: tron@2342: rubidium@6247: static void LoadSpriteTables() truelight@0: { rubidium@10062: memset(_palette_remap_grf, 0, sizeof(_palette_remap_grf)); rubidium@7805: uint i = FIRST_GRF_SLOT; dominik@614: rubidium@10062: _palette_remap_grf[i] = (_use_palette != _used_graphics_set->palette); rubidium@10037: LoadGrfFile(_used_graphics_set->files[GFT_BASE].filename, 0, i++); tron@2353: rubidium@7841: /* rubidium@7841: * The second basic file always starts at the given location and does rubidium@7841: * contain a different amount of sprites depending on the "type"; DOS rubidium@7841: * has a few sprites less. However, we do not care about those missing rubidium@7841: * sprites as they are not shown anyway (logos in intro game). rubidium@7841: */ rubidium@10062: _palette_remap_grf[i] = (_use_palette != _used_graphics_set->palette); rubidium@10037: LoadGrfFile(_used_graphics_set->files[GFT_LOGOS].filename, 4793, i++); truelight@0: rubidium@7841: /* rubidium@7841: * Load additional sprites for climates other than temperate. rubidium@7841: * This overwrites some of the temperate sprites, such as foundations rubidium@7841: * and the ground sprites. rubidium@7841: */ rubidium@9413: if (_settings_game.game_creation.landscape != LT_TEMPERATE) { rubidium@10062: _palette_remap_grf[i] = (_use_palette != _used_graphics_set->palette); tron@2309: LoadGrfIndexed( rubidium@10037: _used_graphics_set->files[GFT_ARCTIC + _settings_game.game_creation.landscape - 1].filename, rubidium@9413: _landscape_spriteindexes[_settings_game.game_creation.landscape - 1], tron@2309: i++ tron@2309: ); tron@2309: } truelight@0: peter1138@5156: /* Initialize the unicode to sprite mapping table */ peter1138@5156: InitializeUnicodeGlyphMap(); peter1138@5156: rubidium@7882: /* rubidium@7882: * Load the base NewGRF with OTTD required graphics as first NewGRF. rubidium@7882: * However, we do not want it to show up in the list of used NewGRFs, rubidium@7882: * so we have to manually add it, and then remove it later. rubidium@7882: */ rubidium@7882: GRFConfig *top = _grfconfig; rubidium@7882: GRFConfig *master = CallocT(1); rubidium@10037: master->filename = strdup(_used_graphics_set->files[GFT_EXTRA].filename); rubidium@7882: FillGRFDetails(master, false); rubidium@10066: master->windows_paletted = (_used_graphics_set->palette == PAL_WINDOWS); skidd13@7929: ClrBit(master->flags, GCF_INIT_ONLY); rubidium@7882: master->next = top; rubidium@7882: _grfconfig = master; rubidium@7882: rubidium@7882: LoadNewGRF(SPR_NEWGRFS_BASE, i); rubidium@7882: rubidium@7882: /* Free and remove the top element. */ rubidium@7882: ClearGRFConfig(&master); rubidium@7882: _grfconfig = top; truelight@0: } truelight@0: truelight@0: rubidium@6247: void GfxLoadSprites() tron@1093: { rubidium@9413: DEBUG(sprite, 2, "Loading sprite set %d", _settings_game.game_creation.landscape); truelight@0: peter1138@5151: GfxInitSpriteMem(); peter1138@5151: LoadSpriteTables(); peter1138@5151: GfxInitPalettes(); truelight@0: } rubidium@9994: rubidium@9994: /** rubidium@10037: * Try to read a single piece of metadata and return false if it doesn't exist. rubidium@10037: * @param name the name of the item to fetch. rubidium@9994: */ rubidium@10037: #define fetch_metadata(name) \ rubidium@10037: item = metadata->GetItem(name, false); \ rubidium@10037: if (item == NULL || strlen(item->value) == 0) { \ rubidium@10037: DEBUG(grf, 0, "Base graphics set detail loading: %s field missing", name); \ rubidium@10037: return false; \ rubidium@10037: } rubidium@10037: rubidium@10037: /** Names corresponding to the GraphicsFileType */ rubidium@10037: static const char *_gft_names[MAX_GFT] = { "base", "logos", "arctic", "tropical", "toyland", "extra" }; rubidium@10037: rubidium@10037: /** rubidium@10037: * Read the graphics set information from a loaded ini. rubidium@10037: * @param graphics the graphics set to write to rubidium@10037: * @param ini the ini to read from rubidium@10037: * @param path the path to this ini file (for filenames) rubidium@10037: * @return true if loading was successful. rubidium@10037: */ rubidium@10037: static bool FillGraphicsSetDetails(GraphicsSet *graphics, IniFile *ini, const char *path) rubidium@10037: { rubidium@10037: memset(graphics, 0, sizeof(*graphics)); rubidium@10037: rubidium@10037: IniGroup *metadata = ini->GetGroup("metadata"); rubidium@10037: IniItem *item; rubidium@10037: rubidium@10037: fetch_metadata("name"); rubidium@10037: graphics->name = strdup(item->value); rubidium@10037: rubidium@10037: fetch_metadata("description"); rubidium@10037: graphics->description = strdup(item->value); rubidium@10037: rubidium@10037: fetch_metadata("shortname"); rubidium@10037: for (uint i = 0; item->value[i] != '\0' && i < 4; i++) { rubidium@10037: graphics->shortname |= ((uint8)item->value[i]) << (32 - i * 8); rubidium@10037: } rubidium@10037: rubidium@10037: fetch_metadata("version"); rubidium@10037: graphics->version = atoi(item->value); rubidium@10037: rubidium@10037: fetch_metadata("palette"); rubidium@10037: graphics->palette = (*item->value == 'D' || *item->value == 'd') ? PAL_DOS : PAL_WINDOWS; rubidium@10037: rubidium@10037: /* For each of the graphics file types we want to find the file, MD5 checksums and warning messages. */ rubidium@10037: IniGroup *files = ini->GetGroup("files"); rubidium@10037: IniGroup *md5s = ini->GetGroup("md5s"); rubidium@10037: IniGroup *origin = ini->GetGroup("origin"); rubidium@10037: for (uint i = 0; i < MAX_GFT; i++) { rubidium@10037: MD5File *file = &graphics->files[i]; rubidium@10037: /* Find the filename first. */ rubidium@10037: item = files->GetItem(_gft_names[i], false); rubidium@10037: if (item == NULL) { rubidium@10037: DEBUG(grf, 0, "No graphics file for: %s", _gft_names[i]); rubidium@10037: return false; rubidium@10037: } rubidium@10037: rubidium@10037: const char *filename = item->value; rubidium@10037: file->filename = MallocT(strlen(filename) + strlen(path) + 1); rubidium@10037: sprintf((char*)file->filename, "%s%s", path, filename); rubidium@10037: rubidium@10037: /* Then find the MD5 checksum */ rubidium@10037: item = md5s->GetItem(filename, false); rubidium@10037: if (item == NULL) { rubidium@10037: DEBUG(grf, 0, "No MD5 checksum specified for: %s", filename); rubidium@10037: return false; rubidium@10037: } rubidium@10037: char *c = item->value; rubidium@10037: for (uint i = 0; i < sizeof(file->hash) * 2; i++, c++) { rubidium@10037: uint j; rubidium@10037: if ('0' <= *c && *c <= '9') { rubidium@10037: j = *c - '0'; rubidium@10037: } else if ('a' <= *c && *c <= 'f') { rubidium@10037: j = *c - 'a' + 10; rubidium@10037: } else if ('A' <= *c && *c <= 'F') { rubidium@10037: j = *c - 'A' + 10; rubidium@10037: } else { rubidium@10037: DEBUG(grf, 0, "Malformed MD5 checksum specified for: %s", filename); rubidium@10037: return false; rubidium@10037: } rubidium@10037: if (i % 2 == 0) { rubidium@10037: file->hash[i / 2] = j << 4; rubidium@10037: } else { rubidium@10037: file->hash[i / 2] |= j; rubidium@10037: } rubidium@10037: } rubidium@10037: rubidium@10037: /* Then find the warning message when the file's missing */ rubidium@10037: item = origin->GetItem(filename, false); rubidium@10037: if (item == NULL) item = origin->GetItem("default", false); rubidium@10037: if (item == NULL) { rubidium@10037: DEBUG(grf, 1, "No origin warning message specified for: %s", filename); rubidium@10037: file->missing_warning = strdup(""); rubidium@10037: } else { rubidium@10037: file->missing_warning = strdup(item->value); rubidium@10037: } rubidium@10037: rubidium@10037: if (FileMD5(*file)) graphics->found_grfs++; rubidium@10037: } rubidium@10037: rubidium@10037: return true; rubidium@10037: } rubidium@10037: rubidium@10037: /** Helper for scanning for files with GRF as extension */ rubidium@10037: class OBGFileScanner : FileScanner { rubidium@10037: public: rubidium@10037: /* virtual */ bool AddFile(const char *filename, size_t basepath_length); rubidium@10037: rubidium@10037: /** Do the scan for OBGs. */ rubidium@10037: static uint DoScan() rubidium@10037: { rubidium@10037: OBGFileScanner fs; rubidium@10037: return fs.Scan(".obg", DATA_DIR); rubidium@10037: } rubidium@10037: }; rubidium@10037: rubidium@10037: /** rubidium@10037: * Try to add a graphics set with the given filename. rubidium@10037: * @param filename the full path to the file to read rubidium@10037: * @param basepath_length amount of characters to chop of before to get a relative DATA_DIR filename rubidium@10037: * @return true if the file is added. rubidium@10037: */ rubidium@10037: bool OBGFileScanner::AddFile(const char *filename, size_t basepath_length) rubidium@10037: { rubidium@10037: bool ret = false; rubidium@10037: DEBUG(grf, 1, "Found %s as base graphics set", filename); rubidium@10037: rubidium@10037: GraphicsSet *graphics = new GraphicsSet();; rubidium@10037: IniFile *ini = new IniFile(); rubidium@10037: ini->LoadFromDisk(filename); rubidium@10037: rubidium@10037: char *path = strdup(filename + basepath_length); rubidium@10037: char *psep = strrchr(path, PATHSEPCHAR); rubidium@10037: if (psep != NULL) { rubidium@10037: psep[1] = '\0'; rubidium@10037: } else { rubidium@10037: *path = '\0'; rubidium@10037: } rubidium@10037: rubidium@10037: if (FillGraphicsSetDetails(graphics, ini, path)) { rubidium@10037: bool duplicate = false; rubidium@10037: for (const GraphicsSet *c = _available_graphics_sets; !duplicate && c != NULL; c = c->next) { rubidium@10037: duplicate = (strcmp(c->name, graphics->name) == 0) || (c->shortname == graphics->shortname && c->version == graphics->version); rubidium@10037: } rubidium@10037: if (duplicate) { rubidium@10037: delete graphics; rubidium@10037: } else { rubidium@10037: graphics->next = _available_graphics_sets; rubidium@10037: _available_graphics_sets = graphics; rubidium@10037: ret = true; rubidium@10037: } rubidium@10037: } else { rubidium@10037: delete graphics; rubidium@10037: } rubidium@10037: free(path); rubidium@10037: rubidium@10037: delete ini; rubidium@10037: return ret; rubidium@10037: } rubidium@10037: rubidium@10037: rubidium@10037: rubidium@10037: /** Scan for all Grahpics sets */ rubidium@9994: void FindGraphicsSets() rubidium@9994: { rubidium@10037: DEBUG(grf, 1, "Scanning for Graphics sets"); rubidium@10037: OBGFileScanner::DoScan(); rubidium@9994: } rubidium@9994: rubidium@9994: /** rubidium@9994: * Set the graphics set to be used. rubidium@9994: * @param name of the graphics set to use rubidium@9994: * @return true if it could be loaded rubidium@9994: */ rubidium@9994: bool SetGraphicsSet(const char *name) rubidium@9994: { rubidium@9994: if (StrEmpty(name)) { peter1138@10048: if (!DetermineGraphicsPack()) return false; rubidium@9994: CheckExternalFiles(); rubidium@9994: return true; rubidium@9994: } rubidium@9994: rubidium@10037: for (const GraphicsSet *g = _available_graphics_sets; g != NULL; g = g->next) { rubidium@10037: if (strcmp(name, g->name) == 0) { rubidium@10037: _used_graphics_set = g; rubidium@9994: CheckExternalFiles(); rubidium@9994: return true; rubidium@9994: } rubidium@9994: } rubidium@9994: return false; rubidium@9994: } rubidium@9994: rubidium@9994: /** rubidium@9994: * Returns a list with the graphics sets. rubidium@9994: * @param p where to print to rubidium@9994: * @param last the last character to print to rubidium@9994: * @return the last printed character rubidium@9994: */ rubidium@9994: char *GetGraphicsSetsList(char *p, const char *last) rubidium@9994: { rubidium@10299: p += seprintf(p, last, "List of graphics sets:\n"); rubidium@10037: for (const GraphicsSet *g = _available_graphics_sets; g != NULL; g = g->next) { rubidium@10037: if (g->found_grfs <= 1) continue; rubidium@9994: rubidium@10299: p += seprintf(p, last, "%18s: %s", g->name, g->description); rubidium@10037: int difference = MAX_GFT - g->found_grfs; rubidium@9994: if (difference != 0) { rubidium@10299: p += seprintf(p, last, " (missing %i file%s)\n", difference, difference == 1 ? "" : "s"); rubidium@9994: } else { rubidium@10299: p += seprintf(p, last, "\n"); rubidium@9994: } rubidium@9994: } rubidium@10299: p += seprintf(p, last, "\n"); rubidium@9994: rubidium@9994: return p; rubidium@9994: }