1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Loads all standard graphics from Graphics.c4g */
18
19#include "C4GuiResource.h"
20#include <C4Include.h>
21#include <C4GraphicsResource.h>
22
23#include <C4Gui.h>
24#include <C4Log.h>
25#include <C4Game.h>
26
27#include <StdGL.h>
28
29#include <array>
30
31C4GraphicsResource::C4GraphicsResource()
32{
33 Default();
34}
35
36C4GraphicsResource::~C4GraphicsResource()
37{
38 Clear();
39}
40
41void C4GraphicsResource::Default()
42{
43 fInitialized = false;
44
45 sfcControl.Default();
46 idSfcControl = 0;
47 idPalGrp = 0;
48
49 fctPlayer.Default();
50 fctFlag.Default();
51 fctCrew.Default();
52 fctScore.Default();
53 fctWealth.Default();
54 fctRank.Default();
55 fctFire.Default();
56 fctBackground.Default();
57 sfcLiquidAnimation.Default(); idSfcLiquidAnimation = 0;
58 fctCaptain.Default();
59 fctMouseCursor.Default();
60 fctSelectMark.Default();
61 fctMenu.Default();
62 fctUpperBoard.Default();
63 fctLogo.Default();
64 fctConstruction.Default();
65 fctEnergy.Default();
66 fctMagic.Default();
67 fctArrow.Default();
68 fctExit.Default();
69 fctHand.Default();
70 fctGamepad.Default();
71 fctBuild.Default();
72 fctEnergyBars.Default();
73
74 std::fill(first: GamePalette, last: std::end(arr&: GamePalette), value: 0);
75 std::fill(first: AlphaPalette, last: std::end(arr&: AlphaPalette), value: 0);
76 fctCrewClr.Default();
77 fctFlagClr.Default();
78 fctPlayerClr.Default();
79
80 fctCursor.Default();
81 fctDropTarget.Default();
82 fctInsideSymbol.Default();
83 fctKeyboard.Default();
84 fctGamepad.Default();
85 fctCommand.Default();
86 fctKey.Default();
87 fctOKCancel.Default();
88 fctMouse.Default();
89
90 iNumRanks = 1;
91 idRegisteredMainGroupSetFiles = -1;
92 fOldStyleCursor = false;
93}
94
95void C4GraphicsResource::Clear()
96{
97 fInitialized = false;
98 // GUI data
99 C4GUI::Resource::Unload();
100
101 sfcControl.Clear();
102 idSfcControl = 0;
103 idPalGrp = 0;
104
105 fctCrewClr.Clear();
106 fctFlagClr.Clear();
107 fctPlayerClr.Clear();
108 fctPlayerGray.Clear();
109
110 fctPlayer.Clear();
111 fctFlag.Clear();
112 fctCrew.Clear();
113 fctScore.Clear();
114 fctWealth.Clear();
115 fctRank.Clear();
116 fctFire.Clear();
117 fctBackground.Clear();
118 sfcLiquidAnimation.Clear();
119 fctCaptain.Clear();
120 fctMouseCursor.Clear();
121 fctSelectMark.Clear();
122 fctMenu.Clear();
123 fctUpperBoard.Clear();
124 fctLogo.Clear();
125 fctConstruction.Clear();
126 fctEnergy.Clear();
127 fctMagic.Clear();
128 fctArrow.Clear();
129 fctExit.Clear();
130 fctHand.Clear();
131 fctGamepad.Clear();
132 fctBuild.Clear();
133 fctEnergyBars.Clear();
134
135 // unhook deflist from font
136 FontRegular.SetCustomImages(nullptr);
137
138 // closing the group set will also close the graphics.c4g
139 // this is just for games that failed to init
140 // normally, this is done after successful init anyway
141 CloseFiles();
142}
143
144bool C4GraphicsResource::InitFonts()
145{
146 // update group set
147 if (!RegisterMainGroups())
148 {
149 LogFatal(id: C4ResStrTableKey::IDS_ERR_GFX_REGISTERMAIN);
150 return false;
151 }
152 // reinit main font
153 // this regards scenario-specific fonts or overloads in Extra.c4g
154 const char *szFont;
155 if (*Game.C4S.Head.Font) szFont = Game.C4S.Head.Font; else szFont = Config.General.RXFontName;
156#ifndef USE_CONSOLE
157 if (!Game.FontLoader.InitFont(rFont&: FontRegular, szFontName: szFont, eType: C4FontLoader::C4FT_Main, iSize: Config.General.RXFontSize, pGfxGroups: &Files))
158 return false;
159 // assign def list as custom image source
160 FontRegular.SetCustomImages(&Game.Defs);
161 // load additional fonts
162 if (!Game.FontLoader.InitFont(rFont&: FontTitle, szFontName: szFont, eType: C4FontLoader::C4FT_Title, iSize: Config.General.RXFontSize, pGfxGroups: &Game.GraphicsResource.Files)) return false;
163 if (!Game.FontLoader.InitFont(rFont&: FontCaption, szFontName: szFont, eType: C4FontLoader::C4FT_Caption, iSize: Config.General.RXFontSize, pGfxGroups: &Game.GraphicsResource.Files)) return false;
164 if (!Game.FontLoader.InitFont(rFont&: FontTiny, szFontName: szFont, eType: C4FontLoader::C4FT_Log, iSize: Config.General.RXFontSize, pGfxGroups: &Game.GraphicsResource.Files)) return false;
165 if (!Game.FontLoader.InitFont(rFont&: FontTooltip, szFontName: szFont, eType: C4FontLoader::C4FT_Main, iSize: Config.General.RXFontSize, pGfxGroups: &Game.GraphicsResource.Files, fDoShadow: false)) return false;
166#endif
167 // done, success
168 return true;
169}
170
171bool C4GraphicsResource::Init()
172{
173 // Init fonts (double init will never if groups didnt change)
174 if (!InitFonts())
175 return false;
176 // Game palette - could perhaps be eliminated...
177 int32_t idNewPalGrp;
178 C4Group *pPalGrp = Files.FindEntry(szWildcard: "C4.pal", pPriority: nullptr, pID: &idNewPalGrp);
179 if (!pPalGrp) { LogNTr(level: spdlog::level::err, fmt: "{}: {}", args: LoadResStr(id: C4ResStrTableKey::IDS_PRC_FILENOTFOUND), args: "C4.pal"); return false; }
180 if (idPalGrp != idNewPalGrp)
181 {
182 if (!pPalGrp->AccessEntry(szWildCard: "C4.pal")) { LogFatalNTr(message: "Pal error!"); return false; }
183 if (!pPalGrp->Read(pBuffer: GamePalette, iSize: 256 * 3)) { LogFatalNTr(message: "Pal error!"); return false; }
184 for (int32_t cnt = 0; cnt < 256 * 3; cnt++) GamePalette[cnt] <<= 2;
185 std::fill_n(first: AlphaPalette, n: 256, value: 0);
186 // Set default force field color
187 GamePalette[191 * 3 + 0] = 0;
188 GamePalette[191 * 3 + 1] = 0;
189 GamePalette[191 * 3 + 2] = 255;
190 // color 0 is transparent
191 GamePalette[0] = GamePalette[1] = GamePalette[2] = 0;
192 AlphaPalette[0] = 255;
193 AlphaPalette[191] = 127;
194 // update game pal
195 if (!Game.GraphicsSystem.SetPalette()) { LogFatalNTr(message: "Pal error (2)!"); return false; }
196 idPalGrp = idNewPalGrp;
197 }
198
199 // Control
200 if (!LoadFile(sfc&: sfcControl, szName: "Control", rGfxSet&: Files, ridCurrSfc&: idSfcControl)) return false;
201 fctKeyboard.Set(nsfc: &sfcControl, nx: 0, ny: 0, nwdt: 80, nhgt: 36);
202 fctCommand .Set(nsfc: &sfcControl, nx: 0, ny: 36, nwdt: 32, nhgt: 32);
203 fctKey .Set(nsfc: &sfcControl, nx: 0, ny: 100, nwdt: 64, nhgt: 64);
204 fctOKCancel.Set(nsfc: &sfcControl, nx: 128, ny: 100, nwdt: 32, nhgt: 32);
205 fctMouse .Set(nsfc: &sfcControl, nx: 198, ny: 100, nwdt: 32, nhgt: 32);
206
207 // Facet bitmap resources
208 if (!LoadFile(fct&: fctFire, szName: "Fire", rGfxSet&: Files, iWdt: C4FCT_Height)) return false;
209 if (!LoadFile(fct&: fctBackground, szName: "Background", rGfxSet&: Files)) return false;
210 if (!LoadFile(fct&: fctFlag, szName: "Flag", rGfxSet&: Files)) return false; // (new format)
211 if (!LoadFile(fct&: fctCrew, szName: "Crew", rGfxSet&: Files)) return false; // (new format)
212 if (!LoadFile(fct&: fctScore, szName: "Score", rGfxSet&: Files)) return false; // (new)
213 if (!LoadFile(fct&: fctWealth, szName: "Wealth", rGfxSet&: Files)) return false; // (new)
214 if (!LoadFile(fct&: fctPlayer, szName: "Player", rGfxSet&: Files)) return false; // (new format)
215 if (!LoadFile(fct&: fctRank, szName: "Rank", rGfxSet&: Files, iWdt: C4FCT_Height)) return false;
216 if (!LoadFile(fct&: fctCaptain, szName: "Captain", rGfxSet&: Files)) return false;
217 if (!LoadCursorGfx()) return false;
218 if (!LoadFile(fct&: fctSelectMark, szName: "SelectMark", rGfxSet&: Files, iWdt: C4FCT_Height)) return false;
219 if (!LoadFile(fct&: fctMenu, szName: "Menu", rGfxSet&: Files, iWdt: 35, iHgt: 35)) return false;
220 if (!LoadFile(fct&: fctLogo, szName: "Logo", rGfxSet&: Files)) return false;
221 if (!LoadFile(fct&: fctConstruction, szName: "Construction", rGfxSet&: Files)) return false; // (new)
222 if (!LoadFile(fct&: fctEnergy, szName: "Energy", rGfxSet&: Files)) return false; // (new)
223 if (!LoadFile(fct&: fctMagic, szName: "Magic", rGfxSet&: Files)) return false; // (new)
224 if (!LoadFile(fct&: fctOptions, szName: "Options", rGfxSet&: Files, iWdt: C4FCT_Height)) return false;
225 if (!LoadFile(fct&: fctUpperBoard, szName: "UpperBoard", rGfxSet&: Files)) return false;
226 if (!LoadFile(fct&: fctArrow, szName: "Arrow", rGfxSet&: Files, iWdt: C4FCT_Height)) return false;
227 if (!LoadFile(fct&: fctExit, szName: "Exit", rGfxSet&: Files)) return false;
228 if (!LoadFile(fct&: fctHand, szName: "Hand", rGfxSet&: Files, iWdt: C4FCT_Height)) return false;
229 if (!LoadFile(fct&: fctGamepad, szName: "Gamepad", rGfxSet&: Files, iWdt: 80)) return false;
230 if (!LoadFile(fct&: fctBuild, szName: "Build", rGfxSet&: Files)) return false;
231 if (!LoadFile(fct&: fctEnergyBars, szName: "EnergyBars", rGfxSet&: Files)) return false;
232 if (!LoadFile(sfc&: sfcLiquidAnimation, szName: "Liquid", rGfxSet&: Files, ridCurrSfc&: idSfcLiquidAnimation)) return false;
233 if (!ReloadResolutionDependentFiles()) return false;
234 // life bar facets
235 if (fctEnergyBars.Surface)
236 {
237 int32_t bar_wdt = fctEnergyBars.Surface->Wdt / 6;
238 int32_t bar_hgt = fctEnergyBars.Surface->Hgt / 3;
239 if (!bar_wdt || !bar_hgt) { LogFatalNTr(message: "EnergyBars.png invalid or too small!"); return false; }
240 fctEnergyBars.Set(nsfc: fctEnergyBars.Surface, nx: 0, ny: 0, nwdt: bar_wdt, nhgt: bar_hgt);
241 }
242
243 // create ColorByOwner overlay surfaces
244 if (fctCrew.idSourceGroup != fctCrewClr.idSourceGroup)
245 {
246 if (!fctCrewClr.CreateClrByOwner(pBySurface: fctCrew.Surface)) { LogFatalNTr(message: "ClrByOwner error! (1)"); return false; }
247 fctCrewClr.Wdt = fctCrew.Wdt;
248 fctCrewClr.Hgt = fctCrew.Hgt;
249 fctCrewClr.idSourceGroup = fctCrew.idSourceGroup;
250 }
251 if (fctFlag.idSourceGroup != fctFlagClr.idSourceGroup)
252 {
253 if (!fctFlagClr.CreateClrByOwner(pBySurface: fctFlag.Surface)) { LogFatalNTr(message: "ClrByOwner error! (1)"); return false; }
254 fctFlagClr.Wdt = fctFlag.Wdt;
255 fctFlagClr.Hgt = fctFlag.Hgt;
256 fctFlagClr.idSourceGroup = fctFlag.idSourceGroup;
257 }
258 if (fctPlayer.idSourceGroup != fctPlayerGray.idSourceGroup)
259 {
260 fctPlayerGray.Create(iWdt: fctPlayer.Wdt, iHgt: fctPlayer.Hgt);
261 fctPlayer.Draw(cgo&: fctPlayerGray);
262 fctPlayerGray.Grayscale(iOffset: 30);
263 fctPlayerGray.idSourceGroup = fctPlayer.idSourceGroup;
264 }
265 if (fctPlayer.idSourceGroup != fctPlayerClr.idSourceGroup)
266 {
267 if (!fctPlayerClr.CreateClrByOwner(pBySurface: fctPlayer.Surface)) { LogFatalNTr(message: "ClrByOwner error! (1)"); return false; }
268 fctPlayerClr.Wdt = fctPlayer.Wdt;
269 fctPlayerClr.Hgt = fctPlayer.Hgt;
270 fctPlayerClr.idSourceGroup = fctPlayer.idSourceGroup;
271 }
272
273 // get number of ranks
274 int32_t Q; fctRank.GetPhaseNum(rX&: iNumRanks, rY&: Q);
275 if (!iNumRanks) iNumRanks = 1;
276
277 // load GUI files
278 C4GUI::Resource *pRes = C4GUI::GetRes();
279 if (!pRes) pRes = new C4GUI::Resource(FontCaption, FontTitle, FontRegular, FontTiny, FontTooltip);
280 if (!pRes->Load(rFromGroup&: Files)) { delete pRes; return false; }
281
282 // CloseFiles() must not be called now:
283 // The sky still needs to be loaded from the global graphics
284 // group in C4Game::InitGame -> C4Sky::Init so we need to keep the group(s) open.
285 // In activated NETWORK2, the files mustn't be closed either, because there will be
286 // multiple calls to this function to allow overloadings
287
288 // mark initialized
289 fInitialized = true;
290
291 return true;
292}
293
294bool C4GraphicsResource::LoadCursorGfx()
295{
296 // old-style cursor file overloads new-stye, because old scenarios might want to have their own cursors
297 if (!LoadFile(fct&: fctMouseCursor, szName: "Cursor", rGfxSet&: Files, iWdt: C4FCT_Height, iHgt: C4FCT_Full, fNoWarnIfNotFound: true))
298 {
299 struct CursorSize
300 {
301 const char *filename;
302 std::size_t facetIndex;
303 };
304 // the order needs to match the order defined in the sorting list in C4Components.h
305 static constexpr CursorSize cursors[]
306 {
307 {.filename: "CursorSmall", .facetIndex: 7},
308 {.filename: "CursorMedium", .facetIndex: 6},
309 {.filename: "CursorLarge", .facetIndex: 5},
310 {.filename: "CursorXLarge", .facetIndex: 4},
311 {.filename: "CursorXXLarge", .facetIndex: 3},
312 {.filename: "CursorXXXLarge", .facetIndex: 2},
313 {.filename: "CursorXXXXLarge", .facetIndex: 1},
314 {.filename: "CursorXXXXXLarge", .facetIndex: 0},
315 };
316 for (const auto &cursor : cursors)
317 {
318 if (!LoadFile(fct&: fctCursors[cursor.facetIndex], szName: cursor.filename, rGfxSet&: Files, iWdt: C4FCT_Height, iHgt: C4FCT_Full))
319 return false;
320 }
321 }
322 return true;
323}
324
325void C4GraphicsResource::ApplyCursorGfx()
326{
327 // adjust dependent faces
328 int32_t iCursorSize = fctMouseCursor.Hgt;
329 if (iCursorSize == 13)
330 {
331 fctCursor.Set(nsfc: fctMouseCursor.Surface, nx: 455, ny: 0, nwdt: 13, nhgt: 13);
332 fOldStyleCursor = true;
333 }
334 else
335 {
336 fctCursor.Set(nsfc: fctMouseCursor.Surface, nx: 35 * iCursorSize, ny: 0, nwdt: iCursorSize, nhgt: iCursorSize);
337 fOldStyleCursor = false;
338 }
339 if (iCursorSize == 13)
340 {
341 fctInsideSymbol.Set(nsfc: fctMouseCursor.Surface, nx: 468, ny: 0, nwdt: 13, nhgt: 13);
342 fctDropTarget .Set(nsfc: fctMouseCursor.Surface, nx: 494, ny: 0, nwdt: 13, nhgt: 13);
343 }
344 else
345 {
346 fctInsideSymbol.Set(nsfc: fctMouseCursor.Surface, nx: 36 * iCursorSize, ny: 0, nwdt: iCursorSize, nhgt: iCursorSize);
347 fctDropTarget .Set(nsfc: fctMouseCursor.Surface, nx: 38 * iCursorSize, ny: 0, nwdt: iCursorSize, nhgt: iCursorSize);
348 }
349}
350
351bool C4GraphicsResource::RegisterGlobalGraphics()
352{
353 // Create main gfx group - register with fixed ID 1, to prevent unnecessary font reloading.
354 // FontLoader-initializations always check whether the font has already been initialized
355 // with the same parameters. If the game is simply reloaded in console-mode, this means
356 // that non-bitmap-fonts are not reinitialized. This will also apply for InGame-scenario
357 // switches yet to be implemented.
358 // Bitmap fonts from other groups are always reloaded, because the group indices of the gfx
359 // group set are not reset, and will then differ for subsequent group registrations.
360 // Resetting the group index of the gfx group set at game reset would cause problems if a
361 // scenario with its own font face is being closed, and then another scenario with another,
362 // overloaded font face is opened. The group indices could match and the old font would
363 // then be kept.
364 // The cleanest alternative would be to reinit all the fonts whenever a scenario is reloaded
365 C4Group *pMainGfxGrp = new C4Group();
366 if (!pMainGfxGrp->Open(C4CFN_Graphics) || !Files.RegisterGroup(rGroup&: *pMainGfxGrp, fOwnGrp: true, C4GSPrio_Base, C4GSCnt_Graphics, fCheckContent: 1))
367 {
368 // error
369 LogFatal(id: C4ResStrTableKey::IDS_PRC_NOGFXFILE, C4CFN_Graphics, args: pMainGfxGrp->GetError());
370 delete pMainGfxGrp;
371 return false;
372 }
373 return true;
374}
375
376bool C4GraphicsResource::RegisterMainGroups()
377{
378 // register main groups
379 Files.RegisterGroups(rCopy&: Game.GroupSet, C4GSCnt_Graphics, C4CFN_Graphics, iMaxSkipID: idRegisteredMainGroupSetFiles);
380 idRegisteredMainGroupSetFiles = Game.GroupSet.GetLastID();
381 return true;
382}
383
384void C4GraphicsResource::CloseFiles()
385{
386 // closes main gfx group; releases dependencies into game group set
387 Files.Clear();
388 idRegisteredMainGroupSetFiles = -1;
389}
390
391C4Group *FindSuitableFile(const char *szName, C4GroupSet &rGfxSet, char *szFileName, int32_t &rGroupID)
392{
393 const char *const extensions[] = { "bmp", "jpeg", "jpg", "png" };
394
395 C4Group *pGrp = nullptr;
396 C4Group *pGrp2;
397 int32_t iPrio = -1;
398 int32_t iPrio2;
399 int32_t GroupID;
400 char FileName[_MAX_FNAME];
401 SCopy(szSource: szName, sTarget: FileName);
402 for (int i = 0; i < 4; ++i)
403 {
404 EnforceExtension(szFileName: FileName, szExtension: extensions[i]);
405 pGrp2 = rGfxSet.FindEntry(szWildcard: FileName, pPriority: &iPrio2, pID: &GroupID);
406 if ((!pGrp || iPrio2 >= iPrio) && pGrp2)
407 {
408 rGroupID = GroupID;
409 pGrp = pGrp2;
410 SCopy(szSource: FileName, sTarget: szFileName);
411 }
412 }
413 // return found group, if any
414 return pGrp;
415}
416
417bool C4GraphicsResource::LoadFile(C4FacetExID &fct, const char *szName, C4GroupSet &rGfxSet, int32_t iWdt, int32_t iHgt, bool fNoWarnIfNotFound)
418{
419 char FileName[_MAX_FNAME]; int32_t ID;
420 C4Group *pGrp = FindSuitableFile(szName, rGfxSet, szFileName: FileName, rGroupID&: ID);
421 if (!pGrp)
422 {
423 // FIXME: Use LogFatal here
424 if (!fNoWarnIfNotFound)
425 {
426 Log(id: C4ResStrTableKey::IDS_PRC_NOGFXFILE, args&: szName, args: LoadResStr(id: C4ResStrTableKey::IDS_PRC_FILENOTFOUND));
427 }
428 return false;
429 }
430 // check group
431 if (fct.idSourceGroup == ID)
432 // already up-to-date
433 return true;
434 // load
435 if (!fct.Load(hGroup&: *pGrp, szName: FileName, iWdt, iHgt))
436 {
437 Log(id: C4ResStrTableKey::IDS_PRC_NOGFXFILE, args: +FileName, args: LoadResStr(id: C4ResStrTableKey::IDS_ERR_NOFILE));
438 return false;
439 }
440 fct.idSourceGroup = ID;
441 return true;
442}
443
444bool C4GraphicsResource::LoadFile(C4Surface &sfc, const char *szName, C4GroupSet &rGfxSet, int32_t &ridCurrSfc)
445{
446 // find
447 char FileName[_MAX_FNAME]; int32_t ID;
448 C4Group *pGrp = FindSuitableFile(szName, rGfxSet, szFileName: FileName, rGroupID&: ID);
449 if (!pGrp)
450 {
451 Log(id: C4ResStrTableKey::IDS_PRC_NOGFXFILE, args&: szName, args: LoadResStr(id: C4ResStrTableKey::IDS_PRC_FILENOTFOUND));
452 return false;
453 }
454 // check group
455 if (ID == ridCurrSfc)
456 // already up-to-date
457 return true;
458 // load
459 if (!sfc.Load(hGroup&: *pGrp, szFilename: FileName))
460 {
461 Log(id: C4ResStrTableKey::IDS_PRC_NOGFXFILE, args: +FileName, args: LoadResStr(id: C4ResStrTableKey::IDS_ERR_NOFILE));
462 return false;
463 }
464 ridCurrSfc = ID;
465 return true;
466}
467
468bool C4GraphicsResource::ReloadResolutionDependentFiles()
469{
470 // reload any files that depend on the current resolution
471 // reloads the cursor
472 const auto scale = Application.GetScale();
473 const auto resX = Config.Graphics.ResX * scale;
474 constexpr std::array<int32_t, 2> breakPoints
475 {
476 1280,
477 800
478 };
479 size_t index = 5;
480 if (resX > breakPoints.front())
481 {
482 index -= std::min(a: index, b: static_cast<size_t>(std::max(a: 1.f, b: Application.GetScale()) - 0.5f));
483 }
484 else
485 {
486 for (const auto selectX : breakPoints)
487 {
488 if (resX >= selectX)
489 {
490 break;
491 }
492 ++index;
493 }
494 }
495 if (!fctCursors[index].Wdt && !LoadCursorGfx()) return false;
496 if (fctCursors[index].Wdt)
497 {
498 fctMouseCursor.idSourceGroup = 0;
499 fctMouseCursor.Set(fctCursors[index]);
500 ApplyCursorGfx();
501 return true;
502 }
503 return false;
504}
505