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 /* A viewport to each player */
19 
20 #include "C4Include.h"
21 #include "C4ForbidLibraryCompilation.h"
22 #include "game/C4Viewport.h"
23 
24 #include "editor/C4Console.h"
25 #include "editor/C4ViewportWindow.h"
26 #include "game/C4FullScreen.h"
27 #include "game/C4GraphicsSystem.h"
28 #include "graphics/C4Draw.h"
29 #include "graphics/C4GraphicsResource.h"
30 #include "gui/C4GameMessage.h"
31 #include "gui/C4MouseControl.h"
32 #include "gui/C4ScriptGuiWindow.h"
33 #include "landscape/C4Landscape.h"
34 #include "landscape/C4PXS.h"
35 #include "landscape/C4Particles.h"
36 #include "landscape/C4Sky.h"
37 #include "landscape/fow/C4FoWRegion.h"
38 #include "lib/C4Stat.h"
39 #include "network/C4Network2.h"
40 #include "object/C4Def.h"
41 #include "object/C4GameObjects.h"
42 #include "object/C4Object.h"
43 #include "object/C4ObjectMenu.h"
44 #include "player/C4Player.h"
45 #include "player/C4PlayerList.h"
46 
DropFile(const char * fileName,float x,float y)47 void C4Viewport::DropFile(const char* fileName, float x, float y)
48 {
49 	Game.DropFile(fileName, GetViewX()+x/Zoom, GetViewY()+y/Zoom);
50 }
51 
UpdateOutputSize(int32_t new_width,int32_t new_height)52 bool C4Viewport::UpdateOutputSize(int32_t new_width, int32_t new_height)
53 {
54 	if (!pWindow) return false;
55 	// Output size
56 	C4Rect rect;
57 	if (new_width)
58 	{
59 		rect.x = rect.y = 0;
60 		rect.Wdt = new_width;
61 		rect.Hgt = new_height;
62 	}
63 	else
64 	{
65 #if defined(WITH_QT_EDITOR)
66 		// Never query the window - size is always passed from Qt.
67 		return false;
68 #else
69 		if (!pWindow->GetSize(&rect)) return false;
70 #endif
71 	}
72 	OutX=rect.x; OutY=rect.y;
73 	ViewWdt=rect.Wdt; ViewHgt=rect.Hgt;
74 	ScrollView(0,0);
75 	// Scroll bars
76 	ScrollBarsByViewPosition();
77 	// Reset menus
78 	ResetMenuPositions=true;
79 	// update internal GL size
80 	if (pWindow && pWindow->pSurface)
81 		pWindow->pSurface->UpdateSize(ViewWdt, ViewHgt);
82 	// Update zoom limits based on new size
83 	C4Player *plr = ::Players.Get(Player);
84 	if (plr) plr->ZoomLimitsToViewport(this);
85 	// Done
86 	return true;
87 }
88 
C4Viewport()89 C4Viewport::C4Viewport()
90 {
91 	Player = 0;
92 	viewX = viewY = 0;
93 	targetViewX = targetViewY = 0;
94 	ViewWdt = ViewHgt = 0;
95 	BorderLeft = BorderTop = BorderRight = BorderBottom = 0;
96 	OutX = OutY = ViewWdt = ViewHgt = 0;
97 	DrawX = DrawY = 0;
98 	Zoom = 1.0;
99 	ZoomTarget = 0.0;
100 	ViewportOpenFrame = 0;
101 	ZoomLimitMin = ZoomLimitMax = 0; // no limit
102 	Next = nullptr;
103 	PlayerLock = true;
104 	ResetMenuPositions = false;
105 	viewOffsX = viewOffsY = 0;
106 	fIsNoOwnerViewport = false;
107 }
108 
~C4Viewport()109 C4Viewport::~C4Viewport()
110 {
111 	DisableFoW();
112 	if (pWindow) { delete pWindow->pSurface; pWindow->Clear(); }
113 }
114 
DrawOverlay(C4TargetFacet & cgo,const ZoomData & GameZoom)115 void C4Viewport::DrawOverlay(C4TargetFacet &cgo, const ZoomData &GameZoom)
116 {
117 	if (!Game.C4S.Head.Film || !Game.C4S.Head.Replay)
118 	{
119 		// Player info
120 		C4ST_STARTNEW(PInfoStat, "C4Viewport::DrawOverlay: Player Info")
121 		DrawPlayerInfo(cgo);
122 		C4ST_STOP(PInfoStat)
123 		C4ST_STARTNEW(MenuStat, "C4Viewport::DrawOverlay: Menu")
124 		DrawMenu(cgo);
125 		C4ST_STOP(MenuStat)
126 	}
127 
128 	// Control overlays (if not film/replay)
129 	if (!Game.C4S.Head.Film || !Game.C4S.Head.Replay)
130 	{
131 		// Mouse control
132 		if (::MouseControl.IsViewport(this))
133 		{
134 			C4ST_STARTNEW(MouseStat, "C4Viewport::DrawOverlay: Mouse")
135 			::MouseControl.Draw(cgo, GameZoom);
136 			// Draw GUI-mouse in EM if active
137 			if (pWindow) ::pGUI->RenderMouse(cgo);
138 			C4ST_STOP(MouseStat)
139 		}
140 	}
141 }
142 
DrawMenu(C4TargetFacet & cgo0)143 void C4Viewport::DrawMenu(C4TargetFacet &cgo0)
144 {
145 	// Get player
146 	C4Player *pPlr = ::Players.Get(Player);
147 
148 	// for menus, cgo is using GUI-syntax: TargetX/Y marks the drawing offset; x/y/Wdt/Hgt marks the offset rect
149 	C4TargetFacet cgo; cgo.Set(cgo0);
150 	cgo.X = 0; cgo.Y = 0;
151 	cgo.Wdt = cgo0.Wdt * cgo0.Zoom; cgo.Hgt = cgo0.Hgt * cgo0.Zoom;
152 	cgo.TargetX = float(cgo0.X); cgo.TargetY = float(cgo0.Y);
153 	cgo.Zoom = 1;
154 	pDraw->SetZoom(cgo.X, cgo.Y, cgo.Zoom);
155 
156 	// Player eliminated
157 	if (pPlr && pPlr->Eliminated)
158 	{
159 		pDraw->TextOut(FormatString(LoadResStr(pPlr->Surrendered ? "IDS_PLR_SURRENDERED" :  "IDS_PLR_ELIMINATED"),pPlr->GetName()).getData(),
160 		                           ::GraphicsResource.FontRegular, 1.0, cgo.Surface,cgo.TargetX+cgo.Wdt/2,cgo.TargetY+2*cgo.Hgt/3,0xfaFF0000,ACenter);
161 		return;
162 	}
163 
164 	// Player cursor object menu
165 	if (pPlr && pPlr->Cursor && pPlr->Cursor->Menu)
166 	{
167 		if (ResetMenuPositions) pPlr->Cursor->Menu->ResetLocation();
168 		// if mouse is dragging, make it transparent to easy construction site drag+drop
169 		bool fDragging=false;
170 		if (::MouseControl.IsDragging() && ::MouseControl.IsViewport(this))
171 		{
172 			fDragging = true;
173 			pDraw->ActivateBlitModulation(0x4fffffff);
174 		}
175 		// draw menu
176 		pPlr->Cursor->Menu->Draw(cgo);
177 		// reset modulation for dragging
178 		if (fDragging) pDraw->DeactivateBlitModulation();
179 	}
180 	// Player menu
181 	if (pPlr && pPlr->Menu.IsActive())
182 	{
183 		if (ResetMenuPositions) pPlr->Menu.ResetLocation();
184 		pPlr->Menu.Draw(cgo);
185 	}
186 	// Fullscreen menu
187 	if (FullScreen.pMenu && FullScreen.pMenu->IsActive())
188 	{
189 		if (ResetMenuPositions) FullScreen.pMenu->ResetLocation();
190 		FullScreen.pMenu->Draw(cgo);
191 	}
192 
193 	// Flag done
194 	ResetMenuPositions=false;
195 
196 	// restore Zoom
197 	pDraw->SetZoom(cgo0.X, cgo0.Y, cgo0.Zoom);
198 }
199 
Draw(C4TargetFacet & cgo0,bool fDrawGame,bool fDrawOverlay)200 void C4Viewport::Draw(C4TargetFacet &cgo0, bool fDrawGame, bool fDrawOverlay)
201 {
202 #ifdef USE_CONSOLE
203 	// No drawing in console mode
204 	return;
205 #endif
206 	C4TargetFacet cgo; cgo.Set(cgo0);
207 	ZoomData GameZoom;
208 	GameZoom.X = cgo.X; GameZoom.Y = cgo.Y;
209 	GameZoom.Zoom = cgo.Zoom;
210 
211 	// Draw landscape borders
212 	if (BorderLeft > 0.0f)   pDraw->BlitSurfaceTile(::GraphicsResource.fctBackground.Surface, cgo.Surface, DrawX, DrawY, BorderLeft, ViewHgt, -DrawX, -DrawY, nullptr);
213 	if (BorderTop > 0.0f)    pDraw->BlitSurfaceTile(::GraphicsResource.fctBackground.Surface, cgo.Surface, DrawX + BorderLeft, DrawY, ViewWdt - BorderLeft - BorderRight, BorderTop, -DrawX - BorderLeft, -DrawY, nullptr);
214 	if (BorderRight > 0.0f)  pDraw->BlitSurfaceTile(::GraphicsResource.fctBackground.Surface, cgo.Surface, DrawX + ViewWdt - BorderRight, DrawY, BorderRight, ViewHgt, -DrawX - ViewWdt + BorderRight, -DrawY, nullptr);
215 	if (BorderBottom > 0.0f) pDraw->BlitSurfaceTile(::GraphicsResource.fctBackground.Surface, cgo.Surface, DrawX + BorderLeft, DrawY + ViewHgt - BorderBottom, ViewWdt - BorderLeft - BorderRight, BorderBottom, -DrawX - BorderLeft, -DrawY - ViewHgt + BorderBottom, nullptr);
216 
217 	// Compute non-bordered viewport area
218 	cgo.X += BorderLeft; cgo.Y += BorderTop; cgo.Wdt -= (BorderLeft + BorderRight) / cgo.Zoom; cgo.Hgt -= (BorderTop + BorderBottom) / cgo.Zoom;
219 	cgo.TargetX += BorderLeft / Zoom; cgo.TargetY += BorderTop / Zoom;
220 
221 	// Apply Zoom
222 	GameZoom.X = cgo.X; GameZoom.Y = cgo.Y;
223 	pDraw->SetZoom(GameZoom);
224 	// Set clipper to integer bounds around floating point viewport region
225 	const FLOAT_RECT clipRect = { DrawX + BorderLeft, DrawX + ViewWdt - BorderRight, DrawY + BorderTop, DrawY + ViewHgt - BorderBottom };
226 	const C4Rect& clipRectInt(clipRect);
227 	pDraw->SetPrimaryClipper(clipRectInt.x, clipRectInt.y, clipRectInt.x + clipRectInt.Wdt - 1, clipRectInt.y + clipRectInt.Hgt - 1);
228 
229 	last_game_draw_cgo = cgo;
230 
231 	if (fDrawGame)
232 	{
233 		// --- activate FoW here ---
234 
235 		// Render FoW only if active for player
236 		C4Player *pPlr = ::Players.Get(Player);
237 		C4FoWRegion* pFoW = nullptr;
238 		if (pPlr && pPlr->fFogOfWar) pFoW = this->pFoW.get();
239 
240 		// Update FoW
241 		if (pFoW)
242 		{
243 			// Viewport region in landscape coordinates
244 			const FLOAT_RECT vpRect = { cgo.TargetX, cgo.TargetX + cgo.Wdt, cgo.TargetY, cgo.TargetY + cgo.Hgt };
245 			// Region in which the light is calculated
246 			// At the moment, just choose integer coordinates to surround the viewport
247 			const C4Rect lightRect(vpRect);
248 			if (!lightRect.Wdt || !lightRect.Hgt)
249 			{
250 				// Do not bother initializing FoW on empty region; would cause errors in drawing proc
251 				pFoW = nullptr;
252 			}
253 			else
254 			{
255 				pFoW->Update(lightRect, vpRect);
256 
257 				if (!pFoW->Render())
258 				{
259 					// If FoW init fails, do not set it for further drawing
260 					pFoW = nullptr;
261 				}
262 			}
263 		}
264 
265 		pDraw->SetFoW(pFoW);
266 
267 		C4ST_STARTNEW(SkyStat, "C4Viewport::Draw: Sky")
268 			::Landscape.GetSky().Draw(cgo);
269 		C4ST_STOP(SkyStat)
270 
271 			::Objects.Draw(cgo, Player, -2147483647 - 1 /* INT32_MIN */, 0);
272 
273 		// Draw Landscape
274 		C4ST_STARTNEW(LandStat, "C4Viewport::Draw: Landscape")
275 			::Landscape.Draw(cgo, pFoW);
276 		C4ST_STOP(LandStat)
277 
278 			// draw PXS (unclipped!)
279 			C4ST_STARTNEW(PXSStat, "C4Viewport::Draw: PXS")
280 			::PXS.Draw(cgo);
281 		C4ST_STOP(PXSStat)
282 
283 		// Draw objects which are behind the particle plane.
284 		const int particlePlane = 900;
285 		C4ST_STARTNEW(ObjStat, "C4Viewport::Draw: Objects (1)")
286 			::Objects.Draw(cgo, Player, 1, particlePlane);
287 		C4ST_STOP(ObjStat)
288 
289 		// Draw global dynamic particles on a specific Plane
290 		// to enable scripters to put objects both behind and in front of particles.
291 		C4ST_STARTNEW(PartStat, "C4Viewport::Draw: Dynamic Particles")
292 			::Particles.DrawGlobalParticles(cgo);
293 		C4ST_STOP(PartStat)
294 
295 		// Now the remaining objects in front of the particles (e.g. GUI elements)
296 		C4ST_STARTNEW(Obj2Stat, "C4Viewport::Draw: Objects (2)")
297 			::Objects.Draw(cgo, Player, particlePlane + 1, 2147483647 /* INT32_MAX */);
298 		C4ST_STOP(Obj2Stat)
299 
300 			// Draw everything else without FoW
301 			pDraw->SetFoW(nullptr);
302 	}
303 	else
304 	{
305 		pDraw->DrawBoxDw(cgo.Surface, cgo.X, cgo.Y, cgo.X + cgo.Wdt, cgo.Y + cgo.Hgt, 0xff000000);
306 	}
307 
308 	// Draw PathFinder
309 	if (::GraphicsSystem.ShowPathfinder) Game.PathFinder.Draw(cgo);
310 
311 	// Draw overlay
312 	if (!Game.C4S.Head.Film || !Game.C4S.Head.Replay) Game.DrawCrewOverheadText(cgo, Player);
313 
314 	// Lights overlay
315 	if (::GraphicsSystem.ShowLights && pFoW) pFoW->Render(&cgo);
316 
317 	if (fDrawOverlay)
318 	{
319 		// Determine zoom of overlay
320 		float fGUIZoom = GetGUIZoom();
321 		// now restore complete cgo range for overlay drawing
322 		pDraw->SetZoom(DrawX,DrawY, fGUIZoom);
323 		pDraw->SetPrimaryClipper(DrawX,DrawY,DrawX+(ViewWdt-1),DrawY+(ViewHgt-1));
324 		C4TargetFacet gui_cgo;
325 		gui_cgo.Set(cgo0);
326 
327 		gui_cgo.X = DrawX; gui_cgo.Y = DrawY; gui_cgo.Zoom = fGUIZoom;
328 		gui_cgo.Wdt = int(float(ViewWdt)/fGUIZoom); gui_cgo.Hgt = int(float(ViewHgt)/fGUIZoom);
329 		gui_cgo.TargetX = GetViewX(); gui_cgo.TargetY = GetViewY();
330 
331 		last_gui_draw_cgo = gui_cgo;
332 
333 		// draw custom GUI objects
334 		::Objects.ForeObjects.DrawIfCategory(gui_cgo, Player, C4D_Foreground, false);
335 
336 		// Draw overlay
337 		C4ST_STARTNEW(OvrStat, "C4Viewport::Draw: Overlay")
338 
339 		if (Application.isEditor) ::Console.EditCursor.Draw(cgo);
340 
341 		// Game messages
342 		C4ST_STARTNEW(MsgStat, "C4Viewport::DrawOverlay: Messages")
343 		pDraw->SetZoom(0, 0, 1.0);
344 		::Messages.Draw(gui_cgo, cgo, Player);
345 		C4ST_STOP(MsgStat)
346 
347 		// ingame menus
348 		C4ST_STARTNEW(GuiWindowStat, "C4Viewport::DrawOverlay: Menus")
349 		pDraw->SetZoom(0, 0, 1.0);
350 		::Game.ScriptGuiRoot->DrawAll(gui_cgo, Player);
351 		C4ST_STOP(GuiWindowStat)
352 
353 		DrawOverlay(gui_cgo, GameZoom);
354 
355 		// Netstats
356 		if (::GraphicsSystem.ShowNetstatus)
357 			::Network.DrawStatus(gui_cgo);
358 
359 		C4ST_STOP(OvrStat)
360 
361 	}
362 
363 	// Remove zoom n clippers
364 	pDraw->SetZoom(0, 0, 1.0);
365 	pDraw->NoPrimaryClipper();
366 
367 }
368 
BlitOutput()369 void C4Viewport::BlitOutput()
370 {
371 	if (pWindow)
372 	{
373 		C4Rect rtSrc,rtDst;
374 		rtSrc.x = DrawX; rtSrc.y = DrawY;  rtSrc.Wdt = ViewWdt; rtSrc.Hgt = ViewHgt;
375 		rtDst.x = OutX;  rtDst.y = OutY;   rtDst.Wdt = ViewWdt; rtDst.Hgt = ViewHgt;
376 		pWindow->pSurface->PageFlip(&rtSrc, &rtDst);
377 	}
378 }
379 
Execute()380 void C4Viewport::Execute()
381 {
382 	// Adjust position
383 	AdjustZoomAndPosition();
384 	// Current graphics output
385 	C4TargetFacet cgo;
386 	C4Surface *target = pWindow ? pWindow->pSurface : FullScreen.pSurface;
387 	cgo.Set(target,DrawX,DrawY,float(ViewWdt)/Zoom,float(ViewHgt)/Zoom,GetViewX(),GetViewY(),Zoom);
388 	pDraw->PrepareRendering(target);
389 	// Load script uniforms from Global.Uniforms
390 	auto uniform_pop = pDraw->scriptUniform.Push(::GameScript.ScenPropList.getPropList());
391 	// Do not spoil game contents on owner-less viewport
392 	bool draw_game = true;
393 	if (Player == NO_OWNER)
394 		if (!::Application.isEditor && !::Game.DebugMode)
395 			if (!::Network.isEnabled() || !::Network.Clients.GetLocal() || !::Network.Clients.GetLocal()->isObserver())
396 				if (::Game.PlayerInfos.GetJoinIssuedPlayerCount() > 0) // free scrolling allowed if the scenario was started explicitely without players to inspect the landscape
397 					if (Game.C4S.Landscape.Secret)
398 						draw_game = false;
399 	// Draw
400 	Draw(cgo, draw_game, true);
401 	// Blit output
402 	BlitOutput();
403 }
404 
405 /* This method is called whenever the viewport size is changed. Thus, its job
406    is to recalculate the zoom and zoom limits with the new values for ViewWdt
407    and ViewHgt. */
CalculateZoom()408 void C4Viewport::CalculateZoom()
409 {
410 	// Zoom is only initialized by player or global setting during viewport creation time, because after that
411 	// the player may have changed to another preferred zoom.
412 	// However, viewports may change multiple times during startup (because of NO_OWNER viewport being deleted
413 	// and possible other player joins). So check by frame counter. Zoom changes done in paused mode on the
414 	// player init frame will be lost, but that should not be a problem.
415 	if(ViewportOpenFrame >= Game.FrameCounter)
416 		InitZoom();
417 
418 	C4Player *plr = Players.Get(Player);
419 	if (plr)
420 		plr->ZoomLimitsToViewport(this);
421 	else
422 		SetZoomLimits(0.8*std::min<float>(float(ViewWdt)/::Landscape.GetWidth(),float(ViewHgt)/::Landscape.GetHeight()), 8);
423 
424 }
425 
InitZoom()426 void C4Viewport::InitZoom()
427 {
428 	C4Player *plr = Players.Get(Player);
429 	if (plr)
430 	{
431 		plr->ZoomToViewport(this, true);
432 	}
433 	else
434 	{
435 		ZoomTarget = std::max<float>(float(ViewWdt)/::Landscape.GetWidth(), 1.0f);
436 		Zoom = ZoomTarget;
437 	}
438 }
439 
ChangeZoom(float by_factor)440 void C4Viewport::ChangeZoom(float by_factor)
441 {
442 	ZoomTarget *= by_factor;
443 	if (ZoomLimitMin && ZoomTarget < ZoomLimitMin) ZoomTarget = ZoomLimitMin;
444 	if (ZoomLimitMax && ZoomTarget > ZoomLimitMax) ZoomTarget = ZoomLimitMax;
445 }
446 
SetZoom(float to_value,bool direct)447 void C4Viewport::SetZoom(float to_value, bool direct)
448 {
449 	ZoomTarget = to_value;
450 	if (Player != NO_OWNER || !::Application.isEditor)
451 	{
452 		if (ZoomLimitMin && ZoomTarget < ZoomLimitMin) ZoomTarget = ZoomLimitMin;
453 		if (ZoomLimitMax && ZoomTarget > ZoomLimitMax) ZoomTarget = ZoomLimitMax;
454 	}
455 	// direct: Set zoom without scrolling to it
456 	if (direct) Zoom = ZoomTarget;
457 }
458 
SetZoomLimits(float to_min_zoom,float to_max_zoom)459 void C4Viewport::SetZoomLimits(float to_min_zoom, float to_max_zoom)
460 {
461 	ZoomLimitMin = to_min_zoom;
462 	ZoomLimitMax = to_max_zoom;
463 	if (ZoomLimitMax && ZoomLimitMax < ZoomLimitMin) ZoomLimitMax = ZoomLimitMin;
464 	ChangeZoom(1); // Constrains zoom to limit.
465 }
466 
GetZoomByViewRange(int32_t size_x,int32_t size_y) const467 float C4Viewport::GetZoomByViewRange(int32_t size_x, int32_t size_y) const
468 {
469 	// set zoom such that both size_x and size_y are guarantueed to fit into the viewport range
470 	// determine whether zoom is to be calculated by x or by y
471 	bool zoom_by_y = false;
472 	if (size_x && size_y)
473 	{
474 		zoom_by_y = (size_y * ViewWdt > size_x * ViewHgt);
475 	}
476 	else if (size_y)
477 	{
478 		// no x size passed - zoom by y
479 		zoom_by_y = true;
480 	}
481 	else
482 	{
483 		// 0/0 size passed - zoom to default
484 		if (!size_x)
485 			size_x = C4VP_DefViewRangeX * 2;
486 		zoom_by_y = false;
487 	}
488 	// zoom calculation
489 	if (zoom_by_y)
490 		return float(ViewHgt) / size_y;
491 	else
492 		return float(ViewWdt) / size_x;
493 }
494 
SetZoom(float zoomValue)495 void C4Viewport::SetZoom(float zoomValue)
496 {
497 	Zoom = zoomValue;
498 	// also set target to prevent zoom from changing back
499 	ZoomTarget = zoomValue;
500 }
501 
AdjustZoomAndPosition()502 void C4Viewport::AdjustZoomAndPosition()
503 {
504 	// Move zoom towards target zoom
505 	if (ZoomTarget < 0.000001f) CalculateZoom();
506 	// Change Zoom
507 
508 	if (Zoom != ZoomTarget)
509 	{
510 		float DeltaZoom = Zoom / ZoomTarget;
511 		if (DeltaZoom<1) DeltaZoom = 1 / DeltaZoom;
512 
513 		// Minimal Zoom change factor
514 		static const float Z0 = pow(C4GFX_ZoomStep, 1.0f / 8.0f);
515 
516 		// We change zoom based on (logarithmic) distance of current zoom
517 		// to target zoom. The greater the distance the more we adjust the
518 		// zoom in one frame. There is a minimal zoom change Z0 to make sure
519 		// we reach ZoomTarget in finite time.
520 		float ZoomAdjustFactor = Z0 * pow(DeltaZoom, 1.0f / 8.0f);
521 
522 		if (Zoom == 0)
523 			Zoom = ZoomTarget;
524 		else
525 		{
526 			// Remember old viewport center
527 			float view_mid_x = this->viewX + float(this->ViewWdt) / Zoom / 2.0f;
528 			float view_mid_y = this->viewY + float(this->ViewHgt) / Zoom / 2.0f;
529 
530 			if (Zoom < ZoomTarget)
531 				Zoom = std::min(Zoom * ZoomAdjustFactor, ZoomTarget);
532 			if (Zoom > ZoomTarget)
533 				Zoom = std::max(Zoom / ZoomAdjustFactor, ZoomTarget);
534 
535 			// Restore new viewport center
536 			this->viewX = view_mid_x - float(this->ViewWdt) / Zoom / 2.0f;
537 			this->viewY = view_mid_y - float(this->ViewHgt) / Zoom / 2.0f;
538 		}
539 	}
540 	// Adjust position
541 	AdjustPosition(false);
542 }
543 
AdjustPosition(bool immediate)544 void C4Viewport::AdjustPosition(bool immediate)
545 {
546 	if (ViewWdt == 0 || ViewHgt == 0)
547 	{
548 		// zero-sized viewport, possibly minimized editor window
549 		// don't do anything then
550 		return;
551 	}
552 
553 	assert(Zoom>0);
554 	assert(ZoomTarget>0);
555 
556 	float ViewportScrollBorder = fIsNoOwnerViewport ? 0 : float(C4ViewportScrollBorder);
557 	C4Player *pPlr = ::Players.Get(Player);
558 
559 	// View position
560 	if (PlayerLock && ValidPlr(Player))
561 	{
562 		float scrollRange, extraBoundsX, extraBoundsY;
563 
564 		scrollRange = extraBoundsX = extraBoundsY = 0;
565 
566 		// target view position (landscape coordinates)
567 		float targetCenterViewX = fixtof(pPlr->ViewX);
568 		float targetCenterViewY = fixtof(pPlr->ViewY);
569 
570 		if (pPlr->ViewMode == C4PVM_Scrolling)
571 		{
572 			extraBoundsX = extraBoundsY = ViewportScrollBorder;
573 		}
574 		else
575 		{
576 			scrollRange = std::min(ViewWdt/(10*Zoom),ViewHgt/(10*Zoom));
577 
578 			// if view is close to border, allow scrolling
579 			if (targetCenterViewX < ViewportScrollBorder) extraBoundsX = std::min<float>(ViewportScrollBorder - targetCenterViewX, ViewportScrollBorder);
580 			else if (targetCenterViewX >= ::Landscape.GetWidth() - ViewportScrollBorder) extraBoundsX = std::min<float>(targetCenterViewX - ::Landscape.GetWidth(), 0) + ViewportScrollBorder;
581 			if (targetCenterViewY < ViewportScrollBorder) extraBoundsY = std::min<float>(ViewportScrollBorder - targetCenterViewY, ViewportScrollBorder);
582 			else if (targetCenterViewY >= ::Landscape.GetHeight() - ViewportScrollBorder) extraBoundsY = std::min<float>(targetCenterViewY - ::Landscape.GetHeight(), 0) + ViewportScrollBorder;
583 		}
584 
585 		extraBoundsX = std::max(extraBoundsX, (ViewWdt/Zoom - ::Landscape.GetWidth())/2 + 1);
586 		extraBoundsY = std::max(extraBoundsY, (ViewHgt/Zoom - ::Landscape.GetHeight())/2 + 1);
587 
588 		// add mouse auto scroll
589 		if (pPlr->MouseControl && ::MouseControl.InitCentered && Config.Controls.MouseAutoScroll)
590 		{
591 			float strength = Config.Controls.MouseAutoScroll/100.0f;
592 			targetCenterViewX += strength*(::MouseControl.VpX - ViewWdt/2.0f)/Zoom;
593 			targetCenterViewY += strength*(::MouseControl.VpY - ViewHgt/2.0f)/Zoom;
594 		}
595 
596 		// scroll range
597 		if (!immediate)
598 		{
599 			targetCenterViewX = Clamp(targetCenterViewX, targetCenterViewX - scrollRange, targetCenterViewX + scrollRange);
600 			targetCenterViewY = Clamp(targetCenterViewY, targetCenterViewY - scrollRange, targetCenterViewY + scrollRange);
601 		}
602 		// bounds
603 		targetCenterViewX = Clamp(targetCenterViewX, ViewWdt/Zoom/2 - extraBoundsX, ::Landscape.GetWidth() - ViewWdt/Zoom/2 + extraBoundsX);
604 		targetCenterViewY = Clamp(targetCenterViewY, ViewHgt/Zoom/2 - extraBoundsY, ::Landscape.GetHeight() - ViewHgt/Zoom/2 + extraBoundsY);
605 
606 		targetViewX = targetCenterViewX - ViewWdt/Zoom/2 + viewOffsX;
607 		targetViewY = targetCenterViewY - ViewHgt/Zoom/2 + viewOffsY;
608 
609 		if (immediate)
610 		{
611 			// immediate scroll
612 			SetViewX(targetViewX);
613 			SetViewY(targetViewY);
614 		}
615 		else
616 		{
617 			// smooth scroll
618 			int32_t smooth = Clamp<int32_t>(Config.General.ScrollSmooth, 1, 50);
619 			ScrollView((targetViewX - viewX) / smooth, (targetViewY - viewY) / smooth);
620 		}
621 	}
622 
623 	UpdateBordersX();
624 	UpdateBordersY();
625 
626 	// NO_OWNER can't scroll
627 	if (fIsNoOwnerViewport) { viewOffsX=0; viewOffsY=0; }
628 }
629 
CenterPosition()630 void C4Viewport::CenterPosition()
631 {
632 	// center viewport position on map
633 	// set center position
634 	SetViewX(::Landscape.GetWidth()/2 + ViewWdt/Zoom/2);
635 	SetViewY(::Landscape.GetHeight()/2 + ViewHgt/Zoom/2);
636 }
637 
UpdateBordersX()638 void C4Viewport::UpdateBordersX()
639 {
640 	BorderLeft = std::max(-GetViewX() * Zoom, 0.0f);
641 	BorderRight = std::max(ViewWdt - ::Landscape.GetWidth() * Zoom + GetViewX() * Zoom, 0.0f);
642 }
643 
UpdateBordersY()644 void C4Viewport::UpdateBordersY()
645 {
646 	BorderTop = std::max(-GetViewY() * Zoom, 0.0f);
647 	BorderBottom = std::max(ViewHgt - ::Landscape.GetHeight() * Zoom + GetViewY() * Zoom, 0.0f);
648 }
649 
DrawPlayerInfo(C4TargetFacet & cgo)650 void C4Viewport::DrawPlayerInfo(C4TargetFacet &cgo)
651 {
652 	C4Facet ccgo;
653 	if (!ValidPlr(Player)) return;
654 	// Controls
655 	DrawPlayerStartup(cgo);
656 }
657 
Init(int32_t iPlayer,bool fSetTempOnly)658 bool C4Viewport::Init(int32_t iPlayer, bool fSetTempOnly)
659 {
660 	// Fullscreen viewport initialization
661 	// Set Player
662 	if (!ValidPlr(iPlayer)) iPlayer = NO_OWNER;
663 	Player=iPlayer;
664 	ViewportOpenFrame = Game.FrameCounter;
665 	if (!fSetTempOnly) fIsNoOwnerViewport = (iPlayer == NO_OWNER);
666 	if (Application.isEditor)
667 	{
668 		// Console viewport initialization
669 		// Create window
670 		pWindow = std::make_unique<C4ViewportWindow>(this);
671 		if (!pWindow->Init(Player))
672 			return false;
673 		UpdateOutputSize();
674 		// Disable player lock on unowned viewports
675 		if (!ValidPlr(Player)) TogglePlayerLock();
676 		// Don't call Execute right away since it is not yet guaranteed that
677 		// the Player has set this as its Viewport, and the drawing routines rely
678 		// on that.
679 	}
680 	else
681 	{
682 		// Owned viewport: clear any flash message explaining observer menu
683 		if (ValidPlr(iPlayer)) ::GraphicsSystem.FlashMessage("");
684 	}
685 
686 	EnableFoW();
687 	return true;
688 }
689 
DisableFoW()690 void C4Viewport::DisableFoW()
691 {
692 	pFoW.reset();
693 }
694 
EnableFoW()695 void C4Viewport::EnableFoW()
696 {
697 	if (::Landscape.HasFoW() && Player != NO_OWNER)
698 	{
699 		pFoW = std::make_unique<C4FoWRegion>(::Landscape.GetFoW(), ::Players.Get(Player));
700 	}
701 	else
702 	{
703 		DisableFoW();
704 	}
705 }
706 
707 extern int32_t DrawMessageOffset;
708 
DrawPlayerStartup(C4TargetFacet & cgo)709 void C4Viewport::DrawPlayerStartup(C4TargetFacet &cgo)
710 {
711 	C4Player *pPlr;
712 	if (!(pPlr = ::Players.Get(Player))) return;
713 	if (!pPlr->LocalControl || !pPlr->ShowStartup) return;
714 	int32_t iNameHgtOff=0;
715 
716 	// Control
717 	// unnecessary with the current control sets
718 	if (pPlr && pPlr->ControlSet)
719 	{
720 		C4Facet controlset_facet = pPlr->ControlSet->GetPicture();
721 		if (controlset_facet.Wdt) controlset_facet.Draw(cgo.Surface,
722 			    cgo.X+(cgo.Wdt-controlset_facet.Wdt)/2,
723 			    cgo.Y+cgo.Hgt * 2/3 + DrawMessageOffset,
724 			    0,0);
725 		iNameHgtOff=GfxR->fctKeyboard.Hgt;
726 	}
727 
728 	// Name
729 	pDraw->TextOut(pPlr->GetName(), ::GraphicsResource.FontRegular, 1.0, cgo.Surface,
730 	                           cgo.X+cgo.Wdt/2,cgo.Y+cgo.Hgt*2/3+iNameHgtOff + DrawMessageOffset,
731 	                           pPlr->ColorDw | 0xff000000, ACenter);
732 }
733 
ScrollView(float byX,float byY)734 void C4Viewport::ScrollView(float byX, float byY)
735 {
736 	SetViewX(viewX + byX);
737 	SetViewY(viewY + byY);
738 }
739 
SetViewX(float x)740 void C4Viewport::SetViewX(float x)
741 {
742 	viewX = x;
743 
744 	if (fIsNoOwnerViewport)
745 	{
746 		if(::Landscape.GetWidth() < ViewWdt / Zoom)
747 		{
748 			viewX = ::Landscape.GetWidth()/2 - ViewWdt / Zoom / 2;
749 		}
750 		else
751 		{
752 			viewX = Clamp(x, 0.0f, ::Landscape.GetWidth() - ViewWdt / Zoom);
753 		}
754 	}
755 
756 	UpdateBordersX();
757 }
758 
SetViewY(float y)759 void C4Viewport::SetViewY(float y)
760 {
761 	viewY = y;
762 
763 	if (fIsNoOwnerViewport)
764 	{
765 		if(::Landscape.GetHeight() < ViewHgt / Zoom)
766 		{
767 			viewY = ::Landscape.GetHeight()/2 - ViewHgt / Zoom / 2;
768 		}
769 		else
770 		{
771 			viewY = Clamp(y, 0.0f, ::Landscape.GetHeight() - ViewHgt / Zoom);
772 		}
773 	}
774 
775 	UpdateBordersY();
776 }
777 
SetOutputSize(int32_t iDrawX,int32_t iDrawY,int32_t iOutX,int32_t iOutY,int32_t iOutWdt,int32_t iOutHgt)778 void C4Viewport::SetOutputSize(int32_t iDrawX, int32_t iDrawY, int32_t iOutX, int32_t iOutY, int32_t iOutWdt, int32_t iOutHgt)
779 {
780 	int32_t deltaWidth = ViewWdt-iOutWdt;
781 	int32_t deltaHeight = ViewHgt-iOutHgt;
782 	// update output parameters
783 	DrawX=iDrawX; DrawY=iDrawY;
784 	OutX=iOutX; OutY=iOutY;
785 	ViewWdt=iOutWdt; ViewHgt=iOutHgt;
786 	// update view position: Remain centered at previous position
787 	// scrolling the view must be done after setting the new view width and height
788 	ScrollView(deltaWidth/2, deltaHeight/2);
789 	CalculateZoom();
790 	// Reset menus
791 	ResetMenuPositions=true;
792 	// player uses mouse control? then clip the cursor
793 	C4Player *pPlr;
794 	if ((pPlr=::Players.Get(Player)))
795 		if (pPlr->MouseControl)
796 		{
797 			::MouseControl.UpdateClip();
798 			// and inform GUI
799 			::pGUI->SetPreferredDlgRect(C4Rect(iOutX, iOutY, iOutWdt, iOutHgt));
800 		}
801 }
802 
ClearPointers(C4Object * pObj)803 void C4Viewport::ClearPointers(C4Object *pObj)
804 {
805 
806 }
807 
NextPlayer()808 void C4Viewport::NextPlayer()
809 {
810 	C4Player *pPlr; int32_t iPlr;
811 	if (!(pPlr = ::Players.Get(Player)))
812 	{
813 		if (!(pPlr = ::Players.First)) return;
814 	}
815 	else if (!(pPlr = pPlr->Next))
816 		if (Game.C4S.Head.Film && Game.C4S.Head.Replay)
817 			pPlr = ::Players.First; // cycle to first in film mode only; in network obs mode allow NO_OWNER-view
818 	if (pPlr) iPlr = pPlr->Number; else iPlr = NO_OWNER;
819 	if (iPlr != Player) Init(iPlr, true);
820 }
821 
IsViewportMenu(class C4Menu * pMenu)822 bool C4Viewport::IsViewportMenu(class C4Menu *pMenu)
823 {
824 	// check all associated menus
825 	// Get player
826 	C4Player *pPlr = ::Players.Get(Player);
827 	// Player eliminated: No menu
828 	if (pPlr && pPlr->Eliminated) return false;
829 	// Player cursor object menu
830 	if (pPlr && pPlr->Cursor && pPlr->Cursor->Menu == pMenu) return true;
831 	// Player menu
832 	if (pPlr && pPlr->Menu.IsActive() && &(pPlr->Menu) == pMenu) return true;
833 	// Fullscreen menu (if active, only one viewport can exist)
834 	if (FullScreen.pMenu && FullScreen.pMenu->IsActive() && FullScreen.pMenu == pMenu) return true;
835 	// no match
836 	return false;
837 }
838 
839 // C4ViewportList
840 
841 C4ViewportList Viewports;
842 
C4ViewportList()843 C4ViewportList::C4ViewportList()
844 {
845 	ViewportArea.Default();
846 }
847 C4ViewportList::~C4ViewportList() = default;
Clear()848 void C4ViewportList::Clear()
849 {
850 	C4Viewport *next;
851 	while (FirstViewport)
852 	{
853 		next=FirstViewport->Next;
854 		delete FirstViewport;
855 		FirstViewport=next;
856 	}
857 	FirstViewport=nullptr;
858 }
859 
Execute(bool DrawBackground)860 void C4ViewportList::Execute(bool DrawBackground)
861 {
862 	// Background redraw
863 	if (DrawBackground)
864 		DrawFullscreenBackground();
865 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next)
866 	{
867 		if (cvp->GetWindow())
868 			cvp->GetWindow()->RequestUpdate();
869 		else
870 			cvp->Execute();
871 	}
872 }
873 
DrawFullscreenBackground()874 void C4ViewportList::DrawFullscreenBackground()
875 {
876 	for (int i=0, iNum=BackgroundAreas.GetCount(); i<iNum; ++i)
877 	{
878 		const C4Rect &rc = BackgroundAreas.Get(i);
879 		pDraw->BlitSurfaceTile(::GraphicsResource.fctBackground.Surface,FullScreen.pSurface,rc.x,rc.y,rc.Wdt,rc.Hgt,-rc.x,-rc.y, nullptr);
880 	}
881 }
882 
CloseViewport(C4Viewport * cvp)883 bool C4ViewportList::CloseViewport(C4Viewport * cvp)
884 {
885 	if (!cvp) return false;
886 	// Chop the start of the chain off
887 	if (FirstViewport == cvp)
888 	{
889 		FirstViewport = cvp->Next;
890 		delete cvp;
891 		StartSoundEffect("UI::CloseViewport");
892 	}
893 	// Take out of the chain
894 	else for (C4Viewport * prev = FirstViewport; prev; prev = prev->Next)
895 		{
896 			if (prev->Next == cvp)
897 			{
898 				prev->Next = cvp->Next;
899 				delete cvp;
900 				StartSoundEffect("UI::CloseViewport");
901 			}
902 		}
903 	// Recalculate viewports
904 	RecalculateViewports();
905 	// Done
906 	return true;
907 }
908 #ifdef USE_WIN32_WINDOWS
GetViewport(HWND hwnd)909 C4Viewport* C4ViewportList::GetViewport(HWND hwnd)
910 {
911 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next)
912 		if (cvp->pWindow->hWindow==hwnd)
913 			return cvp;
914 	return nullptr;
915 }
916 #endif
CreateViewport(int32_t iPlayer,bool fSilent)917 bool C4ViewportList::CreateViewport(int32_t iPlayer, bool fSilent)
918 {
919 	// Create and init new viewport, add to viewport list
920 	int32_t iLastCount = GetViewportCount();
921 	C4Viewport *nvp = new C4Viewport;
922 	bool fOkay = nvp->Init(iPlayer, false);
923 	if (!fOkay) { delete nvp; return false; }
924 	C4Viewport *pLast;
925 	for (pLast=FirstViewport; pLast && pLast->Next; pLast=pLast->Next) {}
926 	if (pLast) pLast->Next=nvp; else FirstViewport=nvp;
927 	// Recalculate viewports
928 	RecalculateViewports();
929 	// Viewports start off at centered position
930 	nvp->CenterPosition();
931 	// Initial player zoom values to viewport (in case they were set early in InitializePlayer, loaded from savegame, etc.)
932 	C4Player *plr = ::Players.Get(iPlayer);
933 	if (plr)
934 	{
935 		plr->ZoomToViewport(nvp, true, false, false);
936 		plr->ZoomLimitsToViewport(nvp);
937 	}
938 	// Action sound
939 	if (GetViewportCount()!=iLastCount) if (!fSilent)
940 			StartSoundEffect("UI::CloseViewport");
941 	return true;
942 }
943 
DisableFoW()944 void C4ViewportList::DisableFoW()
945 {
946 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next)
947 		cvp->DisableFoW();
948 }
949 
EnableFoW()950 void C4ViewportList::EnableFoW()
951 {
952 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next)
953 		cvp->EnableFoW();
954 }
955 
ClearPointers(C4Object * pObj)956 void C4ViewportList::ClearPointers(C4Object *pObj)
957 {
958 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next)
959 		cvp->ClearPointers(pObj);
960 }
CloseViewport(int32_t iPlayer,bool fSilent)961 bool C4ViewportList::CloseViewport(int32_t iPlayer, bool fSilent)
962 {
963 	// Close all matching viewports
964 	int32_t iLastCount = GetViewportCount();
965 	C4Viewport *next,*prev=nullptr;
966 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=next)
967 	{
968 		next=cvp->Next;
969 		if (cvp->Player==iPlayer || (iPlayer==NO_OWNER && cvp->fIsNoOwnerViewport))
970 		{
971 			delete cvp;
972 			if (prev) prev->Next=next;
973 			else FirstViewport=next;
974 		}
975 		else
976 			prev=cvp;
977 	}
978 	// Anything was done?
979 	if (GetViewportCount()!=iLastCount)
980 	{
981 		// Recalculate viewports
982 		RecalculateViewports();
983 		// Action sound
984 		if (!fSilent) StartSoundEffect("UI::CloseViewport");
985 	}
986 	return true;
987 }
988 
RecalculateViewports()989 void C4ViewportList::RecalculateViewports()
990 {
991 
992 	// Fullscreen only
993 	if (Application.isEditor) return;
994 
995 	// Sort viewports
996 	SortViewportsByPlayerControl();
997 
998 	// Viewport area
999 	int32_t iBorderTop = 0;
1000 	if (Config.Graphics.UpperBoard)
1001 		iBorderTop = C4UpperBoardHeight;
1002 	ViewportArea.Set(FullScreen.pSurface,0,iBorderTop, C4GUI::GetScreenWdt(), C4GUI::GetScreenHgt()-iBorderTop);
1003 
1004 	// Redraw flag
1005 	::GraphicsSystem.InvalidateBg();
1006 #ifdef _WIN32
1007 	// reset mouse clipping
1008 	ClipCursor(nullptr);
1009 #else
1010 	// StdWindow handles this.
1011 #endif
1012 	// reset GUI dlg pos
1013 	::pGUI->SetPreferredDlgRect(C4Rect(ViewportArea.X, ViewportArea.Y, ViewportArea.Wdt, ViewportArea.Hgt));
1014 
1015 	// fullscreen background: First, cover all of screen
1016 	BackgroundAreas.Clear();
1017 	BackgroundAreas.AddRect(C4Rect(ViewportArea.X, ViewportArea.Y, ViewportArea.Wdt, ViewportArea.Hgt));
1018 
1019 	// Viewports
1020 	C4Viewport *cvp;
1021 	int32_t iViews = 0;
1022 	for (cvp=FirstViewport; cvp; cvp=cvp->Next) iViews++;
1023 	if (!iViews) return;
1024 	int32_t iViewsH = (int32_t) sqrt(float(iViews));
1025 	int32_t iViewsX = iViews / iViewsH;
1026 	int32_t iViewsL = iViews % iViewsH;
1027 	int32_t cViewH,cViewX,ciViewsX;
1028 	int32_t cViewWdt,cViewHgt,cOffWdt,cOffHgt,cOffX,cOffY;
1029 	cvp=FirstViewport;
1030 	for (cViewH=0; cViewH<iViewsH; cViewH++)
1031 	{
1032 		ciViewsX = iViewsX; if (cViewH<iViewsL) ciViewsX++;
1033 		for (cViewX=0; cViewX<ciViewsX; cViewX++)
1034 		{
1035 			cViewWdt = ViewportArea.Wdt/ciViewsX;
1036 			cViewHgt = ViewportArea.Hgt/iViewsH;
1037 			cOffX = ViewportArea.X;
1038 			cOffY = ViewportArea.Y;
1039 			cOffWdt = cOffHgt = 0;
1040 			if (ciViewsX * cViewWdt < ViewportArea.Wdt)
1041 				cOffX = (ViewportArea.Wdt - ciViewsX * cViewWdt) / 2;
1042 			if (iViewsH * cViewHgt < ViewportArea.Hgt)
1043 				cOffY = (ViewportArea.Hgt - iViewsH * cViewHgt) / 2 + ViewportArea.Y;
1044 			if (Config.Graphics.SplitscreenDividers)
1045 			{
1046 				if (cViewX < ciViewsX - 1) cOffWdt=4;
1047 				if (cViewH < iViewsH - 1) cOffHgt=4;
1048 			}
1049 			int32_t coViewWdt=cViewWdt-cOffWdt;
1050 			int32_t coViewHgt=cViewHgt-cOffHgt;
1051 			C4Rect rcOut(cOffX+cViewX*cViewWdt, cOffY+cViewH*cViewHgt, coViewWdt, coViewHgt);
1052 			cvp->SetOutputSize(rcOut.x,rcOut.y,rcOut.x,rcOut.y,rcOut.Wdt,rcOut.Hgt);
1053 			cvp=cvp->Next;
1054 			// clip down area avaiable for background drawing
1055 			BackgroundAreas.ClipByRect(rcOut);
1056 		}
1057 	}
1058 	// and finally recalculate script menus
1059 	if (::Game.ScriptGuiRoot)
1060 		::Game.ScriptGuiRoot->RequestLayoutUpdate();
1061 }
1062 
GetViewportCount()1063 int32_t C4ViewportList::GetViewportCount()
1064 {
1065 	int32_t iResult = 0;
1066 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next) iResult++;
1067 	return iResult;
1068 }
1069 
GetViewport(int32_t iPlayer,C4Viewport * pPrev)1070 C4Viewport* C4ViewportList::GetViewport(int32_t iPlayer, C4Viewport* pPrev)
1071 {
1072 	for (C4Viewport *cvp=pPrev ? pPrev->Next : FirstViewport; cvp; cvp=cvp->Next)
1073 		if (cvp->Player==iPlayer || (iPlayer==NO_OWNER && cvp->fIsNoOwnerViewport))
1074 			return cvp;
1075 	return nullptr;
1076 }
1077 
GetAudibility(int32_t iX,int32_t iY,int32_t * iPan,int32_t iAudibilityRadius,int32_t * outPlayer)1078 int32_t C4ViewportList::GetAudibility(int32_t iX, int32_t iY, int32_t *iPan, int32_t iAudibilityRadius, int32_t *outPlayer)
1079 {
1080 	// default audibility radius
1081 	if (!iAudibilityRadius) iAudibilityRadius = C4AudibilityRadius;
1082 	// Accumulate audibility by viewports
1083 	int32_t iAudible=0; *iPan = 0;
1084 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next)
1085 	{
1086 		float distanceToCenterOfViewport = Distance(cvp->GetViewCenterX(),cvp->GetViewCenterY(),iX,iY);
1087 		int32_t audibility = Clamp<int32_t>(100 - 100 * distanceToCenterOfViewport / C4AudibilityRadius, 0, 100);
1088 		if (audibility > iAudible)
1089 		{
1090 			iAudible = audibility;
1091 			if (outPlayer) *outPlayer = cvp->Player;
1092 		}
1093 		*iPan += (iX-(cvp->GetViewCenterX())) / 5;
1094 	}
1095 	*iPan = Clamp<int32_t>(*iPan, -100, 100);
1096 	return iAudible;
1097 }
1098 
SortViewportsByPlayerControl()1099 void C4ViewportList::SortViewportsByPlayerControl()
1100 {
1101 
1102 	// Sort
1103 	bool fSorted;
1104 	C4Player *pPlr1,*pPlr2;
1105 	C4Viewport *pView,*pNext,*pPrev;
1106 	do
1107 	{
1108 		fSorted = true;
1109 		for (pPrev=nullptr,pView=FirstViewport; pView && (pNext = pView->Next); pView=pNext)
1110 		{
1111 			// Get players
1112 			pPlr1 = ::Players.Get(pView->Player);
1113 			pPlr2 = ::Players.Get(pNext->Player);
1114 			// Swap order
1115 			if (pPlr1 && pPlr2 && pPlr1->ControlSet && pPlr2->ControlSet && ( pPlr1->ControlSet->GetLayoutOrder() > pPlr2->ControlSet->GetLayoutOrder() ))
1116 			{
1117 				if (pPrev) pPrev->Next = pNext; else FirstViewport = pNext;
1118 				pView->Next = pNext->Next;
1119 				pNext->Next = pView;
1120 				pPrev = pNext;
1121 				pNext = pView;
1122 				fSorted = false;
1123 			}
1124 			// Don't swap
1125 			else
1126 			{
1127 				pPrev = pView;
1128 			}
1129 		}
1130 	}
1131 	while (!fSorted);
1132 
1133 }
1134 
ViewportNextPlayer()1135 bool C4ViewportList::ViewportNextPlayer()
1136 {
1137 	// safety: switch valid?
1138 	if ((!Game.C4S.Head.Film || !Game.C4S.Head.Replay) && !GetViewport(NO_OWNER)) return false;
1139 	// do switch then
1140 	C4Viewport *vp = GetFirstViewport();
1141 	if (!vp) return false;
1142 	vp->NextPlayer();
1143 	return true;
1144 }
1145 
FreeScroll(C4Vec2D vScrollBy)1146 bool C4ViewportList::FreeScroll(C4Vec2D vScrollBy)
1147 {
1148 	// safety: move valid?
1149 	if ((!Game.C4S.Head.Replay || !Game.C4S.Head.Film) && !GetViewport(NO_OWNER)) return false;
1150 	C4Viewport *vp = GetFirstViewport();
1151 	if (!vp) return false;
1152 	// move then (old static code crap...)
1153 	static int32_t vp_vx=0; static int32_t vp_vy=0; static int32_t vp_vf=0;
1154 	int32_t dx=vScrollBy.x; int32_t dy=vScrollBy.y;
1155 	if (Game.FrameCounter-vp_vf < 5)
1156 		{ dx += vp_vx; dy += vp_vy; }
1157 	vp_vx=dx; vp_vy=dy; vp_vf=Game.FrameCounter;
1158 	vp->ScrollView(dx, dy);
1159 	return true;
1160 }
1161 
ViewportZoomOut()1162 bool C4ViewportList::ViewportZoomOut()
1163 {
1164 	for (C4Viewport *vp = FirstViewport; vp; vp = vp->Next) vp->ChangeZoom(1.0f/C4GFX_ZoomStep);
1165 	return true;
1166 }
1167 
ViewportZoomIn()1168 bool C4ViewportList::ViewportZoomIn()
1169 {
1170 	for (C4Viewport *vp = FirstViewport; vp; vp = vp->Next) vp->ChangeZoom(C4GFX_ZoomStep);
1171 	return true;
1172 }
1173 
MouseMoveToViewport(int32_t iButton,int32_t iX,int32_t iY,DWORD dwKeyParam)1174 void C4ViewportList::MouseMoveToViewport(int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam)
1175 {
1176 	// Pass on to mouse controlled viewport
1177 	for (C4Viewport *cvp=FirstViewport; cvp; cvp=cvp->Next)
1178 		if (::MouseControl.IsViewport(cvp))
1179 			::MouseControl.Move( iButton,
1180 			                     Clamp<int32_t>(iX-cvp->OutX,0,cvp->ViewWdt-1),
1181 			                     Clamp<int32_t>(iY-cvp->OutY,0,cvp->ViewHgt-1),
1182 			                     dwKeyParam );
1183 }
1184