1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2021, 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/* Operates viewports, message board and draws the game */
18
19#include <C4GraphicsSystem.h>
20
21#include <C4Viewport.h>
22#include <C4Application.h>
23#include <C4Console.h>
24#include <C4Random.h>
25#include <C4SurfaceFile.h>
26#include <C4FullScreen.h>
27#include <C4Gui.h>
28#include <C4LoaderScreen.h>
29#include <C4Wrappers.h>
30#include <C4Player.h>
31#include <C4Object.h>
32#include <C4SoundSystem.h>
33
34#include <StdBitmap.h>
35#include <StdPNG.h>
36
37#ifdef _WIN32
38#include "res/engine_resource.h"
39#endif
40
41#include <algorithm>
42#include <format>
43#include <stdexcept>
44
45C4GraphicsSystem::C4GraphicsSystem()
46{
47 Default();
48}
49
50C4GraphicsSystem::~C4GraphicsSystem()
51{
52 Clear();
53}
54
55bool C4GraphicsSystem::Init()
56{
57 // Success
58 return true;
59}
60
61void C4GraphicsSystem::Clear()
62{
63 // Clear message board
64 MessageBoard.Clear();
65 // Clear upper board
66 UpperBoard.Clear();
67 // clear loader
68 delete pLoaderScreen; pLoaderScreen = nullptr;
69 // Close viewports
70 Viewports.clear();
71 // No debug stuff
72 DeactivateDebugOutput();
73}
74
75bool C4GraphicsSystem::SetPalette()
76{
77 // Set primary palette by game palette
78 if (!Application.DDraw->SetPrimaryPalette(pBuf: Game.GraphicsResource.GamePalette, pAlphaBuf: Game.GraphicsResource.AlphaPalette)) return false;
79 return true;
80}
81
82extern int32_t iLastControlSize, iPacketDelay;
83extern int32_t ControlQueueSize, ControlQueueDataSize;
84
85int32_t ScreenTick = 0, ScreenRate = 1;
86
87bool C4GraphicsSystem::StartDrawing()
88{
89 // only if ddraw is ready
90 if (!Application.DDraw) return false;
91 if (!Application.DDraw->Active) return false;
92
93 // only in main thread
94 if (!Application.IsMainThread()) return false;
95
96 // Don't draw if application is inactive (unless enabled by config)
97 if (!Application.Active)
98 {
99 // Player mode
100 const auto &renderInactive = Config.Graphics.RenderInactive;
101 if (Application.isFullScreen && !(renderInactive & C4ConfigGraphics::Fullscreen))
102 return false;
103 // Developer mode
104 if (!Application.isFullScreen && !(renderInactive & C4ConfigGraphics::Console))
105 return false;
106 }
107
108 // drawing OK
109 return true;
110}
111
112void C4GraphicsSystem::FinishDrawing()
113{
114 if (Application.isFullScreen) Application.DDraw->PageFlip();
115}
116
117void C4GraphicsSystem::Execute()
118{
119 // activity check
120 if (!StartDrawing()) return;
121
122 bool fBGDrawn = false;
123
124 // If lobby running, message board only (page flip done by startup message board)
125 if (!Game.pGUI || !Game.pGUI->HasFullscreenDialog(fIncludeFading: true)) // allow for message board behind GUI
126 if (Game.Network.isLobbyActive() || !Game.IsRunning)
127 if (Application.isFullScreen)
128 {
129 // Message board
130 if (iRedrawBackground) ClearFullscreenBackground();
131 MessageBoard.Execute();
132 if (!Game.pGUI || !C4GUI::IsActive())
133 {
134 FinishDrawing(); return;
135 }
136 fBGDrawn = true;
137 }
138
139 // fullscreen GUI?
140 if (Application.isFullScreen && Game.pGUI && C4GUI::IsActive() && (Game.pGUI->HasFullscreenDialog(fIncludeFading: false) || !Game.IsRunning))
141 {
142 if (!fBGDrawn && iRedrawBackground) ClearFullscreenBackground();
143 Game.pGUI->Render(fDoBG: !fBGDrawn);
144 FinishDrawing();
145 return;
146 }
147
148 // Fixed screen rate in old network
149 ScreenRate = 1;
150
151 // Background redraw
152 if (Application.isFullScreen)
153 if (iRedrawBackground)
154 DrawFullscreenBackground();
155
156 // Screen rate skip frame draw
157 ScreenTick++; if (ScreenTick >= ScreenRate) ScreenTick = 0;
158
159 // Reset object audibility
160 Game.Objects.ResetAudibility();
161
162 // some hack to ensure the mouse is drawn after a dialog close and before any
163 // movement messages
164 if (Game.pGUI && !C4GUI::IsActive())
165 SetMouseInGUI(fInGUI: false, fByMouse: false);
166
167 // Viewports
168 for (const auto &cvp : Viewports)
169 cvp->Execute();
170
171 if (Application.isFullScreen)
172 {
173 // Message board
174 MessageBoard.Execute();
175
176 // Upper board
177 UpperBoard.Execute();
178
179 // Help & Messages
180 DrawHelp();
181 DrawHoldMessages();
182 DrawFlashMessage();
183 }
184
185 // InGame-GUI
186 if (Game.pGUI && C4GUI::IsActive())
187 {
188 Game.pGUI->Render(fDoBG: false);
189 }
190
191 // Palette update
192 if (fSetPalette) { SetPalette(); fSetPalette = false; }
193
194 // gamma update
195 if (fSetGamma)
196 {
197 ApplyGamma();
198 fSetGamma = false;
199 }
200
201 // done
202 FinishDrawing();
203}
204
205bool C4GraphicsSystem::CloseViewport(C4Viewport *cvp)
206{
207 if (!cvp) return false;
208 if (const auto it = std::find_if(first: Viewports.begin(), last: Viewports.end(), pred: [cvp](const auto &viewport)
209 {
210 return viewport.get() == cvp;
211 }); it != Viewports.end())
212 {
213 Viewports.erase(position: it);
214 }
215 else
216 {
217 return false;
218 }
219 StartSoundEffect(name: "CloseViewport");
220 // Recalculate viewports
221 RecalculateViewports();
222 // Done
223 return true;
224}
225
226bool C4GraphicsSystem::CreateViewport(int32_t iPlayer, bool fSilent)
227{
228 // Create and init new viewport, add to viewport list
229 C4Viewport *nvp = new C4Viewport;
230 bool fOkay = false;
231 if (Application.isFullScreen)
232 fOkay = nvp->Init(iPlayer, fSetTempOnly: false);
233 else
234 fOkay = nvp->Init(pParent: &Console, pApp: &Application, iPlayer);
235 if (!fOkay)
236 {
237 delete nvp;
238 return false;
239 }
240 Viewports.emplace_back(args&: nvp);
241 // Recalculate viewports
242 RecalculateViewports();
243 // Viewports start off at centered position
244 nvp->CenterPosition();
245 // Action sound
246 if (!fSilent)
247 {
248 StartSoundEffect(name: "CloseViewport");
249 }
250 return true;
251}
252
253void C4GraphicsSystem::ClearPointers(C4Object *pObj)
254{
255 for (const auto &cvp : Viewports)
256 cvp->ClearPointers(pObj);
257}
258
259void C4GraphicsSystem::Default()
260{
261 UpperBoard.Default();
262 MessageBoard.Default();
263 Viewports.clear();
264 InvalidateBg();
265 ViewportArea.Default();
266 ShowVertices = false;
267 ShowAction = false;
268 ShowCommand = false;
269 ShowEntrance = false;
270 ShowPathfinder = false;
271 ShowNetstatus = false;
272 ShowSolidMask = false;
273 ShowHelp = false;
274 FlashMessageText[0] = 0;
275 FlashMessageTime = 0; FlashMessageX = FlashMessageY = 0;
276 fSetPalette = false;
277 for (int32_t iRamp = 0; iRamp < 3 * C4MaxGammaRamps; iRamp += 3)
278 {
279 dwGamma[iRamp + 0] = 0; dwGamma[iRamp + 1] = 0x808080; dwGamma[iRamp + 2] = 0xffffff;
280 }
281 fSetGamma = false;
282 pLoaderScreen = nullptr;
283}
284
285void C4GraphicsSystem::DrawFullscreenBackground()
286{
287 for (size_t i = 0, iNum = BackgroundAreas.GetCount(); i < iNum; ++i)
288 {
289 const C4Rect &rc = BackgroundAreas.Get(idx: i);
290 Application.DDraw->BlitSurfaceTile(sfcSurface: Game.GraphicsResource.fctBackground.Surface, sfcTarget: Application.DDraw->lpBack, iToX: rc.x, iToY: rc.y, iToWdt: rc.Wdt, iToHgt: rc.Hgt, iOffsetX: -rc.x, iOffsetY: -rc.y);
291 }
292 --iRedrawBackground;
293}
294
295void C4GraphicsSystem::ClearFullscreenBackground()
296{
297 Application.DDraw->FillBG(dwClr: 0);
298 --iRedrawBackground;
299}
300
301bool C4GraphicsSystem::InitLoaderScreen(const char *szLoaderSpec)
302{
303 // create new loader; overwrite current only if successful
304 C4LoaderScreen *pNewLoader = new C4LoaderScreen();
305 if (!pNewLoader->Init(szLoaderSpec)) { delete pNewLoader; return false; }
306 delete pLoaderScreen;
307 pLoaderScreen = pNewLoader;
308 // apply user gamma for loader
309 ApplyGamma();
310 // done, success
311 return true;
312}
313
314bool C4GraphicsSystem::CloseViewport(int32_t iPlayer, bool fSilent)
315{
316 // Close all matching viewports
317 if (const auto it = std::remove_if(first: Viewports.begin(), last: Viewports.end(), pred: [iPlayer](const auto &viewport)
318 {
319 return viewport->Player == iPlayer || (iPlayer == NO_OWNER && viewport->fIsNoOwnerViewport);
320 }); it != Viewports.end())
321 {
322 Viewports.erase(first: it, last: Viewports.end());
323 // Recalculate viewports
324 RecalculateViewports();
325 if (!fSilent)
326 {
327 // Action sound
328 StartSoundEffect(name: "CloseViewport");
329 }
330 }
331 return true;
332}
333
334void C4GraphicsSystem::RecalculateViewports()
335{
336 // Fullscreen only
337 if (!Application.isFullScreen) return;
338
339 // Sort viewports
340 SortViewportsByPlayerControl();
341
342 // Viewport area
343 int32_t iBorderTop = 0, iBorderBottom = 0;
344 if (Config.Graphics.UpperBoard)
345 iBorderTop = C4UpperBoard::Height();
346 iBorderBottom = MessageBoard.Output.Hgt;
347 ViewportArea.Set(nsfc: Application.DDraw->lpBack, nx: 0, ny: iBorderTop, nwdt: Config.Graphics.ResX, nhgt: Config.Graphics.ResY - iBorderTop - iBorderBottom);
348
349 // Redraw flag
350 InvalidateBg();
351#ifdef _WIN32
352 // reset mouse clipping
353 ClipCursor(nullptr);
354#else
355 // StdWindow handles this.
356#endif
357 // reset GUI dlg pos
358 if (Game.pGUI)
359 Game.pGUI->SetPreferredDlgRect(C4Rect(ViewportArea.X, ViewportArea.Y, ViewportArea.Wdt, ViewportArea.Hgt));
360
361 // fullscreen background: First, cover all of screen
362 BackgroundAreas.Clear();
363 BackgroundAreas.AddRect(rNewRect: C4Rect(ViewportArea.X, ViewportArea.Y, ViewportArea.Wdt, ViewportArea.Hgt));
364
365 // Viewports
366 const int32_t iViews{GetViewportCount()};
367 if (!iViews) return;
368 int32_t iViewsH = static_cast<int32_t>(sqrt(x: float(iViews)));
369 int32_t iViewsX = iViews / iViewsH;
370 int32_t iViewsL = iViews % iViewsH;
371 int32_t cViewH, cViewX, ciViewsX;
372 int32_t cViewWdt, cViewHgt, cOffWdt, cOffHgt, cOffX, cOffY;
373 auto cvp = Viewports.begin();
374 for (cViewH = 0; cViewH < iViewsH; cViewH++)
375 {
376 ciViewsX = iViewsX; if (cViewH < iViewsL) ciViewsX++;
377 for (cViewX = 0; cViewX < ciViewsX; cViewX++)
378 {
379 cViewWdt = ViewportArea.Wdt / ciViewsX;
380 cViewHgt = ViewportArea.Hgt / iViewsH;
381 cOffX = ViewportArea.X;
382 cOffY = ViewportArea.Y;
383 cOffWdt = cOffHgt = 0;
384 int32_t ViewportScrollBorder = Application.isFullScreen ? C4ViewportScrollBorder : 0;
385 if (ciViewsX * std::min<int32_t>(a: cViewWdt, GBackWdt + 2 * ViewportScrollBorder) < ViewportArea.Wdt)
386 cOffX = (ViewportArea.Wdt - ciViewsX * std::min<int32_t>(a: cViewWdt, GBackWdt + 2 * ViewportScrollBorder)) / 2;
387 if (iViewsH * std::min<int32_t>(a: cViewHgt, GBackHgt + 2 * ViewportScrollBorder) < ViewportArea.Hgt)
388 cOffY = (ViewportArea.Hgt - iViewsH * std::min<int32_t>(a: cViewHgt, GBackHgt + 2 * ViewportScrollBorder)) / 2 + ViewportArea.Y;
389 if (Config.Graphics.SplitscreenDividers)
390 {
391 if (cViewX < ciViewsX - 1) cOffWdt = 4;
392 if (cViewH < iViewsH - 1) cOffHgt = 4;
393 }
394 int32_t coViewWdt = cViewWdt - cOffWdt; if (coViewWdt > GBackWdt + 2 * ViewportScrollBorder) { coViewWdt = GBackWdt + 2 * ViewportScrollBorder; }
395 int32_t coViewHgt = cViewHgt - cOffHgt; if (coViewHgt > GBackHgt + 2 * ViewportScrollBorder) { coViewHgt = GBackHgt + 2 * ViewportScrollBorder; }
396 C4Rect rcOut(cOffX + cViewX * cViewWdt, cOffY + cViewH * cViewHgt, coViewWdt, coViewHgt);
397 (*cvp)->SetOutputSize(iDrawX: rcOut.x, iDrawY: rcOut.y, iOutX: rcOut.x, iOutY: rcOut.y, iOutWdt: rcOut.Wdt, iOutHgt: rcOut.Hgt);
398 ++cvp;
399 // clip down area avaiable for background drawing
400 BackgroundAreas.ClipByRect(rClip: rcOut);
401 }
402 }
403}
404
405int32_t C4GraphicsSystem::GetViewportCount()
406{
407 return Viewports.size();
408}
409
410C4Viewport *C4GraphicsSystem::GetViewport(int32_t iPlayer)
411{
412 if (const auto cvp = std::find_if(first: Viewports.begin(), last: Viewports.end(), pred: [iPlayer](const auto &viewport)
413 {
414 return viewport->Player == iPlayer || (iPlayer == NO_OWNER && viewport->fIsNoOwnerViewport);
415 }); cvp != Viewports.end())
416 {
417 return cvp->get();
418 }
419 return nullptr;
420}
421
422int32_t LayoutOrder(int32_t iControl)
423{
424 // Convert keyboard control index to keyboard layout order
425 switch (iControl)
426 {
427 case C4P_Control_Keyboard1: return 0;
428 case C4P_Control_Keyboard2: return 3;
429 case C4P_Control_Keyboard3: return 1;
430 case C4P_Control_Keyboard4: return 2;
431 }
432 return iControl;
433}
434
435void C4GraphicsSystem::SortViewportsByPlayerControl()
436{
437 std::sort(first: Viewports.begin(), last: Viewports.end(), comp: [](const auto &a, const auto &b)
438 {
439 const auto plrA = Game.Players.Get(iPlayer: a->Player);
440 const auto plrB = Game.Players.Get(iPlayer: b->Player);
441 return plrA && plrB && LayoutOrder(plrA->Control) < LayoutOrder(plrB->Control);
442 });
443}
444
445void C4GraphicsSystem::MouseMove(int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyParam, class C4Viewport *pVP)
446{
447 const auto scale = Application.GetScale();
448 iX = static_cast<int32_t>(ceilf(x: iX / scale));
449 iY = static_cast<int32_t>(ceilf(x: iY / scale));
450 // pass on to GUI
451 // Special: Don't pass if dragging and button is not upped
452 if (Game.pGUI && Game.pGUI->IsActive() && !Game.MouseControl.IsDragging())
453 {
454 bool fResult = Game.pGUI->MouseInput(iButton, iX, iY, dwKeyParam, pDlg: nullptr, pVP);
455 if (Game.pGUI && Game.pGUI->HasMouseFocus()) { SetMouseInGUI(fInGUI: true, fByMouse: true); return; }
456 // non-exclusive GUI: inform mouse-control about GUI-result
457 SetMouseInGUI(fInGUI: fResult, fByMouse: true);
458 // abort if GUI processed it
459 if (fResult) return;
460 }
461 else
462 // no GUI: mouse is not in GUI
463 SetMouseInGUI(fInGUI: false, fByMouse: true);
464 // mouse control enabled?
465 if (!Game.MouseControl.IsActive())
466 {
467 // enable mouse in GUI, if a mouse-only-dlg is displayed
468 if (Game.pGUI && Game.pGUI->GetMouseControlledDialogCount())
469 SetMouseInGUI(fInGUI: true, fByMouse: true);
470 return;
471 }
472 // Pass on to mouse controlled viewport
473 MouseMoveToViewport(iButton, iX, iY, dwKeyParam);
474}
475
476void C4GraphicsSystem::MouseMoveToViewport(int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyParam)
477{
478 // Pass on to mouse controlled viewport
479 for (const auto &cvp : Viewports)
480 if (Game.MouseControl.IsViewport(pViewport: cvp.get()))
481 Game.MouseControl.Move(iButton,
482 iX: BoundBy<int32_t>(bval: iX - cvp->OutX, lbound: 0, rbound: cvp->ViewWdt - 1),
483 iY: BoundBy<int32_t>(bval: iY - cvp->OutY, lbound: 0, rbound: cvp->ViewHgt - 1),
484 dwKeyFlags: dwKeyParam);
485}
486
487void C4GraphicsSystem::SetMouseInGUI(bool fInGUI, bool fByMouse)
488{
489 // inform mouse control and GUI
490 if (Game.pGUI)
491 {
492 Game.pGUI->Mouse.SetOwnedMouse(fInGUI);
493 // initial movement to ensure mouse control pos is correct
494 if (!Game.MouseControl.IsMouseOwned() && !fInGUI && !fByMouse)
495 {
496 Game.MouseControl.SetOwnedMouse(true);
497 MouseMoveToViewport(iButton: C4MC_Button_None, iX: Game.pGUI->Mouse.x, iY: Game.pGUI->Mouse.y, dwKeyParam: Game.pGUI->Mouse.dwKeys);
498 }
499 }
500 Game.MouseControl.SetOwnedMouse(!fInGUI);
501}
502
503bool C4GraphicsSystem::SaveScreenshot(bool fSaveAll)
504{
505 // Filename
506 char szFilename[_MAX_PATH + 1];
507 int32_t iScreenshotIndex = 1;
508 const char *strFilePath = nullptr;
509 do
510 FormatWithNull(buf&: szFilename, fmt: "Screenshot{:03}.png", args: iScreenshotIndex++);
511 while (FileExists(szFileName: strFilePath = Config.AtScreenshotPath(szFilename)));
512 bool fSuccess = DoSaveScreenshot(fSaveAll, szFilename: strFilePath);
513
514 // log if successful/where it has been stored
515 if (fSuccess)
516 {
517 Log(id: C4ResStrTableKey::IDS_PRC_SCREENSHOT, args: Config.AtExeRelativePath(szFilename: Config.AtScreenshotPath(szFilename)));
518 }
519 else
520 {
521 Log(id: C4ResStrTableKey::IDS_PRC_SCREENSHOTERROR, args: Config.AtExeRelativePath(szFilename: Config.AtScreenshotPath(szFilename)));
522 }
523
524 // return success
525 return !!fSuccess;
526}
527
528bool C4GraphicsSystem::DoSaveScreenshot(bool fSaveAll, const char *szFilename)
529{
530 // Fullscreen only
531 if (!Application.isFullScreen) return false;
532 // back surface must be present
533 if (!Application.DDraw->lpBack) return false;
534
535 const auto scale = Application.GetScale();
536
537 // save landscape
538 if (fSaveAll)
539 {
540 if (Viewports.empty()) return false;
541 // get viewport to draw in
542 const auto &pVP = Viewports.front();
543 // create image large enough to hold the landcape
544 int32_t lWdt = static_cast<int32_t>(ceilf(GBackWdt * scale)), lHgt = static_cast<int32_t>(ceilf(GBackHgt * scale));
545 StdBitmap bmp(lWdt, lHgt, false);
546 // get backbuffer size
547 int32_t bkWdt = static_cast<int32_t>(ceilf(x: Config.Graphics.ResX * scale)), bkHgt = static_cast<int32_t>(ceilf(x: Config.Graphics.ResY * scale));
548 if (!bkWdt || !bkHgt) return false;
549 // facet for blitting
550 C4FacetEx bkFct;
551 // mark background to be redrawn
552 InvalidateBg();
553 // backup and clear sky parallaxity
554 int32_t iParX = Game.Landscape.Sky.ParX; Game.Landscape.Sky.ParX = 10;
555 int32_t iParY = Game.Landscape.Sky.ParY; Game.Landscape.Sky.ParY = 10;
556 // temporarily change viewport player
557 int32_t iVpPlr = pVP->Player; pVP->Player = NO_OWNER;
558 // blit all tiles needed
559 for (int32_t iY = 0, iRealY = 0; iY < lHgt; iY += Config.Graphics.ResY, iRealY += bkHgt) for (int32_t iX = 0, iRealX = 0; iX < lWdt; iX += Config.Graphics.ResX, iRealX += bkWdt)
560 {
561 // get max width/height
562 int32_t bkWdt2 = bkWdt, bkHgt2 = bkHgt;
563 if (iRealX + bkWdt2 > lWdt) bkWdt2 -= iRealX + bkWdt2 - lWdt;
564 if (iRealY + bkHgt2 > lHgt) bkHgt2 -= iRealY + bkHgt2 - lHgt;
565 // update facet
566 bkFct.Set(nsfc: Application.DDraw->lpBack, nx: 0, ny: 0, nwdt: static_cast<int32_t>(ceilf(x: bkWdt2 / scale)), nhgt: static_cast<int32_t>(ceilf(x: bkHgt2 / scale)), ntx: iX, nty: iY);
567 // draw there
568 pVP->Draw(cgo&: bkFct, fDrawOverlay: false);
569 // render
570 Application.DDraw->PageFlip(); Application.DDraw->PageFlip();
571 // get output (locking primary!)
572 if (Application.DDraw->lpBack->Lock())
573 {
574 // transfer each pixel - slooow...
575 for (int32_t iY2 = 0; iY2 < bkHgt2; ++iY2)
576 for (int32_t iX2 = 0; iX2 < bkWdt2; ++iX2)
577 {
578 const std::uint32_t pixel{Application.DDraw->lpBack->GetPixDw(iX: iX2, iY: iY2, fApplyModulation: false, scale)};
579 bmp.SetPixel24(x: iRealX + iX2, y: iRealY + iY2, value: Config.Graphics.Shader ? pixel : Application.DDraw->ApplyGammaTo(dwClr: pixel));
580 }
581 // done; unlock
582 Application.DDraw->lpBack->Unlock();
583 }
584 }
585 // restore viewport player
586 pVP->Player = iVpPlr;
587 // restore parallaxity
588 Game.Landscape.Sky.ParX = iParX;
589 Game.Landscape.Sky.ParY = iParY;
590 // Save bitmap to PNG file
591 try
592 {
593 CPNGFile(szFilename, lWdt, lHgt, false).Encode(pixels: bmp.GetBytes());
594 }
595 catch (const std::runtime_error &e)
596 {
597 LogNTr(level: spdlog::level::err, fmt: "Could not write screenshot to PNG file: {}", args: e.what());
598 return false;
599 }
600 return true;
601 }
602 // Save primary surface
603 return Application.DDraw->lpBack->SavePNG(szFilename, fSaveAlpha: false, fApplyGamma: !Config.Graphics.Shader, fSaveOverlayOnly: false, scale);
604}
605
606void C4GraphicsSystem::DeactivateDebugOutput()
607{
608 ShowVertices = false;
609 ShowAction = false;
610 ShowCommand = false;
611 ShowEntrance = false;
612 ShowPathfinder = false; // allow pathfinder! - why this??
613 ShowSolidMask = false;
614 ShowNetstatus = false;
615}
616
617void C4GraphicsSystem::DrawHoldMessages()
618{
619 if (Application.isFullScreen)
620 {
621 if (Game.HaltCount)
622 {
623 Application.DDraw->TextOut(szText: "Pause", rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: Application.DDraw->lpBack, iTx: Config.Graphics.ResX / 2, iTy: Config.Graphics.ResY / 2 - Game.GraphicsResource.FontRegular.GetLineHeight() * 2, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ACenter);
624 Game.GraphicsSystem.OverwriteBg();
625 }
626 }
627}
628
629void C4GraphicsSystem::FlashMessage(const char *szMessage)
630{
631 // Store message
632 SCopy(szSource: szMessage, sTarget: FlashMessageText, iMaxL: C4MaxTitle);
633 // Calculate message time
634 FlashMessageTime = SLen(sptr: FlashMessageText) * 2;
635 // Initial position
636 FlashMessageX = -1;
637 FlashMessageY = 10;
638 // Upper board active: stay below upper board
639 FlashMessageY += C4UpperBoard::Height();
640 // More than one viewport: try to stay below portraits etc.
641 if (GetViewportCount() > 1)
642 FlashMessageY += 64;
643 // New flash message: redraw background (might be drawing one message on top of another)
644 InvalidateBg();
645}
646
647void C4GraphicsSystem::FlashMessageOnOff(const char *strWhat, bool fOn)
648{
649 FlashMessage(szMessage: std::format(fmt: "{}: {}", args&: strWhat, args: LoadResStrChoice(condition: fOn, ifTrue: C4ResStrTableKey::IDS_CTL_ON, ifFalse: C4ResStrTableKey::IDS_CTL_OFF)).c_str());
650}
651
652void C4GraphicsSystem::DrawFlashMessage()
653{
654 if (!FlashMessageTime) return;
655 if (!Application.isFullScreen) return;
656 Application.DDraw->TextOut(szText: FlashMessageText, rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: Application.DDraw->lpBack,
657 iTx: (FlashMessageX == -1) ? Config.Graphics.ResX / 2 : FlashMessageX,
658 iTy: (FlashMessageY == -1) ? Config.Graphics.ResY / 2 : FlashMessageY,
659 dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR,
660 byForm: (FlashMessageX == -1) ? ACenter : ALeft);
661 FlashMessageTime--;
662 // Flash message timed out: redraw background
663 if (!FlashMessageTime) InvalidateBg();
664}
665
666void C4GraphicsSystem::DrawHelp()
667{
668 if (!ShowHelp) return;
669 if (!Application.isFullScreen) return;
670 int32_t iX = ViewportArea.X, iY = ViewportArea.Y;
671 int32_t iWdt = ViewportArea.Wdt;
672
673 std::string text{std::format(
674 // left coloumn
675 fmt: "[{}]\n\n",
676 // main functions
677 args: "<c ffff00>{}</c> - {}\n"
678 "<c ffff00>{}</c> - {}\n"
679 "<c ffff00>{}</c> - {}\n"
680 "<c ffff00>{}</c> - {}\n"
681 // messages
682 "\n<c ffff00>{}/{}</c> - {}\n"
683 "<c ffff00>{}</c> - {}\n"
684 "<c ffff00>{}</c> - {}\n"
685 // irc chat
686 "\n<c ffff00>{}</c> - {}\n"
687 // scoreboard
688 "\n<c ffff00>{}</c> - {}\n"
689 // screenshots
690 "\n<c ffff00>{}</c> - {}\n"
691 "<c ffff00>{}</c> - {}\n",
692
693 args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_GAMEFUNCTIONS),
694 args: GetKeyboardInputName(szKeyName: "ToggleShowHelp"), args: LoadResStr(id: C4ResStrTableKey::IDS_CON_HELP),
695 args: GetKeyboardInputName(szKeyName: "MusicToggle"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_MUSIC),
696 args: GetKeyboardInputName(szKeyName: "SoundToggle"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_SOUND),
697 args: GetKeyboardInputName(szKeyName: "NetClientListDlgToggle"), args: LoadResStr(id: C4ResStrTableKey::IDS_DLG_NETWORK),
698 args: GetKeyboardInputName(szKeyName: "ChatOpen", fShort: false, iIndex: 1), args: GetKeyboardInputName(szKeyName: "ChatOpen", fShort: false, iIndex: 0), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_SENDMESSAGE),
699 args: GetKeyboardInputName(szKeyName: "MsgBoardScrollUp"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_MESSAGEBOARDBACK),
700 args: GetKeyboardInputName(szKeyName: "MsgBoardScrollDown"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_MESSAGEBOARDFORWARD),
701 args: GetKeyboardInputName(szKeyName: "ToggleChat"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_IRCCHAT),
702 args: GetKeyboardInputName(szKeyName: "ScoreboardToggle"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_SCOREBOARD),
703 args: GetKeyboardInputName(szKeyName: "Screenshot"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_SCREENSHOT),
704 args: GetKeyboardInputName(szKeyName: "ScreenshotEx"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_SCREENSHOTEX)
705 )};
706
707 Application.DDraw->TextOut(szText: text.c_str(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: Application.DDraw->lpBack,
708 iTx: iX + 128, iTy: iY + 64, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ALeft);
709
710 text = std::format(
711 // right coloumn
712 // game speed
713 fmt: "\n\n<c ffff00>{}</c> - {}\n"
714 "<c ffff00>{}</c> - {}\n"
715 // debug
716 "\n\n[Debug]\n\n"
717 "<c ffff00>{}</c> - {}\n"
718 "<c ffff00>{}</c> - {}\n"
719 "<c ffff00>{}</c> - {}\n"
720 "<c ffff00>{}</c> - {}\n",
721
722 args: GetKeyboardInputName(szKeyName: "GameSpeedUp"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_GAMESPEEDUP),
723 args: GetKeyboardInputName(szKeyName: "GameSpeedDown"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_GAMESPEEDDOWN),
724 args: GetKeyboardInputName(szKeyName: "DbgModeToggle"), args: LoadResStr(id: C4ResStrTableKey::IDS_CTL_DEBUGMODE),
725 args: GetKeyboardInputName(szKeyName: "DbgShowVtxToggle"), args: "Entrance+Vertices",
726 args: GetKeyboardInputName(szKeyName: "DbgShowActionToggle"), args: "Actions/Commands/Pathfinder",
727 args: GetKeyboardInputName(szKeyName: "DbgShowSolidMaskToggle"), args: "SolidMasks"
728 );
729
730 Application.DDraw->TextOut(szText: text.c_str(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: Application.DDraw->lpBack,
731 iTx: iX + iWdt / 2 + 64, iTy: iY + 64, dwFCol: CStdDDraw::DEFAULT_MESSAGE_COLOR, byForm: ALeft);
732}
733
734int32_t C4GraphicsSystem::GetAudibility(int32_t iX, int32_t iY, int32_t *iPan, int32_t iAudibilityRadius)
735{
736 // default audibility radius
737 if (!iAudibilityRadius) iAudibilityRadius = C4SoundSystem::AudibilityRadius;
738 // Accumulate audibility by viewports
739 int32_t iAudible = 0; *iPan = 0;
740 for (const auto &cvp : Viewports)
741 {
742 auto listenerX = cvp->ViewX + cvp->ViewWdt / 2;
743 auto listenerY = cvp->ViewY + cvp->ViewHgt / 2;
744
745 const auto player = Game.Players.Get(iPlayer: cvp->GetPlayer());
746 if (player)
747 {
748 auto cursor = player->ViewCursor;
749 if (!cursor)
750 {
751 cursor = player->ViewTarget;
752 if (!cursor)
753 {
754 cursor = player->Cursor;
755 }
756 }
757 if (cursor)
758 {
759 listenerX = cursor->x;
760 listenerY = cursor->y;
761 }
762 }
763
764 iAudible = (std::max)(a: iAudible,
765 b: BoundBy<int32_t>(bval: 100 - 100 * Distance(iX1: listenerX, iY1: listenerY, iX2: iX, iY2: iY) / C4SoundSystem::AudibilityRadius, lbound: 0, rbound: 100));
766 *iPan += (iX - (cvp->ViewX + cvp->ViewWdt / 2)) / 5;
767 }
768 *iPan = BoundBy<int32_t>(bval: *iPan, lbound: -100, rbound: 100);
769 return iAudible;
770}
771
772void C4GraphicsSystem::SetGamma(uint32_t dwClr1, uint32_t dwClr2, uint32_t dwClr3, int32_t iRampIndex)
773{
774 // No gamma effects
775 if (Config.Graphics.DisableGamma) return;
776 if (iRampIndex < 0 || iRampIndex >= C4MaxGammaRamps) return;
777 // turn ramp index into array offset
778 iRampIndex *= 3;
779 // set array members
780 dwGamma[iRampIndex + 0] = dwClr1;
781 dwGamma[iRampIndex + 1] = dwClr2;
782 dwGamma[iRampIndex + 2] = dwClr3;
783 // mark gamma ramp to be recalculated
784 fSetGamma = true;
785}
786
787void C4GraphicsSystem::ApplyGamma()
788{
789 // No gamma effects
790 if (Config.Graphics.DisableGamma) return;
791 // calculate color channels by adding the difference between the gamma ramps to their normals
792 int32_t ChanOff[3];
793 uint32_t Gamma[3];
794 const int32_t DefChanVal[3] = { 0x00, 0x80, 0xff };
795 // calc offset for curve points
796 for (int32_t iCurve = 0; iCurve < 3; ++iCurve)
797 {
798 std::fill(first: ChanOff, last: std::end(arr&: ChanOff), value: 0);
799 // ...channels...
800 for (int32_t iChan = 0; iChan < 3; ++iChan)
801 // ...ramps...
802 for (int32_t iRamp = 0; iRamp < C4MaxGammaRamps; ++iRamp)
803 // add offset
804 ChanOff[iChan] += uint8_t(dwGamma[iRamp * 3 + iCurve] >> (16 - iChan * 8)) - DefChanVal[iCurve];
805 // calc curve point
806 Gamma[iCurve] = C4RGB(BoundBy<int32_t>(DefChanVal[iCurve] + ChanOff[0], 0, 255), BoundBy<int32_t>(DefChanVal[iCurve] + ChanOff[1], 0, 255), BoundBy<int32_t>(DefChanVal[iCurve] + ChanOff[2], 0, 255));
807 }
808 // set gamma
809 Application.DDraw->SetGamma(dwClr1: Gamma[0], dwClr2: Gamma[1], dwClr3: Gamma[2]);
810}
811
812bool C4GraphicsSystem::ToggleShowNetStatus()
813{
814 ShowNetstatus = !ShowNetstatus;
815 return true;
816}
817
818bool C4GraphicsSystem::ToggleShowVertices()
819{
820 if (!Game.DebugMode && !Console.Active) { FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_NODEBUGMODE)); return false; }
821 Toggle(v&: ShowVertices);
822 Toggle(v&: ShowEntrance); // vertices and entrance now toggled together
823 FlashMessageOnOff(strWhat: "Entrance+Vertices", fOn: ShowVertices || ShowEntrance);
824 return true;
825}
826
827bool C4GraphicsSystem::ToggleShowAction()
828{
829 if (!Game.DebugMode && !Console.Active) { FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_NODEBUGMODE)); return false; }
830 if (!(ShowAction || ShowCommand || ShowPathfinder))
831 {
832 ShowAction = true; FlashMessage(szMessage: "Actions");
833 }
834 else if (ShowAction)
835 {
836 ShowAction = false; ShowCommand = true; FlashMessage(szMessage: "Commands");
837 }
838 else if (ShowCommand)
839 {
840 ShowCommand = false; ShowPathfinder = true; FlashMessage(szMessage: "Pathfinder");
841 }
842 else if (ShowPathfinder)
843 {
844 ShowPathfinder = false; FlashMessageOnOff(strWhat: "Actions/Commands/Pathfinder", fOn: false);
845 }
846 return true;
847}
848
849bool C4GraphicsSystem::ToggleShowSolidMask()
850{
851 if (!Game.DebugMode && !Console.Active) { FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_NODEBUGMODE)); return false; }
852 Toggle(v&: ShowSolidMask);
853 FlashMessageOnOff(strWhat: "SolidMasks", fOn: !!ShowSolidMask);
854 return true;
855}
856
857bool C4GraphicsSystem::ToggleShowHelp()
858{
859 Toggle(v&: ShowHelp);
860 // Turned off? Invalidate background.
861 if (!ShowHelp) InvalidateBg();
862 return true;
863}
864
865bool C4GraphicsSystem::ViewportNextPlayer()
866{
867 // safety: switch valid?
868 if ((!Game.C4S.Head.Film || !Game.C4S.Head.Replay) && !Game.GraphicsSystem.GetViewport(iPlayer: NO_OWNER)) return false;
869 // do switch then
870 if (Viewports.empty()) return false;
871 Viewports.front()->NextPlayer();
872 return true;
873}
874
875bool C4GraphicsSystem::FreeScroll(C4Vec2D vScrollBy)
876{
877 // safety: move valid?
878 if ((!Game.C4S.Head.Replay || !Game.C4S.Head.Film) && !Game.GraphicsSystem.GetViewport(iPlayer: NO_OWNER)) return false;
879 if (Viewports.empty()) return false;
880 const auto &vp = Viewports.front();
881 // move then (old static code crap...)
882 static int32_t vp_vx = 0; static int32_t vp_vy = 0;
883 int32_t dx = vScrollBy.x; int32_t dy = vScrollBy.y;
884 if (!MostRecentScrolling.Elapsed())
885 {
886 dx += vp_vx; dy += vp_vy;
887 }
888 vp_vx = dx; vp_vy = dy;
889 vp->ViewX += dx; vp->ViewY += dy;
890 MostRecentScrolling.Reset();
891 return true;
892}
893