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