4 * This file is part of OpenTTD.
5 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
6 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
10 /** @file gfxinit.cpp Initializing of the (GRF) graphics. */
15 #include "3rdparty/md5/md5.h"
16 #include "fontcache.h"
18 #include "transparency.h"
19 #include "blitter/factory.hpp"
20 #include "video/video_driver.hpp"
21 #include "window_func.h"
22 #include "zoom_func.h"
23 #include "clear_map.h"
24 #include "clear_func.h"
26 #include "table/tree_land.h"
27 #include "blitter/32bpp_base.hpp"
29 /* The type of set we're replacing */
30 #define SET_TYPE "graphics"
31 #include "base_media_func.h"
33 #include "table/sprites.h"
35 #include "safeguards.h"
37 /** Whether the given NewGRFs must get a palette remap from windows to DOS or not. */
38 bool _palette_remap_grf
[MAX_FILE_SLOTS
];
40 #include "table/landscape_sprite.h"
42 /** Offsets for loading the different "replacement" sprites in the files. */
43 static const SpriteID
* const _landscape_spriteindexes
[] = {
44 _landscape_spriteindexes_arctic
,
45 _landscape_spriteindexes_tropic
,
46 _landscape_spriteindexes_toyland
,
50 * Load an old fashioned GRF file.
51 * @param filename The name of the file to open.
52 * @param load_index The offset of the first sprite.
53 * @param file_index The Fio offset to load the file in.
54 * @return The number of loaded sprites.
56 static uint
LoadGrfFile(const char *filename
, uint load_index
, int file_index
)
58 uint load_index_org
= load_index
;
61 FioOpenFile(file_index
, filename
, BASESET_DIR
);
63 DEBUG(sprite
, 2, "Reading grf-file '%s'", filename
);
65 byte container_ver
= GetGRFContainerVersion();
66 if (container_ver
== 0) usererror("Base grf '%s' is corrupt", filename
);
67 ReadGRFSpriteOffsets(container_ver
);
68 if (container_ver
>= 2) {
69 /* Read compression. */
70 byte compression
= FioReadByte();
71 if (compression
!= 0) usererror("Unsupported compression format");
74 while (LoadNextSprite(load_index
, file_index
, sprite_id
, container_ver
)) {
77 if (load_index
>= MAX_SPRITES
) {
78 usererror("Too many sprites. Recompile with higher MAX_SPRITES value or remove some custom GRF files.");
81 DEBUG(sprite
, 2, "Currently %i sprites are loaded", load_index
);
83 return load_index
- load_index_org
;
87 * Load an old fashioned GRF file to replace already loaded sprites.
88 * @param filename The name of the file to open.
89 * @param index_tlb The offsets of each of the sprites.
90 * @param file_index The Fio offset to load the file in.
91 * @return The number of loaded sprites.
93 static void LoadGrfFileIndexed(const char *filename
, const SpriteID
*index_tbl
, int file_index
)
98 FioOpenFile(file_index
, filename
, BASESET_DIR
);
100 DEBUG(sprite
, 2, "Reading indexed grf-file '%s'", filename
);
102 byte container_ver
= GetGRFContainerVersion();
103 if (container_ver
== 0) usererror("Base grf '%s' is corrupt", filename
);
104 ReadGRFSpriteOffsets(container_ver
);
105 if (container_ver
>= 2) {
106 /* Read compression. */
107 byte compression
= FioReadByte();
108 if (compression
!= 0) usererror("Unsupported compression format");
111 while ((start
= *index_tbl
++) != END
) {
112 uint end
= *index_tbl
++;
115 bool b
= LoadNextSprite(start
, file_index
, sprite_id
, container_ver
);
118 } while (++start
<= end
);
123 * Checks whether the MD5 checksums of the files are correct.
125 * @note Also checks sample.cat and other required non-NewGRF GRFs for corruption.
127 void CheckExternalFiles()
129 if (BaseGraphics::GetUsedSet() == nullptr || BaseSounds::GetUsedSet() == nullptr) return;
131 const GraphicsSet
*used_set
= BaseGraphics::GetUsedSet();
133 DEBUG(grf
, 1, "Using the %s base graphics set", used_set
->name
);
135 static const size_t ERROR_MESSAGE_LENGTH
= 256;
136 static const size_t MISSING_FILE_MESSAGE_LENGTH
= 128;
138 /* Allocate for a message for each missing file and for one error
141 char error_msg
[MISSING_FILE_MESSAGE_LENGTH
* (GraphicsSet::NUM_FILES
+ SoundsSet::NUM_FILES
) + 2 * ERROR_MESSAGE_LENGTH
];
143 char *add_pos
= error_msg
;
144 const char *last
= lastof(error_msg
);
146 if (used_set
->GetNumInvalid() != 0) {
147 /* Not all files were loaded successfully, see which ones */
148 add_pos
+= seprintf(add_pos
, last
, "Trying to load graphics set '%s', but it is incomplete. The game will probably not run correctly until you properly install this set or select another one. See section 4.1 of readme.txt.\n\nThe following files are corrupted or missing:\n", used_set
->name
);
149 for (uint i
= 0; i
< GraphicsSet::NUM_FILES
; i
++) {
150 MD5File::ChecksumResult res
= GraphicsSet::CheckMD5(&used_set
->files
[i
], BASESET_DIR
);
151 if (res
!= MD5File::CR_MATCH
) add_pos
+= seprintf(add_pos
, last
, "\t%s is %s (%s)\n", used_set
->files
[i
].filename
, res
== MD5File::CR_MISMATCH
? "corrupt" : "missing", used_set
->files
[i
].missing_warning
);
153 add_pos
+= seprintf(add_pos
, last
, "\n");
156 const SoundsSet
*sounds_set
= BaseSounds::GetUsedSet();
157 if (sounds_set
->GetNumInvalid() != 0) {
158 add_pos
+= seprintf(add_pos
, last
, "Trying to load sound set '%s', but it is incomplete. The game will probably not run correctly until you properly install this set or select another one. See section 4.1 of readme.txt.\n\nThe following files are corrupted or missing:\n", sounds_set
->name
);
160 assert_compile(SoundsSet::NUM_FILES
== 1);
161 /* No need to loop each file, as long as there is only a single
163 add_pos
+= seprintf(add_pos
, last
, "\t%s is %s (%s)\n", sounds_set
->files
->filename
, SoundsSet::CheckMD5(sounds_set
->files
, BASESET_DIR
) == MD5File::CR_MISMATCH
? "corrupt" : "missing", sounds_set
->files
->missing_warning
);
166 if (add_pos
!= error_msg
) ShowInfoF("%s", error_msg
);
169 /** Actually load the sprite tables. */
170 static void LoadSpriteTables()
172 memset(_palette_remap_grf
, 0, sizeof(_palette_remap_grf
));
173 uint i
= FIRST_GRF_SLOT
;
174 const GraphicsSet
*used_set
= BaseGraphics::GetUsedSet();
176 _palette_remap_grf
[i
] = (PAL_DOS
!= used_set
->palette
);
177 LoadGrfFile(used_set
->files
[GFT_BASE
].filename
, 0, i
++);
179 /* Tracerestrict sprites. */
180 LoadGrfFile("tracerestrict.grf", SPR_TRACERESTRICT_BASE
, i
++);
183 * The second basic file always starts at the given location and does
184 * contain a different amount of sprites depending on the "type"; DOS
185 * has a few sprites less. However, we do not care about those missing
186 * sprites as they are not shown anyway (logos in intro game).
188 _palette_remap_grf
[i
] = (PAL_DOS
!= used_set
->palette
);
189 LoadGrfFile(used_set
->files
[GFT_LOGOS
].filename
, 4793, i
++);
192 * Load additional sprites for climates other than temperate.
193 * This overwrites some of the temperate sprites, such as foundations
194 * and the ground sprites.
196 if (_settings_game
.game_creation
.landscape
!= LT_TEMPERATE
) {
197 _palette_remap_grf
[i
] = (PAL_DOS
!= used_set
->palette
);
199 used_set
->files
[GFT_ARCTIC
+ _settings_game
.game_creation
.landscape
- 1].filename
,
200 _landscape_spriteindexes
[_settings_game
.game_creation
.landscape
- 1],
205 /* Load route step graphics */
206 LoadGrfFile("route_step.grf", SPR_ROUTE_STEP_BASE
, i
++);
209 LoadGrfFile("innerhighlight.grf", SPR_ZONING_INNER_HIGHLIGHT_BASE
, i
++);
211 /* Initialize the unicode to sprite mapping table */
212 InitializeUnicodeGlyphMap();
214 /* Overgrown tracks */
215 LoadGrfFile("oldtracks.grf", SPR_OLDTRACKS_BASE
, i
++);
218 * Load the base and extra NewGRF with OTTD required graphics as first NewGRF.
219 * However, we do not want it to show up in the list of used NewGRFs,
220 * so we have to manually add it, and then remove it later.
222 GRFConfig
*top
= _grfconfig
;
224 /* Default extra graphics */
225 GRFConfig
*master
= new GRFConfig("OPENTTD.GRF");
226 master
->palette
|= GRFP_GRF_DOS
;
227 FillGRFDetails(master
, false, BASESET_DIR
);
228 ClrBit(master
->flags
, GCF_INIT_ONLY
);
230 /* Baseset extra graphics */
231 GRFConfig
*extra
= new GRFConfig(used_set
->files
[GFT_EXTRA
].filename
);
233 /* We know the palette of the base set, so if the base NewGRF is not
234 * setting one, use the palette of the base set and not the global
235 * one which might be the wrong palette for this base NewGRF.
236 * The value set here might be overridden via action14 later. */
237 switch (used_set
->palette
) {
238 case PAL_DOS
: extra
->palette
|= GRFP_GRF_DOS
; break;
239 case PAL_WINDOWS
: extra
->palette
|= GRFP_GRF_WINDOWS
; break;
242 FillGRFDetails(extra
, false, BASESET_DIR
);
243 ClrBit(extra
->flags
, GCF_INIT_ONLY
);
246 master
->next
= extra
;
249 LoadNewGRF(SPR_NEWGRFS_BASE
, i
, 2);
251 uint total_extra_graphics
= SPR_NEWGRFS_BASE
- SPR_OPENTTD_BASE
;
252 _missing_extra_graphics
= GetSpriteCountForSlot(i
, SPR_OPENTTD_BASE
, SPR_NEWGRFS_BASE
);
253 DEBUG(sprite
, 1, "%u extra sprites, %u from baseset, %u from fallback", total_extra_graphics
, total_extra_graphics
- _missing_extra_graphics
, _missing_extra_graphics
);
255 /* The original baseset extra graphics intentionally make use of the fallback graphics.
256 * Let's say everything which provides less than 500 sprites misses the rest intentionally. */
257 if (500 + _missing_extra_graphics
> total_extra_graphics
) _missing_extra_graphics
= 0;
259 /* Free and remove the top element. */
267 * Check blitter needed by NewGRF config and switch if needed.
268 * @return False when nothing changed, true otherwise.
270 static bool SwitchNewGRFBlitter()
272 /* Never switch if the blitter was specified by the user. */
273 if (!_blitter_autodetected
) return false;
275 /* Null driver => dedicated server => do nothing. */
276 if (BlitterFactory::GetCurrentBlitter()->GetScreenDepth() == 0) return false;
278 /* Get preferred depth.
279 * - depth_wanted_by_base: Depth required by the baseset, i.e. the majority of the sprites.
280 * - depth_wanted_by_grf: Depth required by some NewGRF.
281 * Both can force using a 32bpp blitter. depth_wanted_by_base is used to select
282 * between multiple 32bpp blitters, which perform differently with 8bpp sprites.
284 uint depth_wanted_by_base
= BaseGraphics::GetUsedSet()->blitter
== BLT_32BPP
? 32 : 8;
285 uint depth_wanted_by_grf
= _support8bpp
== S8BPP_NONE
? 32 : 8;
286 for (GRFConfig
*c
= _grfconfig
; c
!= nullptr; c
= c
->next
) {
287 if (c
->status
== GCS_DISABLED
|| c
->status
== GCS_NOT_FOUND
|| HasBit(c
->flags
, GCF_INIT_ONLY
)) continue;
288 if (c
->palette
& GRFP_BLT_32BPP
) depth_wanted_by_grf
= 32;
291 /* Search the best blitter. */
292 static const struct {
294 uint animation
; ///< 0: no support, 1: do support, 2: both
295 uint min_base_depth
, max_base_depth
, min_grf_depth
, max_grf_depth
;
296 } replacement_blitters
[] = {
298 { "32bpp-sse4", 0, 32, 32, 8, 32 },
299 { "32bpp-ssse3", 0, 32, 32, 8, 32 },
300 { "32bpp-sse2", 0, 32, 32, 8, 32 },
301 { "32bpp-sse4-anim", 1, 32, 32, 8, 32 },
303 { "8bpp-optimized", 2, 8, 8, 8, 8 },
304 { "32bpp-optimized", 0, 8, 32, 8, 32 },
306 { "32bpp-sse2-anim", 1, 8, 32, 8, 32 },
308 { "32bpp-anim", 1, 8, 32, 8, 32 },
311 const bool animation_wanted
= HasBit(_display_opt
, DO_FULL_ANIMATION
);
312 const char *cur_blitter
= BlitterFactory::GetCurrentBlitter()->GetName();
314 VideoDriver::GetInstance()->AcquireBlitterLock();
316 for (uint i
= 0; i
< lengthof(replacement_blitters
); i
++) {
317 if (animation_wanted
&& (replacement_blitters
[i
].animation
== 0)) continue;
318 if (!animation_wanted
&& (replacement_blitters
[i
].animation
== 1)) continue;
320 if (!IsInsideMM(depth_wanted_by_base
, replacement_blitters
[i
].min_base_depth
, replacement_blitters
[i
].max_base_depth
+ 1)) continue;
321 if (!IsInsideMM(depth_wanted_by_grf
, replacement_blitters
[i
].min_grf_depth
, replacement_blitters
[i
].max_grf_depth
+ 1)) continue;
322 const char *repl_blitter
= replacement_blitters
[i
].name
;
324 if (strcmp(repl_blitter
, cur_blitter
) == 0) {
325 VideoDriver::GetInstance()->ReleaseBlitterLock();
328 if (BlitterFactory::GetBlitterFactory(repl_blitter
) == nullptr) continue;
330 DEBUG(misc
, 1, "Switching blitter from '%s' to '%s'... ", cur_blitter
, repl_blitter
);
331 Blitter
*new_blitter
= BlitterFactory::SelectBlitter(repl_blitter
);
332 if (new_blitter
== nullptr) NOT_REACHED();
333 DEBUG(misc
, 1, "Successfully switched to %s.", repl_blitter
);
337 if (!VideoDriver::GetInstance()->AfterBlitterChange()) {
338 /* Failed to switch blitter, let's hope we can return to the old one. */
339 if (BlitterFactory::SelectBlitter(cur_blitter
) == nullptr || !VideoDriver::GetInstance()->AfterBlitterChange()) usererror("Failed to reinitialize video driver. Specify a fixed blitter in the config");
342 VideoDriver::GetInstance()->ReleaseBlitterLock();
347 /** Check whether we still use the right blitter, or use another (better) one. */
350 if (!SwitchNewGRFBlitter()) return;
353 GfxClearSpriteCache();
357 static void UpdateRouteStepSpriteSize()
359 extern uint _vp_route_step_width
;
360 extern uint _vp_route_step_height_top
;
361 extern uint _vp_route_step_height_middle
;
362 extern uint _vp_route_step_height_bottom
;
363 extern SubSprite _vp_route_step_subsprite
;
365 Dimension d
= GetSpriteSize(SPR_ROUTE_STEP_TOP
);
366 _vp_route_step_width
= d
.width
;
367 _vp_route_step_height_top
= d
.height
;
369 d
= GetSpriteSize(SPR_ROUTE_STEP_MIDDLE
);
370 _vp_route_step_height_middle
= d
.height
;
371 assert(_vp_route_step_width
== d
.width
);
373 d
= GetSpriteSize(SPR_ROUTE_STEP_BOTTOM
);
374 _vp_route_step_height_bottom
= d
.height
;
375 assert(_vp_route_step_width
== d
.width
);
377 const int char_height
= GetCharacterHeight(FS_SMALL
) + 1;
378 _vp_route_step_subsprite
.right
= ScaleByZoom(_vp_route_step_width
, ZOOM_LVL_GUI
);
379 _vp_route_step_subsprite
.bottom
= ScaleByZoom(char_height
, ZOOM_LVL_GUI
);
380 _vp_route_step_subsprite
.left
= 0;
381 _vp_route_step_subsprite
.top
= 0;
384 /* multi can be density, field type, ... */
385 static SpriteID
GetSpriteIDForClearGround(const ClearGround cg
, const Slope slope
, const uint multi
)
389 return GetSpriteIDForClearLand(slope
, (byte
)multi
);
391 return GetSpriteIDForHillyLand(slope
, multi
);
393 return GetSpriteIDForRocks(slope
, multi
);
395 return GetSpriteIDForFields(slope
, multi
);
398 return GetSpriteIDForSnowDesert(slope
, multi
);
399 default: NOT_REACHED();
403 /** Once the sprites are loaded, we can determine main colours of ground/water/... */
404 void GfxDetermineMainColours()
407 extern uint32 _vp_map_water_colour
[5];
408 _vp_map_water_colour
[0] = GetSpriteMainColour(SPR_FLAT_WATER_TILE
, PAL_NONE
);
409 if (BlitterFactory::GetCurrentBlitter()->GetScreenDepth() == 32) {
410 _vp_map_water_colour
[1] = Blitter_32bppBase::MakeTransparent(_vp_map_water_colour
[0], 256, 192).data
; // lighter
411 _vp_map_water_colour
[2] = Blitter_32bppBase::MakeTransparent(_vp_map_water_colour
[0], 192, 256).data
; // darker
412 _vp_map_water_colour
[3] = _vp_map_water_colour
[2];
413 _vp_map_water_colour
[4] = _vp_map_water_colour
[1];
417 extern uint32 _vp_map_vegetation_clear_colours
[16][6][8];
418 memset(_vp_map_vegetation_clear_colours
, 0, sizeof(_vp_map_vegetation_clear_colours
));
423 { 0, 3 }, // CLEAR_GRASS, density
424 { 0, 7 }, // CLEAR_ROUGH, "random" based on position
425 { 0, 1 }, // CLEAR_ROCKS, tile hash parity
426 { 0, 7 }, // CLEAR_FIELDS, some field types
427 { 0, 3 }, // CLEAR_SNOW, density
428 { 1, 3 }, // CLEAR_DESERT, density
430 for (uint s
= 0; s
<= SLOPE_ELEVATED
; s
++) {
431 for (uint cg
= 0; cg
< 6; cg
++) {
432 for (uint m
= multi
[cg
].min
; m
<= multi
[cg
].max
; m
++) {
433 _vp_map_vegetation_clear_colours
[s
][cg
][m
] = GetSpriteMainColour(GetSpriteIDForClearGround((ClearGround
)cg
, (Slope
)s
, m
), PAL_NONE
);
439 extern uint32 _vp_map_vegetation_tree_colours
[5][MAX_TREE_COUNT_BY_LANDSCAPE
];
440 const uint base
= _tree_base_by_landscape
[_settings_game
.game_creation
.landscape
];
441 const uint count
= _tree_count_by_landscape
[_settings_game
.game_creation
.landscape
];
442 for (uint tg
= 0; tg
< 5; tg
++) {
443 for (uint i
= base
; i
< base
+ count
; i
++) {
444 _vp_map_vegetation_tree_colours
[tg
][i
- base
] = GetSpriteMainColour(_tree_sprites
[i
].sprite
, _tree_sprites
[i
].pal
);
446 const int diff
= MAX_TREE_COUNT_BY_LANDSCAPE
- count
;
448 for (uint i
= count
; i
< MAX_TREE_COUNT_BY_LANDSCAPE
; i
++)
449 _vp_map_vegetation_tree_colours
[tg
][i
] = _vp_map_vegetation_tree_colours
[tg
][i
- count
];
454 /** Initialise and load all the sprites. */
455 void GfxLoadSprites()
457 DEBUG(sprite
, 2, "Loading sprite set %d", _settings_game
.game_creation
.landscape
);
459 _grf_bug_too_many_strings
= false;
461 SwitchNewGRFBlitter();
466 GfxDetermineMainColours();
468 UpdateRouteStepSpriteSize();
471 DEBUG(sprite
, 2, "Completed loading sprite set %d", _settings_game
.game_creation
.landscape
);
474 bool GraphicsSet::FillSetDetails(IniFile
*ini
, const char *path
, const char *full_filename
)
476 bool ret
= this->BaseSet
<GraphicsSet
, MAX_GFT
, true>::FillSetDetails(ini
, path
, full_filename
, false);
478 IniGroup
*metadata
= ini
->GetGroup("metadata");
481 fetch_metadata("palette");
482 this->palette
= (*item
->value
== 'D' || *item
->value
== 'd') ? PAL_DOS
: PAL_WINDOWS
;
484 /* Get optional blitter information. */
485 item
= metadata
->GetItem("blitter", false);
486 this->blitter
= (item
!= nullptr && *item
->value
== '3') ? BLT_32BPP
: BLT_8BPP
;
492 * Calculate and check the MD5 hash of the supplied GRF.
493 * @param file The file get the hash of.
494 * @param subdir The sub directory to get the files from.
496 * - #CR_MATCH if the MD5 hash matches
497 * - #CR_MISMATCH if the MD5 does not match
498 * - #CR_NO_FILE if the file misses
500 /* static */ MD5File::ChecksumResult
GraphicsSet::CheckMD5(const MD5File
*file
, Subdirectory subdir
)
503 FILE *f
= FioFOpenFile(file
->filename
, "rb", subdir
, &size
);
504 if (f
== nullptr) return MD5File::CR_NO_FILE
;
506 size_t max
= GRFGetSizeOfDataSection(f
);
510 return file
->CheckMD5(subdir
, max
);
515 * Calculate and check the MD5 hash of the supplied filename.
516 * @param subdir The sub directory to get the files from
517 * @param max_size Only calculate the hash for this many bytes from the file start.
519 * - #CR_MATCH if the MD5 hash matches
520 * - #CR_MISMATCH if the MD5 does not match
521 * - #CR_NO_FILE if the file misses
523 MD5File::ChecksumResult
MD5File::CheckMD5(Subdirectory subdir
, size_t max_size
) const
526 FILE *f
= FioFOpenFile(this->filename
, "rb", subdir
, &size
);
528 if (f
== nullptr) return CR_NO_FILE
;
530 size
= min(size
, max_size
);
537 while ((len
= fread(buffer
, 1, (size
> sizeof(buffer
)) ? sizeof(buffer
) : size
, f
)) != 0 && size
!= 0) {
539 checksum
.Append(buffer
, len
);
544 checksum
.Finish(digest
);
545 return memcmp(this->hash
, digest
, sizeof(this->hash
)) == 0 ? CR_MATCH
: CR_MISMATCH
;
548 /** Names corresponding to the GraphicsFileType */
549 static const char * const _graphics_file_names
[] = { "base", "logos", "arctic", "tropical", "toyland", "extra" };
551 /** Implementation */
552 template <class T
, size_t Tnum_files
, bool Tsearch_in_tars
>
553 /* static */ const char * const *BaseSet
<T
, Tnum_files
, Tsearch_in_tars
>::file_names
= _graphics_file_names
;
555 template <class Tbase_set
>
556 /* static */ bool BaseMedia
<Tbase_set
>::DetermineBestSet()
558 if (BaseMedia
<Tbase_set
>::used_set
!= nullptr) return true;
560 const Tbase_set
*best
= nullptr;
561 for (const Tbase_set
*c
= BaseMedia
<Tbase_set
>::available_sets
; c
!= nullptr; c
= c
->next
) {
562 /* Skip unusable sets */
563 if (c
->GetNumMissing() != 0) continue;
565 if (best
== nullptr ||
566 (best
->fallback
&& !c
->fallback
) ||
567 best
->valid_files
< c
->valid_files
||
568 (best
->valid_files
== c
->valid_files
&& (
569 (best
->shortname
== c
->shortname
&& best
->version
< c
->version
) ||
570 (best
->palette
!= PAL_DOS
&& c
->palette
== PAL_DOS
)))) {
575 BaseMedia
<Tbase_set
>::used_set
= best
;
576 return BaseMedia
<Tbase_set
>::used_set
!= nullptr;
579 template <class Tbase_set
>
580 /* static */ const char *BaseMedia
<Tbase_set
>::GetExtension()
582 return ".obg"; // OpenTTD Base Graphics
585 INSTANTIATE_BASE_MEDIA_METHODS(BaseMedia
<GraphicsSet
>, GraphicsSet
)