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/* Main class to execute the game fullscreen mode */
18
19#include <C4Include.h>
20#include <C4FullScreen.h>
21
22#include <C4Application.h>
23#include <C4UserMessages.h>
24#include <C4Viewport.h>
25#include <C4Gui.h>
26#include <C4Network2.h>
27#include <C4GameDialogs.h>
28#include <C4GamePadCon.h>
29#include <C4Player.h>
30#include <C4GameOverDlg.h>
31#include "C4TextEncoding.h"
32
33#include <format>
34
35#ifdef _WIN32
36#include "res/engine_resource.h"
37
38#include <windowsx.h>
39
40LRESULT APIENTRY FullScreenWinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
41{
42 // Process message
43 switch (uMsg)
44 {
45 case WM_TIMER:
46 if (wParam == SEC1_TIMER) { FullScreen.Sec1Timer(); }
47 return true;
48 case WM_DESTROY:
49 Application.Quit();
50 return 0;
51 case WM_CLOSE:
52 FullScreen.Close();
53 return 0;
54 case WM_KEYUP:
55 if (Game.DoKeyboardInput(wParam, KEYEV_Up, !!(lParam & 0x20000000), Application.IsControlDown(), Application.IsShiftDown(), false, nullptr))
56 return 0;
57 break;
58 case WM_KEYDOWN:
59 if (Game.DoKeyboardInput(wParam, KEYEV_Down, !!(lParam & 0x20000000), Application.IsControlDown(), Application.IsShiftDown(), !!(lParam & 0x40000000), nullptr))
60 return 0;
61 break;
62 case WM_SYSKEYDOWN:
63 if (wParam == VK_MENU) return 0; // ALT
64 if (Game.DoKeyboardInput(wParam, KEYEV_Down, Application.IsAltDown(), Application.IsControlDown(), Application.IsShiftDown(), !!(lParam & 0x40000000), nullptr))
65 {
66 // Remove handled message from queue to prevent Windows "standard" sound for unprocessed system message
67 MSG msg;
68 PeekMessage(&msg, hwnd, 0, 0, PM_REMOVE);
69 return 0;
70 }
71 if (wParam == VK_F10) return 0;
72 break;
73 case WM_CHAR:
74 {
75 char c[2];
76 c[0] = (char)wParam;
77 c[1] = 0;
78 // GUI: forward
79 if (Game.pGUI)
80 if (Game.pGUI->CharIn(c))
81 return 0;
82 return false;
83 }
84 case WM_LBUTTONDOWN:
85 Game.GraphicsSystem.MouseMove(C4MC_Button_LeftDown, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam, nullptr);
86 break;
87 case WM_LBUTTONUP: Game.GraphicsSystem.MouseMove(C4MC_Button_LeftUp, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam, nullptr); break;
88 case WM_RBUTTONDOWN: Game.GraphicsSystem.MouseMove(C4MC_Button_RightDown, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam, nullptr); break;
89 case WM_RBUTTONUP: Game.GraphicsSystem.MouseMove(C4MC_Button_RightUp, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam, nullptr); break;
90 case WM_LBUTTONDBLCLK: Game.GraphicsSystem.MouseMove(C4MC_Button_LeftDouble, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam, nullptr); break;
91 case WM_RBUTTONDBLCLK: Game.GraphicsSystem.MouseMove(C4MC_Button_RightDouble, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam, nullptr); break;
92 case WM_MOUSEMOVE: Game.GraphicsSystem.MouseMove(C4MC_Button_None, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam, nullptr); break;
93 case WM_MOUSEWHEEL:
94 {
95 POINT point = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
96 ScreenToClient(hwnd, &point);
97 Game.GraphicsSystem.MouseMove(C4MC_Button_Wheel, point.x, point.y, wParam, nullptr);
98 break;
99 }
100 // Hide cursor in client area
101 case WM_SETCURSOR:
102 if (LOWORD(lParam) == HTCLIENT)
103 {
104 SetCursor(nullptr);
105 }
106 else
107 {
108 static HCURSOR arrowCursor{nullptr};
109 if (!arrowCursor) arrowCursor = LoadCursor(nullptr, IDC_ARROW);
110 SetCursor(arrowCursor);
111 }
112 break;
113 case WM_SIZE:
114 {
115 const auto width = LOWORD(lParam);
116 const auto height = HIWORD(lParam);
117
118 const auto oldActive = Application.Active;
119 Application.Active = (wParam == SIZE_RESTORED || wParam == SIZE_MAXIMIZED);
120
121 if (width != 0 && height != 0)
122 {
123 const auto &window = *reinterpret_cast<C4FullScreen *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
124 if (window.GetRenderWindow())
125 {
126 ::SetWindowPos(window.GetRenderWindow(), nullptr, 0, 0, width, height, SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOREDRAW | SWP_NOZORDER);
127 }
128
129 Application.SetResolution(width, height);
130 }
131
132 if (Application.DDraw)
133 {
134 if (!oldActive && Application.Active) Application.DDraw->RestoreDeviceObjects();
135 else if (oldActive && !Application.Active) Application.DDraw->InvalidateDeviceObjects();
136 }
137 }
138 break;
139 case WM_ACTIVATEAPP:
140 if (Config.Graphics.UseDisplayMode == DisplayMode::Fullscreen && !wParam)
141 {
142 ShowWindow(hwnd, SW_SHOWMINIMIZED);
143 }
144 return 0;
145 }
146
147 return CStdWindow::DefaultWindowProc(hwnd, uMsg, wParam, lParam);
148}
149
150void C4FullScreen::CharIn(const char *c) { Game.pGUI->CharIn(c); }
151
152bool C4FullScreen::Init(CStdApp *const app, const char *const title, const C4Rect &bounds, CStdWindow *const parent)
153{
154 if (!CStdWindow::Init(app, title, bounds, parent))
155 {
156 return false;
157 }
158
159 RECT rect;
160 GetClientRect(hWindow, &rect);
161
162 hRenderWindow = CreateWindowEx(
163 0,
164 L"STATIC",
165 nullptr,
166 WS_CHILD,
167 0, 0, rect.right - rect.left, rect.bottom - rect.top,
168 hWindow, nullptr, app->hInstance, nullptr);
169
170 ShowWindow(hRenderWindow, SW_SHOW);
171 return true;
172}
173
174void C4FullScreen::Clear()
175{
176 if (hRenderWindow)
177 {
178 DestroyWindow(hRenderWindow);
179 hRenderWindow = nullptr;
180 }
181
182 CStdWindow::Clear();
183}
184
185void C4FullScreen::SetSize(const unsigned int cx, const unsigned int cy)
186{
187 CStdWindow::SetSize(cx, cy);
188
189 if (hRenderWindow)
190 {
191 // Also resize child window
192 SetWindowPos(hRenderWindow, nullptr, 0, 0, cx, cy, SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOCOPYBITS | SWP_NOREDRAW | SWP_NOZORDER);
193 }
194}
195
196WNDCLASSEX C4FullScreen::GetWindowClass(const HINSTANCE instance) const
197{
198 return {
199 .cbSize = sizeof(WNDCLASSEX),
200 .style = CS_DBLCLKS,
201 .lpfnWndProc = &FullScreenWinProc,
202 .cbClsExtra = 0,
203 .cbWndExtra = 0,
204 .hInstance = instance,
205 .hIcon = LoadIcon(instance, MAKEINTRESOURCE(IDI_00_C4X)),
206 .hCursor = nullptr,
207 .hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BACKGROUND),
208 .lpszMenuName = nullptr,
209 .lpszClassName = L"C4FullScreen",
210 .hIconSm = LoadIcon(instance, MAKEINTRESOURCE(IDI_00_C4X))
211 };
212}
213
214#elif defined(USE_X11)
215
216#include <X11/Xlib.h>
217#include <X11/Xutil.h>
218#include <X11/XKBlib.h>
219
220void C4FullScreen::HandleMessage(XEvent &e)
221{
222 // Parent handling
223 CStdWindow::HandleMessage(e);
224
225 switch (e.type)
226 {
227 case KeyPress:
228 {
229 // Do not take into account the state of the various modifiers and locks
230 // we don't need that for keyboard control
231 uint32_t key = XkbKeycodeToKeysym(e.xany.display, e.xkey.keycode, 0, 0);
232 Game.DoKeyboardInput(vk_code: key, eEventType: KEYEV_Down, fAlt: Application.IsAltDown(), fCtrl: Application.IsControlDown(), fShift: Application.IsShiftDown(), fRepeated: false, pForDialog: nullptr);
233 break;
234 }
235 case KeyRelease:
236 {
237 uint32_t key = XkbKeycodeToKeysym(e.xany.display, e.xkey.keycode, 0, 0);
238 Game.DoKeyboardInput(vk_code: key, eEventType: KEYEV_Up, fAlt: e.xkey.state & Mod1Mask, fCtrl: e.xkey.state & ControlMask, fShift: e.xkey.state & ShiftMask, fRepeated: false, pForDialog: nullptr);
239 break;
240 }
241 case ButtonPress:
242 {
243 static int last_left_click, last_right_click;
244 switch (e.xbutton.button)
245 {
246 case Button1:
247 if (timeGetTime() - last_left_click < 400)
248 {
249 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_LeftDouble,
250 iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
251 last_left_click = 0;
252 }
253 else
254 {
255 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_LeftDown,
256 iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
257 last_left_click = timeGetTime();
258 }
259 break;
260 case Button2:
261 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_MiddleDown,
262 iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
263 break;
264 case Button3:
265 if (timeGetTime() - last_right_click < 400)
266 {
267 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_RightDouble,
268 iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
269 last_right_click = 0;
270 }
271 else
272 {
273 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_RightDown,
274 iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
275 last_right_click = timeGetTime();
276 }
277 break;
278 case Button4:
279 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_Wheel,
280 iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state + (short(32) << 16), pVP: nullptr);
281 break;
282 case Button5:
283 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_Wheel,
284 iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state + (short(-32) << 16), pVP: nullptr);
285 break;
286 default:
287 break;
288 }
289 }
290 break;
291 case ButtonRelease:
292 switch (e.xbutton.button)
293 {
294 case Button1:
295 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_LeftUp, iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
296 break;
297 case Button2:
298 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_MiddleUp, iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
299 break;
300 case Button3:
301 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_RightUp, iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
302 break;
303 default:
304 break;
305 }
306 break;
307 case MotionNotify:
308 Game.GraphicsSystem.MouseMove(iButton: C4MC_Button_None, iX: e.xbutton.x, iY: e.xbutton.y, dwKeyParam: e.xbutton.state, pVP: nullptr);
309 break;
310 case FocusIn:
311 Application.Active = true;
312 break;
313 case FocusOut: case UnmapNotify:
314 Application.Active = false;
315 break;
316 case ConfigureNotify:
317 Application.SetResolution(iNewResX: e.xconfigure.width, iNewResY: e.xconfigure.height);
318 break;
319 }
320}
321
322#elif defined(USE_SDL_MAINLOOP)
323// SDL version
324
325namespace
326{
327 void sdlToC4MCBtn(const SDL_MouseButtonEvent &e,
328 int32_t &button)
329 {
330 static int lastLeftClick = 0, lastRightClick = 0;
331
332 button = C4MC_Button_None;
333
334 switch (e.button)
335 {
336 case SDL_BUTTON_LEFT:
337 if (e.state == SDL_PRESSED)
338 if (timeGetTime() - lastLeftClick < 400)
339 {
340 lastLeftClick = 0;
341 button = C4MC_Button_LeftDouble;
342 }
343 else
344 {
345 lastLeftClick = timeGetTime();
346 button = C4MC_Button_LeftDown;
347 }
348 else
349 button = C4MC_Button_LeftUp;
350 break;
351 case SDL_BUTTON_RIGHT:
352 if (e.state == SDL_PRESSED)
353 if (timeGetTime() - lastRightClick < 400)
354 {
355 lastRightClick = 0;
356 button = C4MC_Button_RightDouble;
357 }
358 else
359 {
360 lastRightClick = timeGetTime();
361 button = C4MC_Button_RightDown;
362 }
363 else
364 button = C4MC_Button_RightUp;
365 break;
366 case SDL_BUTTON_MIDDLE:
367 if (e.state == SDL_PRESSED)
368 button = C4MC_Button_MiddleDown;
369 else
370 button = C4MC_Button_MiddleUp;
371 break;
372 }
373 }
374}
375
376#include "StdGL.h"
377
378void C4FullScreen::HandleMessage(SDL_Event &e)
379{
380 switch (e.type)
381 {
382 case SDL_TEXTINPUT:
383 {
384 CharIn(e.text.text);
385 break;
386 }
387 case SDL_KEYDOWN:
388 {
389 Game.DoKeyboardInput(e.key.keysym.scancode, KEYEV_Down,
390 Application.IsAltDown(),
391 Application.IsControlDown(),
392 Application.IsShiftDown(),
393 false, nullptr);
394 break;
395 }
396 case SDL_KEYUP:
397 Game.DoKeyboardInput(e.key.keysym.scancode, KEYEV_Up,
398 Application.IsAltDown(),
399 Application.IsControlDown(),
400 Application.IsShiftDown(), false, nullptr);
401 break;
402 case SDL_MOUSEMOTION:
403 {
404 const auto scale = GetInputScale();
405 Game.GraphicsSystem.MouseMove(C4MC_Button_None, e.motion.x * scale, e.motion.y * scale, Application.GetModifiers(), nullptr);
406 break;
407 }
408 case SDL_MOUSEWHEEL:
409 {
410 const auto scale = GetInputScale();
411 int x, y;
412 SDL_GetMouseState(&x, &y);
413 Game.GraphicsSystem.MouseMove(C4MC_Button_Wheel, x * scale, y * scale, (e.wheel.y * 60) << 16, nullptr);
414 break;
415 }
416 case SDL_MOUSEBUTTONUP:
417 case SDL_MOUSEBUTTONDOWN:
418 {
419 const auto scale = GetInputScale();
420 int32_t button;
421 sdlToC4MCBtn(e.button, button);
422 Game.GraphicsSystem.MouseMove(button, e.button.x * scale, e.button.y * scale, Application.GetModifiers(), nullptr);
423 break;
424 }
425 case SDL_JOYAXISMOTION:
426 case SDL_JOYHATMOTION:
427 case SDL_JOYBALLMOTION:
428 case SDL_JOYBUTTONDOWN:
429 case SDL_JOYBUTTONUP:
430 Application.pGamePadControl->FeedEvent(e);
431 break;
432 case SDL_WINDOWEVENT:
433 switch (e.window.event)
434 {
435 case SDL_WINDOWEVENT_RESIZED:
436 int width, height;
437 SDL_GL_GetDrawableSize(sdlWindow, &width, &height);
438 Application.SetResolution(width, height);
439 break;
440 case SDL_WINDOWEVENT_MINIMIZED:
441 case SDL_WINDOWEVENT_HIDDEN:
442 Application.Active = false;
443 break;
444 case SDL_WINDOWEVENT_SHOWN:
445 case SDL_WINDOWEVENT_EXPOSED:
446 Application.Active = true;
447 }
448 break;
449 }
450}
451
452#endif // _WIN32, USE_X11, USE_SDL_MAINLOOP
453
454#ifndef _WIN32
455void C4FullScreen::CharIn(const char *c)
456{
457 if (Game.pGUI)
458 {
459 Game.pGUI->CharIn(c: TextEncodingConverter.SystemToClonk(input: c).c_str());
460 }
461}
462#endif
463
464C4FullScreen::C4FullScreen()
465{
466 pMenu = nullptr;
467}
468
469C4FullScreen::~C4FullScreen()
470{
471 delete pMenu;
472}
473
474bool C4FullScreen::Init(CStdApp *const app)
475{
476#ifdef _WIN32
477 return Init(app, STD_PRODUCT);
478#else
479 return CStdWindow::Init(app, STD_PRODUCT);
480#endif
481}
482
483void C4FullScreen::Close()
484{
485 if (Game.IsRunning)
486 ShowAbortDlg();
487 else
488 Application.Quit();
489}
490
491void C4FullScreen::Execute()
492{
493 // Execute menu
494 if (pMenu) pMenu->Execute();
495 // Draw
496 Game.GraphicsSystem.Execute();
497}
498
499bool C4FullScreen::ViewportCheck()
500{
501 int iPlrNum; C4Player *pPlr;
502 // Not active
503 if (!Active) return false;
504 // Determine film mode
505 bool fFilm = (Game.C4S.Head.Replay && Game.C4S.Head.Film);
506 // Check viewports
507 switch (Game.GraphicsSystem.GetViewportCount())
508 {
509 // No viewports: create no-owner viewport
510 case 0:
511 iPlrNum = NO_OWNER;
512 // Film mode: create viewport for first player (instead of no-owner)
513 if (fFilm)
514 if (pPlr = Game.Players.First)
515 iPlrNum = pPlr->Number;
516 // Create viewport
517 Game.CreateViewport(iPlayer: iPlrNum, fSilent: iPlrNum == NO_OWNER);
518 // Non-film (observer mode)
519 if (!fFilm)
520 {
521 // Activate mouse control
522 Game.MouseControl.Init(iPlayer: iPlrNum);
523 // Display message for how to open observer menu (this message will be cleared if any owned viewport opens)
524 const std::string key{std::format(fmt: "<c ffff00><{}></c>", args: Game.KeyboardInput.GetKeyCodeNameByKeyName(szKeyName: "FullscreenMenuOpen", fShort: false))};
525 Game.GraphicsSystem.FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_PRESSORPUSHANYGAMEPADBUTT, args: key).c_str());
526 }
527 break;
528 // One viewport: do nothing
529 case 1:
530 break;
531 // More than one viewport: remove all no-owner viewports
532 default:
533 Game.GraphicsSystem.CloseViewport(iPlayer: NO_OWNER, fSilent: true);
534 break;
535 }
536 // Look for no-owner viewport
537 C4Viewport *pNoOwnerVp = Game.GraphicsSystem.GetViewport(iPlayer: NO_OWNER);
538 // No no-owner viewport found
539 if (!pNoOwnerVp)
540 {
541 // Close any open fullscreen menu
542 CloseMenu();
543 }
544 // No-owner viewport present
545 else
546 {
547 // movie mode: player present, and no valid viewport assigned?
548 if (Game.C4S.Head.Replay && Game.C4S.Head.Film && (pPlr = Game.Players.First))
549 // assign viewport to joined player
550 pNoOwnerVp->Init(iPlayer: pPlr->Number, fSetTempOnly: true);
551 }
552 // Done
553 return true;
554}
555
556bool C4FullScreen::ShowAbortDlg()
557{
558 // no gui?
559 if (!Game.pGUI) return false;
560 // abort dialog already shown
561 if (C4AbortGameDialog::IsShown()) return false;
562 // not while game over dialog is open
563 if (C4GameOverDlg::IsShown()) return false;
564 // show abort dialog
565 return Game.pGUI->ShowRemoveDlg(pDlg: new C4AbortGameDialog());
566}
567
568bool C4FullScreen::ActivateMenuMain()
569{
570 // Not during game over dialog
571 if (C4GameOverDlg::IsShown()) return false;
572 // Close previous
573 CloseMenu();
574 // Open menu
575 pMenu = new C4MainMenu();
576 return pMenu->ActivateMain(iPlayer: NO_OWNER);
577}
578
579void C4FullScreen::CloseMenu()
580{
581 if (pMenu && pMenu->IsActive()) pMenu->Close(fOK: false);
582 delete pMenu;
583 pMenu = nullptr;
584}
585
586bool C4FullScreen::MenuKeyControl(uint8_t byCom)
587{
588 if (pMenu) return pMenu->KeyControl(byCom);
589 return false;
590}
591