1 /*
2 * OpenClonk, http://www.openclonk.org
3 *
4 * Copyright (c) 1998-2000, Matthes Bender
5 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
6 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18 /* Operates viewports, message board and draws the game */
19
20 #include "C4Include.h"
21 #include "C4ForbidLibraryCompilation.h"
22 #include "game/C4GraphicsSystem.h"
23
24 #include "editor/C4Console.h"
25 #include "game/C4Application.h"
26 #include "game/C4FullScreen.h"
27 #include "game/C4Viewport.h"
28 #include "graphics/C4Draw.h"
29 #include "graphics/C4GraphicsResource.h"
30 #include "graphics/StdPNG.h"
31 #include "gui/C4Gui.h"
32 #include "gui/C4LoaderScreen.h"
33 #include "landscape/C4Landscape.h"
34 #include "landscape/C4Sky.h"
35 #include "network/C4Network2.h"
36 #include "object/C4GameObjects.h"
37
38 static const int MAX_BACKGROUND_FPS = 5;
39
C4GraphicsSystem()40 C4GraphicsSystem::C4GraphicsSystem()
41 {
42 Default();
43 }
44
~C4GraphicsSystem()45 C4GraphicsSystem::~C4GraphicsSystem()
46 {
47 Clear();
48 }
49
Init()50 bool C4GraphicsSystem::Init()
51 {
52 // Success
53 return true;
54 }
55
Clear()56 void C4GraphicsSystem::Clear()
57 {
58 // Clear message board
59 MessageBoard.reset();
60 // clear loader
61 if (pLoaderScreen) { delete pLoaderScreen; pLoaderScreen=nullptr; }
62 // Close viewports
63 ::Viewports.Clear();
64 // No debug stuff
65 DeactivateDebugOutput();
66 }
67
StartDrawing()68 bool C4GraphicsSystem::StartDrawing()
69 {
70 // only if ddraw is ready
71 if (!pDraw) return false;
72 if (!pDraw->Active) return false;
73
74 // if the window is not focused, draw no more than MAX_BACKGROUND_FPS frames per second
75 if (!Application.Active && (C4TimeMilliseconds::Now() - lastFrame) < 1000 / MAX_BACKGROUND_FPS)
76 return false;
77
78 // drawing OK
79 return true;
80 }
81
FinishDrawing()82 void C4GraphicsSystem::FinishDrawing()
83 {
84 if (!Application.isEditor) FullScreen.pSurface->PageFlip();
85 lastFrame = C4TimeMilliseconds::Now();
86 }
87
Execute()88 void C4GraphicsSystem::Execute()
89 {
90 // activity check
91 if (!StartDrawing()) return;
92
93 bool fBGDrawn = false;
94
95 // If lobby running, message board only (page flip done by startup message board)
96 if (!::pGUI->HasFullscreenDialog(true)) // allow for message board behind GUI
97 if (::Network.isLobbyActive() || !Game.IsRunning)
98 if (!Application.isEditor)
99 {
100 // Message board
101 if (iRedrawBackground) ClearFullscreenBackground();
102 MessageBoard->Execute();
103 if (!C4GUI::IsActive())
104 { FinishDrawing(); return; }
105 fBGDrawn = true;
106 }
107
108 // fullscreen GUI?
109 if (!Application.isEditor && C4GUI::IsActive() && (::pGUI->HasFullscreenDialog(false) || !Game.IsRunning))
110 {
111 if (!fBGDrawn && iRedrawBackground) ClearFullscreenBackground();
112 ::pGUI->Render(!fBGDrawn);
113 FinishDrawing();
114 return;
115 }
116
117
118 // Reset object audibility
119 ::Objects.ResetAudibility();
120
121 // some hack to ensure the mouse is drawn after a dialog close and before any
122 // movement messages
123 if (!C4GUI::IsActive())
124 ::pGUI->SetMouseInGUI(false, false);
125
126 // Viewports
127 ::Viewports.Execute(!Application.isEditor && iRedrawBackground);
128 if (iRedrawBackground) --iRedrawBackground;
129
130 if (!Application.isEditor)
131 {
132 // Upper board
133 UpperBoard.Execute();
134
135 // Message board
136 MessageBoard->Execute();
137
138 // Help & Messages
139 DrawHelp();
140 DrawHoldMessages();
141 DrawFlashMessage();
142 }
143
144 // InGame-GUI
145 if (C4GUI::IsActive())
146 {
147 ::pGUI->Render(false);
148 }
149
150 // done
151 FinishDrawing();
152 }
153
Default()154 void C4GraphicsSystem::Default()
155 {
156 MessageBoard = std::make_unique<C4MessageBoard>();
157 InvalidateBg();
158 ShowVertices=false;
159 ShowAction=false;
160 ShowCommand=false;
161 ShowEntrance=false;
162 ShowPathfinder=false;
163 ShowNetstatus=false;
164 Show8BitSurface=0;
165 ShowLights=false;
166 ShowMenuInfo=false;
167 ShowHelp=false;
168 FlashMessageText[0]=0;
169 FlashMessageTime=0; FlashMessageX=FlashMessageY=0;
170 pLoaderScreen=nullptr;
171 }
172
ClearFullscreenBackground()173 void C4GraphicsSystem::ClearFullscreenBackground()
174 {
175 pDraw->FillBG(0);
176 --iRedrawBackground;
177 }
178
InitLoaderScreen(const char * szLoaderSpec)179 bool C4GraphicsSystem::InitLoaderScreen(const char *szLoaderSpec)
180 {
181 // create new loader; overwrite current only if successful
182 C4LoaderScreen *pNewLoader = new C4LoaderScreen();
183 pNewLoader->SetBlackScreen(false);
184 if (!pNewLoader->Init(szLoaderSpec)) { delete pNewLoader; return false; }
185 if (pLoaderScreen) delete pLoaderScreen;
186 pLoaderScreen = pNewLoader;
187 // done, success
188 return true;
189 }
190
EnableLoaderDrawing()191 void C4GraphicsSystem::EnableLoaderDrawing()
192 {
193 // reset black screen loader flag
194 if (pLoaderScreen) pLoaderScreen->SetBlackScreen(false);
195 }
196
SaveScreenshot(bool fSaveAll,float fSaveAllZoom)197 bool C4GraphicsSystem::SaveScreenshot(bool fSaveAll, float fSaveAllZoom)
198 {
199 // Find a unique screenshot filename by iterating over all possible names
200 // Keep static counter so multiple screenshots in succession do not use same filename even if the background thread hasn't started writing the file yet
201 char szFilename[_MAX_PATH+1];
202 static int32_t iScreenshotIndex=1;
203 const char *strFilePath = nullptr;
204 do
205 sprintf(szFilename,"Screenshot%03i.png",iScreenshotIndex++);
206 while (FileExists(strFilePath = Config.AtScreenshotPath(szFilename)));
207 bool fSuccess=DoSaveScreenshot(fSaveAll, strFilePath, fSaveAllZoom);
208 // log if successful/where it has been stored
209 if (!fSuccess)
210 LogF(LoadResStr("IDS_PRC_SCREENSHOTERROR"), Config.AtUserDataRelativePath(Config.AtScreenshotPath(szFilename)));
211 else
212 LogF(LoadResStr("IDS_PRC_SCREENSHOT"), Config.AtUserDataRelativePath(Config.AtScreenshotPath(szFilename)));
213 // return success
214 return !!fSuccess;
215 }
216
DoSaveScreenshot(bool fSaveAll,const char * szFilename,float fSaveAllZoom)217 bool C4GraphicsSystem::DoSaveScreenshot(bool fSaveAll, const char *szFilename, float fSaveAllZoom)
218 {
219 // Fullscreen only
220 if (Application.isEditor)
221 {
222 Log(LoadResStr("IDS_PRC_SCREENSHOTERROREDITOR"));
223 return false;
224 }
225 // back surface must be present
226 if (!FullScreen.pSurface) return false;
227
228 // save landscape
229 if (fSaveAll)
230 {
231 // Create full map screenshots at zoom 2x. Fractional zooms (like 1.7x) should work but might cause some trouble at screen borders.
232 float zoom = fSaveAllZoom;
233 // get viewport to draw in
234 C4Viewport *pVP=::Viewports.GetFirstViewport(); if (!pVP) return false;
235 // create image large enough to hold the landscape
236 std::unique_ptr<CPNGFile> png(new CPNGFile());
237 int32_t lWdt = ::Landscape.GetWidth() * zoom, lHgt = ::Landscape.GetHeight() * zoom;
238 if (!png->Create(lWdt, lHgt, false)) return false;
239 // get backbuffer size
240 int32_t bkWdt=C4GUI::GetScreenWdt(), bkHgt=C4GUI::GetScreenHgt();
241 if (!bkWdt || !bkHgt) return false;
242 // facet for blitting
243 C4TargetFacet bkFct;
244 // mark background to be redrawn
245 InvalidateBg();
246 // draw on one big viewport
247 pVP->SetOutputSize(0,0,0,0, bkWdt, bkHgt);
248 // backup and clear sky parallaxity
249 int32_t iParX=::Landscape.GetSky().ParX; ::Landscape.GetSky().ParX=10;
250 int32_t iParY=::Landscape.GetSky().ParY; ::Landscape.GetSky().ParY=10;
251 // backup and clear viewport borders
252 FLOAT_RECT vp_borders = { pVP->BorderLeft, pVP->BorderRight, pVP->BorderTop, pVP->BorderBottom };
253 pVP->BorderLeft = pVP->BorderRight = pVP->BorderTop = pVP->BorderBottom = 0.0f;
254 // temporarily change viewport player
255 int32_t iVpPlr=pVP->Player; pVP->Player=NO_OWNER;
256 // blit all tiles needed
257 for (int32_t iY=0; iY<lHgt; iY+=bkHgt) for (int32_t iX=0; iX<lWdt; iX+=bkWdt)
258 {
259 // get max width/height
260 int32_t bkWdt2=bkWdt,bkHgt2=bkHgt;
261 if (iX+bkWdt2>lWdt) bkWdt2-=iX+bkWdt2-lWdt;
262 if (iY+bkHgt2>lHgt) bkHgt2-=iY+bkHgt2-lHgt;
263 // update facet
264 bkFct.Set(FullScreen.pSurface, 0, 0, ceil(float(bkWdt2)/zoom), ceil(float(bkHgt2)/zoom), iX/zoom, iY/zoom, zoom, 0, 0);
265 // draw there
266 pVP->Draw(bkFct, true, false);
267 // render
268 FullScreen.pSurface->PageFlip(); FullScreen.pSurface->PageFlip();
269 // get output (locking primary!)
270 if (FullScreen.pSurface->Lock())
271 {
272 // transfer each pixel - slooow...
273 for (int32_t iY2 = 0; iY2 < bkHgt2; ++iY2)
274 #ifndef USE_CONSOLE
275 glReadPixels(0, FullScreen.pSurface->Hgt - iY2 - 1, bkWdt2, 1, GL_BGR, GL_UNSIGNED_BYTE, reinterpret_cast<BYTE *>(png->GetRow(iY + iY2)) + iX * 3);
276 #else
277 for (int32_t iX2=0; iX2<bkWdt2; ++iX2)
278 png->SetPix(iX+iX2, iY+iY2, FullScreen.pSurface->GetPixDw(iX2, iY2, false));
279 #endif
280 // done; unlock
281 FullScreen.pSurface->Unlock();
282 // This can take a long time and we would like to pump messages
283 // However, we're currently hogging the primary surface and horrible things might happen if we do that, including initiation of another screenshot
284 // The only thing that can be safely run is music (sound could play but that would just make them out of sync of the game)
285 ::Application.MusicSystem.Execute(true);
286 }
287 }
288 // restore viewport player
289 pVP->Player=iVpPlr;
290 // restore viewport borders
291 pVP->BorderLeft = vp_borders.left;
292 pVP->BorderTop = vp_borders.top;
293 pVP->BorderRight = vp_borders.right;
294 pVP->BorderBottom = vp_borders.bottom;
295 // restore parallaxity
296 ::Landscape.GetSky().ParX=iParX;
297 ::Landscape.GetSky().ParY=iParY;
298 // restore viewport size
299 ::Viewports.RecalculateViewports();
300 // save!
301 CPNGFile::ScheduleSaving(png.release(), szFilename);
302 return true;
303 }
304 // Save primary surface in background thread
305 return FullScreen.pSurface->SavePNG(szFilename, false, false, true);
306 }
307
DeactivateDebugOutput()308 void C4GraphicsSystem::DeactivateDebugOutput()
309 {
310 ShowVertices=false;
311 ShowAction=false;
312 ShowCommand=false;
313 ShowEntrance=false;
314 ShowPathfinder=false; // allow pathfinder! - why this??
315 ShowLights=false;
316 Show8BitSurface=0;
317 ShowNetstatus=false;
318 ShowMenuInfo=false;
319 }
320
DrawHoldMessages()321 void C4GraphicsSystem::DrawHoldMessages()
322 {
323 if (!Application.isEditor && Game.HaltCount)
324 {
325 pDraw->TextOut("Pause", ::GraphicsResource.FontRegular,1.0,
326 FullScreen.pSurface, C4GUI::GetScreenWdt()/2,
327 C4GUI::GetScreenHgt()/2 - ::GraphicsResource.FontRegular.GetLineHeight()*2,
328 C4Draw::DEFAULT_MESSAGE_COLOR, ACenter);
329 ::GraphicsSystem.OverwriteBg();
330 }
331 }
332
FlashMessage(const char * szMessage)333 void C4GraphicsSystem::FlashMessage(const char *szMessage)
334 {
335 // Store message
336 SCopy(szMessage, FlashMessageText, C4MaxTitle);
337 // Calculate message time
338 FlashMessageTime = SLen(FlashMessageText) * 2;
339 // Initial position
340 FlashMessageX = -1;
341 FlashMessageY = 10;
342 // Upper board active: stay below upper board
343 if (Config.Graphics.UpperBoard)
344 FlashMessageY += C4UpperBoardHeight;
345 // More than one viewport: try to stay below portraits etc.
346 if (::Viewports.GetViewportCount() > 1)
347 FlashMessageY += 64;
348 // New flash message: redraw background (might be drawing one message on top of another)
349 InvalidateBg();
350 }
351
FlashMessageOnOff(const char * strWhat,bool fOn)352 void C4GraphicsSystem::FlashMessageOnOff(const char *strWhat, bool fOn)
353 {
354 StdStrBuf strMessage;
355 strMessage.Format("%s: %s", strWhat, LoadResStr(fOn ? "IDS_CTL_ON" : "IDS_CTL_OFF"));
356 FlashMessage(strMessage.getData());
357 }
358
DrawFlashMessage()359 void C4GraphicsSystem::DrawFlashMessage()
360 {
361 if (!FlashMessageTime) return;
362 if (Application.isEditor) return;
363 pDraw->TextOut(FlashMessageText, ::GraphicsResource.FontRegular, 1.0, FullScreen.pSurface,
364 (FlashMessageX==-1) ? C4GUI::GetScreenWdt()/2 : FlashMessageX,
365 (FlashMessageY==-1) ? C4GUI::GetScreenHgt()/2 : FlashMessageY,
366 C4Draw::DEFAULT_MESSAGE_COLOR,
367 (FlashMessageX==-1) ? ACenter : ALeft);
368 FlashMessageTime--;
369 // Flash message timed out: redraw background
370 if (!FlashMessageTime) InvalidateBg();
371 }
372
DrawHelp()373 void C4GraphicsSystem::DrawHelp()
374 {
375 if (!ShowHelp) return;
376 if (Application.isEditor) return;
377 int32_t iX = ::Viewports.ViewportArea.X, iY = ::Viewports.ViewportArea.Y;
378 int32_t iWdt = ::Viewports.ViewportArea.Wdt;
379 StdStrBuf strText;
380 // left coloumn
381 strText.AppendFormat("[%s]\n\n", LoadResStr("IDS_CTL_GAMEFUNCTIONS"));
382 // main functions
383 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ToggleShowHelp").getData(), LoadResStr("IDS_CON_HELP"));
384 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("MusicToggle").getData(), LoadResStr("IDS_CTL_MUSIC"));
385 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("NetClientListDlgToggle").getData(), LoadResStr("IDS_DLG_NETWORK"));
386 // messages
387 StdCopyStrBuf strAltChatKey(GetKeyboardInputName("ChatOpen", false, 0));
388 strText.AppendFormat("\n<c ffff00>%s/%s</c> - %s\n", GetKeyboardInputName("ChatOpen", false, 1).getData(), strAltChatKey.getData(), LoadResStr("IDS_CTL_SENDMESSAGE"));
389 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("MsgBoardScrollUp").getData(), LoadResStr("IDS_CTL_MESSAGEBOARDBACK"));
390 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("MsgBoardScrollDown").getData(), LoadResStr("IDS_CTL_MESSAGEBOARDFORWARD"));
391 // irc chat
392 strText.AppendFormat("\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ToggleChat").getData(), LoadResStr("IDS_CTL_IRCCHAT"));
393 // scoreboard
394 strText.AppendFormat("\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ScoreboardToggle").getData(), LoadResStr("IDS_CTL_SCOREBOARD"));
395 // screenshots
396 strText.AppendFormat("\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("Screenshot").getData(), LoadResStr("IDS_CTL_SCREENSHOT"));
397 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("ScreenshotEx").getData(), LoadResStr("IDS_CTL_SCREENSHOTEX"));
398
399 pDraw->TextOut(strText.getData(), ::GraphicsResource.FontRegular, 1.0, FullScreen.pSurface,
400 iX + 128, iY + 64, C4Draw::DEFAULT_MESSAGE_COLOR, ALeft);
401
402 // right coloumn
403 strText.Clear();
404 // game speed
405 strText.AppendFormat("\n\n<c ffff00>%s</c> - %s\n", GetKeyboardInputName("GameSpeedUp").getData(), LoadResStr("IDS_CTL_GAMESPEEDUP"));
406 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("GameSlowDown").getData(), LoadResStr("IDS_CTL_GAMESPEEDDOWN"));
407 // debug
408 strText.AppendFormat("\n\n[%s]\n\n", "Debug");
409 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgModeToggle").getData(), LoadResStr("IDS_CTL_DEBUGMODE"));
410 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgShowVtxToggle").getData(), "Entrance+Vertices");
411 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgShowActionToggle").getData(), "Actions/Commands/Pathfinder/Lights/Menus");
412 strText.AppendFormat("<c ffff00>%s</c> - %s\n", GetKeyboardInputName("DbgShow8BitSurface").getData(), "8-bit surfaces");
413 pDraw->TextOut(strText.getData(), ::GraphicsResource.FontRegular, 1.0, FullScreen.pSurface,
414 iX + iWdt/2 + 64, iY + 64, C4Draw::DEFAULT_MESSAGE_COLOR, ALeft);
415 }
416
ToggleShowNetStatus()417 bool C4GraphicsSystem::ToggleShowNetStatus()
418 {
419 ShowNetstatus = !ShowNetstatus;
420 return true;
421 }
422
ToggleShowVertices()423 bool C4GraphicsSystem::ToggleShowVertices()
424 {
425 if (!Game.DebugMode && !Console.Active) { FlashMessage(LoadResStr("IDS_MSG_NODEBUGMODE")); return false; }
426 ShowVertices = !ShowVertices;
427 ShowEntrance = !ShowEntrance; // vertices and entrance now toggled together
428 FlashMessageOnOff("Entrance+Vertices", ShowVertices || ShowEntrance);
429 return true;
430 }
431
ToggleShowAction()432 bool C4GraphicsSystem::ToggleShowAction()
433 {
434 if (!Game.DebugMode && !Console.Active) { FlashMessage(LoadResStr("IDS_MSG_NODEBUGMODE")); return false; }
435 if (!(ShowAction || ShowCommand || ShowPathfinder || ShowLights || ShowMenuInfo))
436 { ShowAction = true; FlashMessage("Actions"); }
437 else if (ShowAction)
438 { ShowAction = false; ShowCommand = true; FlashMessage("Commands"); }
439 else if (ShowCommand)
440 { ShowCommand = false; ShowPathfinder = true; FlashMessage("Pathfinder"); }
441 else if (ShowPathfinder)
442 { ShowPathfinder = false; ShowLights = true; FlashMessage("Lights"); }
443 else if (ShowLights)
444 { ShowLights = false; ShowMenuInfo = true; FlashMessage("Menu Info"); }
445 else if (ShowMenuInfo)
446 { ShowMenuInfo = false; FlashMessageOnOff("Actions/Commands/Pathfinder/Lights/Menus", false); }
447 return true;
448 }
449
ToggleShow8BitSurface()450 bool C4GraphicsSystem::ToggleShow8BitSurface()
451 {
452 if (!Game.DebugMode && !Console.Active) { FlashMessage(LoadResStr("IDS_MSG_NODEBUGMODE")); return false; }
453 Show8BitSurface = (Show8BitSurface + 1) % 3;
454 if (Show8BitSurface == 0)
455 FlashMessage("Default view");
456 else if (Show8BitSurface == 1)
457 FlashMessage("Foreground 8-bit landscape");
458 else if (Show8BitSurface == 2)
459 FlashMessage("Background 8-bit landscape");
460 return true;
461 }
462
ToggleShowHelp()463 bool C4GraphicsSystem::ToggleShowHelp()
464 {
465 ShowHelp = !ShowHelp;
466 // Turned off? Invalidate background.
467 if (!ShowHelp) InvalidateBg();
468 return true;
469 }
470
471