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/* Mouse input */
18
19#include <C4Include.h>
20#include <C4MouseControl.h>
21
22#include <C4Viewport.h>
23#include <C4Object.h>
24#include <C4Command.h>
25#include <C4Application.h>
26#include <C4FullScreen.h>
27#include <C4Gui.h>
28#include <C4Wrappers.h>
29#include <C4Player.h>
30#include "C4ChatDlg.h"
31
32#include <format>
33
34const int32_t C4MC_Drag_None = 0,
35 C4MC_Drag_Selecting = 1,
36 C4MC_Drag_Moving = 2,
37 C4MC_Drag_Construct = 5,
38
39 C4MC_Selecting_Unknown = 0,
40 C4MC_Selecting_Crew = 1,
41 C4MC_Selecting_Objects = 2;
42
43const int32_t C4MC_Cursor_Region = 0,
44 C4MC_Cursor_Crosshair = 1,
45 C4MC_Cursor_Enter = 2,
46 C4MC_Cursor_Grab = 3,
47 C4MC_Cursor_Chop = 4,
48 C4MC_Cursor_Dig = 5,
49 C4MC_Cursor_Build = 6,
50 C4MC_Cursor_Select = 7,
51 C4MC_Cursor_Object = 8,
52 C4MC_Cursor_Ungrab = 9,
53 C4MC_Cursor_Up = 10,
54 C4MC_Cursor_Down = 11,
55 C4MC_Cursor_Left = 12,
56 C4MC_Cursor_Right = 13,
57 C4MC_Cursor_UpLeft = 14,
58 C4MC_Cursor_UpRight = 15,
59 C4MC_Cursor_DownLeft = 16,
60 C4MC_Cursor_DownRight = 17,
61 C4MC_Cursor_JumpLeft = 18,
62 C4MC_Cursor_JumpRight = 19,
63 C4MC_Cursor_Drop = 20,
64 C4MC_Cursor_ThrowRight = 21,
65 C4MC_Cursor_Put = 22,
66 C4MC_Cursor_Vehicle = 24,
67 C4MC_Cursor_VehiclePut = 25,
68 C4MC_Cursor_ThrowLeft = 26,
69 C4MC_Cursor_Point = 27,
70 C4MC_Cursor_DigObject = 28,
71 C4MC_Cursor_Help = 29,
72 C4MC_Cursor_DigMaterial = 30,
73 C4MC_Cursor_Add = 31,
74 C4MC_Cursor_Construct = 32,
75 C4MC_Cursor_Attack = 33,
76 C4MC_Cursor_Nothing = 34;
77
78const int32_t C4MC_Time_on_Target = 10;
79
80C4MouseControl::C4MouseControl()
81{
82 Default();
83}
84
85C4MouseControl::~C4MouseControl()
86{
87 Clear();
88}
89
90void C4MouseControl::Default()
91{
92 Active = false;
93 Player = NO_OWNER;
94 pPlayer = nullptr;
95 Viewport = nullptr;
96 Cursor = DownCursor = 0;
97 Caption.clear();
98 IsHelpCaption = false;
99 CaptionBottomY = 0;
100 VpX = VpY = X = Y = DownX = DownY = DownOffsetX = DownOffsetY = ViewX = ViewY = 0;
101 ShowPointX = ShowPointY = -1;
102 LeftButtonDown = RightButtonDown = false;
103 LeftDoubleIgnoreUp = false;
104 Visible = true;
105 InitCentered = false;
106 Help = false;
107 FogOfWar = false;
108 DragID = C4ID_None;
109 KeepCaption = 0;
110 Drag = C4MC_Drag_None; DragSelecting = C4MC_Selecting_Unknown;
111 Selection.Default();
112 TargetObject = DownTarget = nullptr;
113 TimeOnTargetObject = 0;
114 ControlDown = false;
115 ShiftDown = false;
116 Scrolling = false;
117 ScrollSpeed = 10;
118 TargetRegion = nullptr;
119 DownRegion.Default();
120 DragImage.Default();
121 DragImagePhase = 0;
122 fMouseOwned = true; // default mouse owned
123 fctViewport.Default();
124}
125
126void C4MouseControl::Clear()
127{
128 Active = false;
129 Selection.Clear();
130 UpdateClip(); // reset mouse clipping!
131}
132
133void C4MouseControl::Execute()
134{
135 if (!Active || !fMouseOwned) return;
136
137 // Scrolling/continuous update
138 if (Scrolling || !Tick5)
139 {
140 uint16_t wKeyState = 0;
141 if (Application.IsControlDown()) wKeyState |= MK_CONTROL;
142 if (Application.IsShiftDown()) wKeyState |= MK_SHIFT;
143 Move(iButton: C4MC_Button_None, iX: VpX, iY: VpY, dwKeyFlags: wKeyState);
144 }
145}
146
147bool C4MouseControl::Init(int32_t iPlayer)
148{
149 Clear();
150 Default();
151 Active = true;
152 Player = iPlayer;
153 InitCentered = false;
154 UpdateClip();
155 return true;
156}
157
158void C4MouseControl::ClearPointers(C4Object *pObj)
159{
160 if (TargetObject == pObj) TargetObject = nullptr;
161 if (DownTarget == pObj) DownTarget = nullptr;
162 Selection.ClearPointers(pObj);
163}
164
165bool C4MouseControl::IsViewport(C4Viewport *pViewport)
166{
167 return (Viewport == pViewport);
168}
169
170void C4MouseControl::UpdateClip()
171{
172#ifndef NDEBUG
173 // never in debug
174 return;
175#endif
176#ifdef _WIN32
177 // fullscreen only
178 if (!Application.isFullScreen) return;
179 // application or mouse control not active? remove any clips
180 if (!Active || !Application.Active || (Game.pGUI && Game.pGUI->HasMouseFocus())) { ClipCursor(nullptr); return; }
181 // get controlled viewport
182 C4Viewport *pVP = Game.GraphicsSystem.GetViewport(Player);
183 if (!pVP) { ClipCursor(nullptr); return; }
184 // adjust size by viewport size
185 const auto scale = Application.GetScale();
186 RECT vpRct;
187 vpRct.left = static_cast<int32_t>(floorf(pVP->OutX * scale)); vpRct.top = static_cast<int32_t>(floorf(pVP->OutY * scale)); vpRct.right = static_cast<int32_t>(ceilf((pVP->OutX + pVP->ViewWdt) * scale)); vpRct.bottom = static_cast<int32_t>(ceilf((pVP->OutY + pVP->ViewHgt) * scale));
188 // adjust by window pos
189 RECT rtWindow;
190 if (GetWindowRect(FullScreen.hWindow, &rtWindow))
191 {
192 vpRct.left += rtWindow.left; vpRct.top += rtWindow.top;
193 vpRct.right += rtWindow.left; vpRct.bottom += rtWindow.top;
194 }
195 ClipCursor(&vpRct);
196 // and inform GUI
197 if (Game.pGUI)
198 Game.pGUI->SetPreferredDlgRect(C4Rect(pVP->OutX, pVP->OutY, pVP->ViewWdt, pVP->ViewHgt));
199#endif
200 // StdWindow manages this.
201}
202
203void C4MouseControl::Move(int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyFlags, bool fCenter)
204{
205 // Active
206 if (!Active || !fMouseOwned) return;
207 // Execute caption
208 if (KeepCaption) KeepCaption--; else { Caption.clear(); IsHelpCaption = false; CaptionBottomY = 0; }
209 // Check player
210 if ((Player > NO_OWNER) && !(pPlayer = Game.Players.Get(iPlayer: Player))) { Active = false; return; }
211 // Check viewport
212 if (!(Viewport = Game.GraphicsSystem.GetViewport(iPlayer: Player))) return;
213 // get view position
214 C4Rect rcViewport = Viewport->GetOutputRect();
215 fctViewport.Set(nsfc: nullptr, nx: rcViewport.x, ny: rcViewport.y, nwdt: rcViewport.Wdt, nhgt: rcViewport.Hgt);
216 ViewX = Viewport->ViewX; ViewY = Viewport->ViewY;
217 // First time viewport attachment: center mouse
218#ifdef _WIN32
219 if (!InitCentered || fCenter)
220 {
221 iX = Viewport->ViewWdt / 2;
222 iY = Viewport->ViewHgt / 2;
223 if (Application.isFullScreen)
224 {
225 int32_t iMidX = Viewport->OutX + iX;
226 int32_t iMidY = Viewport->OutY + iY;
227 RECT rtWindow;
228 if (GetWindowRect(Application.pWindow->hWindow, &rtWindow))
229 {
230 iMidX += rtWindow.left; iMidY += rtWindow.top;
231 }
232 SetCursorPos(iMidX, iMidY);
233 }
234 InitCentered = true;
235 }
236#else
237 if (!InitCentered || fCenter)
238 {
239 iX = Viewport->ViewWdt / 2;
240 iY = Viewport->ViewHgt / 2;
241 InitCentered = true;
242 }
243#endif
244 // passive mode: scrolling and player buttons only
245 if (IsPassive())
246 {
247 if (iButton != C4MC_Button_Wheel)
248 {
249 VpX = iX; VpY = iY; X = ViewX + VpX; Y = ViewY + VpY;
250 }
251 UpdateTargetRegion();
252 UpdateScrolling();
253 if (iButton == C4MC_Button_LeftDown) LeftDown();
254 else if (iButton == C4MC_Button_LeftUp) LeftUp();
255 else UpdateCursorTarget();
256 return;
257 }
258
259 if (iButton != C4MC_Button_Wheel)
260 {
261 // Position
262 VpX = iX; VpY = iY; X = ViewX + VpX; Y = ViewY + VpY;
263 // Control state
264 ControlDown = false; if (dwKeyFlags & MK_CONTROL) ControlDown = true;
265 ShiftDown = false; if (dwKeyFlags & MK_SHIFT) ShiftDown = true;
266 // Target region
267 UpdateTargetRegion();
268 // Scrolling
269 UpdateScrolling();
270 // Fog of war
271 UpdateFogOfWar();
272
273 // Blocked by fog of war: evaluate button up, dragging and region controls only
274 if (FogOfWar && Drag == C4MC_Drag_None && !TargetRegion)
275 {
276 // Left button up
277 if (iButton == C4MC_Button_LeftUp)
278 {
279 LeftButtonDown = false;
280 // End any drag
281 Drag = C4MC_Drag_None;
282 }
283 // Right button up
284 if (iButton == C4MC_Button_RightUp)
285 {
286 RightButtonDown = false;
287 // Evaluate single right click: select next crew
288 if (Drag == C4MC_Drag_None)
289 SendPlayerSelectNext();
290 }
291 }
292 }
293
294 // Move execution by button/drag status
295 switch (iButton)
296 {
297 case C4MC_Button_None:
298 switch (Drag)
299 {
300 case C4MC_Drag_None: DragNone(); break;
301 case C4MC_Drag_Selecting: DragSelect(); break;
302 case C4MC_Drag_Moving: DragMoving(); break;
303 case C4MC_Drag_Construct: DragConstruct(); break;
304 }
305 break;
306
307 case C4MC_Button_LeftDown: LeftDown(); break;
308 case C4MC_Button_LeftUp: LeftUp(); break;
309 case C4MC_Button_LeftDouble: LeftDouble(); break;
310 case C4MC_Button_RightDown: RightDown(); break;
311 case C4MC_Button_RightUp: RightUp(); break;
312
313 case C4MC_Button_Wheel: Wheel(dwFlags: dwKeyFlags); break;
314 }
315}
316
317void C4MouseControl::Draw(C4FacetEx &cgo)
318{
319 int32_t iOffsetX, iOffsetY;
320
321 // Hidden
322 if (!Visible || !fMouseOwned) return;
323
324 // Draw selection
325 if (!IsPassive()) Selection.DrawSelectMark(cgo);
326
327 // Draw control
328 switch (Drag)
329 {
330 case C4MC_Drag_None: case C4MC_Drag_Moving: case C4MC_Drag_Construct:
331 {
332 // Hotspot offset: Usually, hotspot is in center
333 iOffsetX = GfxR->fctMouseCursor.Wdt / 2;
334 iOffsetY = GfxR->fctMouseCursor.Hgt / 2;
335 // Previously, there used to be custom-defined hotspots for all cursors. Calc them in.
336 if (GfxR->fOldStyleCursor)
337 {
338 switch (Cursor)
339 {
340 case C4MC_Cursor_Select: case C4MC_Cursor_Region:
341 iOffsetX = iOffsetY = 0;
342 break;
343 case C4MC_Cursor_Dig: case C4MC_Cursor_DigMaterial:
344 iOffsetX = 0; iOffsetY = GfxR->fctMouseCursor.Hgt;
345 break;
346 case C4MC_Cursor_Construct:
347 // calculated when dragimage is drawn
348 break;
349 }
350 }
351 else
352 {
353 // for new cursors, this hotspot exists for the scrolling cursors only
354 switch (Cursor)
355 {
356 case C4MC_Cursor_Up: iOffsetY += -GfxR->fctMouseCursor.Hgt / 2; break;
357 case C4MC_Cursor_Down: iOffsetY += +GfxR->fctMouseCursor.Hgt / 2; break;
358 case C4MC_Cursor_Left: iOffsetX += -GfxR->fctMouseCursor.Wdt / 2; break;
359 case C4MC_Cursor_Right: iOffsetX += +GfxR->fctMouseCursor.Wdt / 2; break;
360 case C4MC_Cursor_UpLeft: iOffsetX += -GfxR->fctMouseCursor.Wdt / 2; iOffsetY += -GfxR->fctMouseCursor.Hgt / 2; break;
361 case C4MC_Cursor_UpRight: iOffsetX += +GfxR->fctMouseCursor.Wdt / 2; iOffsetY += -GfxR->fctMouseCursor.Hgt / 2; break;
362 case C4MC_Cursor_DownLeft: iOffsetX += -GfxR->fctMouseCursor.Wdt / 2; iOffsetY += +GfxR->fctMouseCursor.Hgt / 2; break;
363 case C4MC_Cursor_DownRight: iOffsetX += +GfxR->fctMouseCursor.Wdt / 2; iOffsetY += +GfxR->fctMouseCursor.Hgt / 2; break;
364 }
365 }
366 // Add mark
367 bool fAddMark; fAddMark = false;
368 if (ShiftDown)
369 if ((Cursor != C4MC_Cursor_Region) && (Cursor != C4MC_Cursor_Select)
370 && (Cursor != C4MC_Cursor_JumpLeft) && (Cursor != C4MC_Cursor_JumpRight))
371 if (!IsPassive())
372 fAddMark = true;
373
374 const auto scale = Application.GetScale();
375 const auto inverseScale = 1 / scale;
376 C4DrawTransform transform;
377 transform.SetMoveScale(dx: 0.f, dy: 0.f, sx: inverseScale, sy: inverseScale);
378 // Drag image
379 if (DragImage.Surface)
380 {
381 // draw in special modulation mode
382 Application.DDraw->SetBlitMode(C4GFXBLIT_MOD2);
383 // draw DragImage in red or green, according to the phase to be used
384 iOffsetX = DragImage.Wdt / 2; iOffsetY = DragImage.Hgt;
385 DragImage.DrawClrMod(sfcTarget: cgo.Surface, iX: cgo.X + VpX - iOffsetX, iY: cgo.Y + VpY - iOffsetY, iPhaseX: 0, iPhaseY: 0, dwModClr: DragImagePhase ? 0x8f7f0000 : 0x1f007f00);
386 Application.DDraw->SetBlitMode(0);
387 }
388 // Cursor
389 else
390 GfxR->fctMouseCursor.DrawT(sfcTarget: cgo.Surface, iX: static_cast<int>((cgo.X + VpX) * scale - iOffsetX), iY: static_cast<int>((cgo.Y + VpY) * scale - iOffsetY), iPhaseX: Cursor, iPhaseY: 0, pTransform: &transform, noScalingCorrection: true);
391 // Point
392 if ((ShowPointX != -1) && (ShowPointY != -1))
393 GfxR->fctMouseCursor.DrawT(sfcTarget: cgo.Surface,
394 iX: static_cast<int>((cgo.X + ShowPointX - cgo.TargetX) * scale - GfxR->fctMouseCursor.Wdt / 2),
395 iY: static_cast<int>((cgo.Y + ShowPointY - cgo.TargetY) * scale - GfxR->fctMouseCursor.Hgt / 2),
396 iPhaseX: C4MC_Cursor_Point, iPhaseY: 0, pTransform: &transform, noScalingCorrection: true);
397 // Add mark
398 if (fAddMark)
399 GfxR->fctMouseCursor.DrawT(sfcTarget: cgo.Surface,
400 iX: static_cast<int>((cgo.X + VpX) * scale - iOffsetX + 8),
401 iY: static_cast<int>((cgo.Y + VpY) * scale - iOffsetY + 8),
402 iPhaseX: C4MC_Cursor_Add, iPhaseY: 0, pTransform: &transform, noScalingCorrection: true);
403 break;
404 }
405
406 case C4MC_Drag_Selecting:
407 // Draw frame
408 Application.DDraw->DrawFrame(sfcDest: cgo.Surface,
409 x1: cgo.X + VpX,
410 y1: cgo.Y + VpY,
411 x2: DownX + cgo.X - cgo.TargetX,
412 y2: DownY + cgo.Y - cgo.TargetY,
413 col: CRed);
414 break;
415 }
416
417 // Draw caption
418 if (!Caption.empty())
419 {
420 if (IsHelpCaption && Game.pGUI)
421 {
422 // Help: Tooltip style
423 C4FacetEx cgoTip; cgoTip = static_cast<const C4Facet &>(cgo);
424 C4GUI::Screen::DrawToolTip(szTip: Caption.c_str(), cgo&: cgoTip, x: cgo.X + VpX, y: cgo.Y + VpY);
425 }
426 else
427 {
428 // Otherwise red mouse control style
429 int32_t iWdt, iHgt;
430 Game.GraphicsResource.FontRegular.GetTextExtent(szText: Caption.c_str(), rsx&: iWdt, rsy&: iHgt, fCheckMarkup: true);
431 Application.DDraw->TextOut(szText: Caption.c_str(), rFont&: Game.GraphicsResource.FontRegular, fZoom: 1.0,
432 sfcDest: cgo.Surface,
433 iTx: cgo.X + BoundBy<int32_t>(bval: VpX, lbound: iWdt / 2 + 1, rbound: cgo.Wdt - iWdt / 2 - 1),
434 iTy: cgo.Y + std::min<int32_t>(a: CaptionBottomY ? CaptionBottomY - iHgt - 1 : VpY + 13, b: cgo.Hgt - iHgt),
435 dwFCol: 0xfaFF0000, byForm: ACenter);
436 }
437 }
438}
439
440void C4MouseControl::UpdateCursorTarget()
441{
442 int32_t iLastCursor = Cursor;
443
444 // Scrolling: no other target
445 if (Scrolling) { TargetObject = nullptr; return; }
446
447 // On target region
448 if (TargetRegion)
449 {
450 TargetObject = nullptr;
451 if (Help) Cursor = C4MC_Cursor_Help;
452 return;
453 }
454
455 // Check player cursor
456 C4Object *pPlrCursor = pPlayer ? pPlayer->Cursor.Object() : nullptr;
457
458 // Target object
459 uint32_t ocf = OCF_Grab | OCF_Chop | OCF_Container | OCF_Construct | OCF_Living | OCF_Carryable | OCF_Container | OCF_Exclusive;
460 if (Help) ocf |= OCF_All;
461 TargetObject = GetTargetObject(iX: X, iY: Y, dwOCF&: ocf);
462 if (TargetObject && FogOfWar && !(TargetObject->Category & C4D_IgnoreFoW)) TargetObject = nullptr;
463
464 // Movement
465 if (!FogOfWar && !IsPassive()) Cursor = C4MC_Cursor_Crosshair;
466
467 // Dig
468 if (!FogOfWar && GBackSolid(x: X, y: Y) && !IsPassive())
469 {
470 Cursor = C4MC_Cursor_Dig;
471 if (ControlDown) Cursor = C4MC_Cursor_DigMaterial;
472 }
473
474 // Target action
475 if (TargetObject && !IsPassive())
476 {
477 // default cursor for object; also set if not in FoW
478 Cursor = C4MC_Cursor_Crosshair;
479 // get position
480 int32_t iObjX, iObjY; TargetObject->GetViewPos(riX&: iObjX, riY&: iObjY, tx: ViewX, ty: ViewY, fctViewport);
481 // Enter (containers)
482 if (ocf & OCF_Container)
483 if (TargetObject->OCF & OCF_Entrance)
484 Cursor = C4MC_Cursor_Enter;
485 // Grab / Ungrab
486 if (ocf & OCF_Grab)
487 {
488 Cursor = C4MC_Cursor_Grab;
489 if (pPlrCursor)
490 if (pPlrCursor->GetProcedure() == DFA_PUSH)
491 if (pPlrCursor->Action.Target == TargetObject)
492 Cursor = C4MC_Cursor_Ungrab;
493 }
494 // Object
495 if (ocf & OCF_Carryable)
496 {
497 Cursor = C4MC_Cursor_Object;
498 if (ocf & OCF_InSolid) Cursor = C4MC_Cursor_DigObject;
499 }
500 // Chop (reduced range)
501 if (ocf & OCF_Chop)
502 if (Inside<int32_t>(ival: X - iObjX, lbound: -TargetObject->Shape.Wdt / 3, rbound: +TargetObject->Shape.Wdt / 3))
503 if (Inside<int32_t>(ival: Y - iObjY, lbound: -TargetObject->Shape.Wdt / 2, rbound: +TargetObject->Shape.Wdt / 3))
504 Cursor = C4MC_Cursor_Chop;
505 // Enter
506 if (ocf & OCF_Entrance)
507 Cursor = C4MC_Cursor_Enter;
508 // Build
509 if (ocf & OCF_Construct) Cursor = C4MC_Cursor_Build;
510 // Select
511 if (ocf & OCF_Alive)
512 if (ValidPlr(plr: Player))
513 if (Game.Players.Get(iPlayer: Player)->ObjectInCrew(tobj: TargetObject))
514 Cursor = C4MC_Cursor_Select;
515 // select custom region
516 if (TargetObject->Category & C4D_MouseSelect)
517 Cursor = C4MC_Cursor_Select;
518 // Attack
519 if (ocf & OCF_Alive)
520 if (TargetObject->GetAlive())
521 if (Hostile(plr1: Player, plr2: TargetObject->Owner))
522 Cursor = C4MC_Cursor_Attack;
523 }
524
525 // Jump - no parallaxity regarded here...
526 if (pPlrCursor)
527 if (!pPlrCursor->Contained)
528 if (pPlrCursor->GetProcedure() == DFA_WALK)
529 if (Inside<int32_t>(ival: Y - pPlrCursor->y, lbound: -25, rbound: -10))
530 {
531 if (Inside<int32_t>(ival: X - pPlrCursor->x, lbound: -15, rbound: -1)) Cursor = C4MC_Cursor_JumpLeft;
532 if (Inside<int32_t>(ival: X - pPlrCursor->x, lbound: +1, rbound: +15)) Cursor = C4MC_Cursor_JumpRight;
533 }
534
535 // Help
536 if (Help)
537 Cursor = C4MC_Cursor_Help;
538 // passive cursor
539 else if (IsPassive())
540 Cursor = C4MC_Cursor_Region;
541
542 // Time on target: caption
543 if (Cursor == iLastCursor)
544 {
545 TimeOnTargetObject++;
546 if (TimeOnTargetObject >= C4MC_Time_on_Target && !KeepCaption)
547 {
548 const char *const targetObjectName{TargetObject ? TargetObject->GetName() : ""};
549 // Target caption by cursor
550 switch (Cursor)
551 {
552 case C4MC_Cursor_Select:
553 SetCaption<C4ResStrTableKey::IDS_CON_SELECT>(nameSource: TargetObject, isDouble: false);
554 break;
555
556 case C4MC_Cursor_JumpLeft:
557 case C4MC_Cursor_JumpRight:
558 SetCaption<C4ResStrTableKey::IDS_CON_JUMP>(false);
559 break;
560
561 case C4MC_Cursor_Grab:
562 SetCaption<C4ResStrTableKey::IDS_CON_GRAB>(nameSource: TargetObject, isDouble: true);
563 break;
564
565 case C4MC_Cursor_Ungrab:
566 SetCaption<C4ResStrTableKey::IDS_CON_UNGRAB>(nameSource: TargetObject, isDouble: true);
567 break;
568
569 case C4MC_Cursor_Build:
570 SetCaption<C4ResStrTableKey::IDS_CON_BUILD>(nameSource: TargetObject, isDouble: true);
571 break;
572
573 case C4MC_Cursor_Chop:
574 SetCaption<C4ResStrTableKey::IDS_CON_CHOP>(nameSource: TargetObject, isDouble: true);
575 break;
576
577 case C4MC_Cursor_Object:
578 SetCaption<C4ResStrTableKey::IDS_CON_COLLECT>(nameSource: TargetObject, isDouble: true);
579 break;
580
581 case C4MC_Cursor_DigObject:
582 SetCaption<C4ResStrTableKey::IDS_CON_DIGOUT>(nameSource: TargetObject, isDouble: true);
583 break;
584
585 case C4MC_Cursor_Enter:
586 SetCaption<C4ResStrTableKey::IDS_CON_ENTER>(nameSource: TargetObject, isDouble: true);
587 break;
588
589 case C4MC_Cursor_Attack:
590 SetCaption<C4ResStrTableKey::IDS_CON_ATTACK>(nameSource: TargetObject, isDouble: true);
591 break;
592
593 case C4MC_Cursor_Help:
594 SetCaption<C4ResStrTableKey::IDS_CON_HELP>(false);
595 break;
596
597 case C4MC_Cursor_DigMaterial:
598 if (MatValid(mat: GBackMat(x: X, y: Y)))
599 {
600 SetCaption<C4ResStrTableKey::IDS_CON_DIGOUT>(nameSource: C4Id2Def(id: Game.Material.Map[GBackMat(x: X, y: Y)].Dig2Object), isDouble: true);
601 }
602 break;
603 }
604 }
605 }
606 else
607 TimeOnTargetObject = 0;
608}
609
610int32_t C4MouseControl::UpdateCrewSelection()
611{
612 Selection.Clear();
613 // Add all active crew objects in drag frame to Selection
614 C4Object *cObj; C4ObjectLink *cLnk;
615 for (cLnk = pPlayer->Crew.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next)
616 if (!cObj->CrewDisabled)
617 {
618 int32_t iObjX, iObjY; cObj->GetViewPos(riX&: iObjX, riY&: iObjY, tx: ViewX, ty: ViewY, fctViewport);
619 if (Inside(ival: iObjX, lbound: (std::min)(a: X, b: DownX), rbound: (std::max)(a: X, b: DownX)))
620 if (Inside(ival: iObjY, lbound: (std::min)(a: Y, b: DownY), rbound: (std::max)(a: Y, b: DownY)))
621 Selection.Add(nObj: cObj, eSort: C4ObjectList::stNone);
622 }
623 return Selection.ObjectCount();
624}
625
626int32_t C4MouseControl::UpdateObjectSelection()
627{
628 Selection.Clear();
629 // Add all collectible objects in drag frame to Selection
630 C4Object *cObj; C4ObjectLink *cLnk;
631 for (cLnk = Game.Objects.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next)
632 if (cObj->Status)
633 if (cObj->OCF & OCF_Carryable)
634 if (!cObj->Contained)
635 {
636 int32_t iObjX, iObjY; cObj->GetViewPos(riX&: iObjX, riY&: iObjY, tx: ViewX, ty: ViewY, fctViewport);
637 if (Inside(ival: iObjX, lbound: (std::min)(a: X, b: DownX), rbound: (std::max)(a: X, b: DownX)))
638 if (Inside(ival: iObjY, lbound: (std::min)(a: Y, b: DownY), rbound: (std::max)(a: Y, b: DownY)))
639 {
640 Selection.Add(nObj: cObj, eSort: C4ObjectList::stNone);
641 if (Selection.ObjectCount() >= 20) break; // max. 20 objects
642 }
643 }
644 return Selection.ObjectCount();
645}
646
647int32_t C4MouseControl::UpdateSingleSelection()
648{
649 // Set single crew selection if cursor on crew (clear prior object selection)
650 if (TargetObject && (Cursor == C4MC_Cursor_Select))
651 {
652 Selection.Clear(); Selection.Add(nObj: TargetObject, eSort: C4ObjectList::stNone);
653 }
654
655 // Cursor has moved off single crew (or target object) selection: clear selection
656 else if (Selection.GetObject())
657 if (Game.Players.Get(iPlayer: Player)->ObjectInCrew(tobj: Selection.GetObject())
658 || (Selection.GetObject()->Category & C4D_MouseSelect))
659 Selection.Clear();
660
661 return Selection.ObjectCount();
662}
663
664void C4MouseControl::UpdateScrolling()
665{
666 // Assume no scrolling
667 Scrolling = false;
668 // No scrolling if on region
669 if (TargetRegion) return;
670 // Scrolling on border
671 if (VpX == 0)
672 {
673 Cursor = C4MC_Cursor_Left; ScrollView(iX: -ScrollSpeed, iY: 0, ViewWdt: Viewport->ViewWdt, ViewHgt: Viewport->ViewHgt); Scrolling = true;
674 }
675 if (VpY == 0)
676 {
677 Cursor = C4MC_Cursor_Up; ScrollView(iX: 0, iY: -ScrollSpeed, ViewWdt: Viewport->ViewWdt, ViewHgt: Viewport->ViewHgt); Scrolling = true;
678 }
679 if (VpX == Viewport->ViewWdt - 1)
680 {
681 Cursor = C4MC_Cursor_Right; ScrollView(iX: +ScrollSpeed, iY: 0, ViewWdt: Viewport->ViewWdt, ViewHgt: Viewport->ViewHgt); Scrolling = true;
682 }
683 if (VpY == Viewport->ViewHgt - 1)
684 {
685 Cursor = C4MC_Cursor_Down; ScrollView(iX: 0, iY: +ScrollSpeed, ViewWdt: Viewport->ViewWdt, ViewHgt: Viewport->ViewHgt); Scrolling = true;
686 }
687 // Set correct cursor
688 if ((VpX == 0) && (VpY == 0)) Cursor = C4MC_Cursor_UpLeft;
689 if ((VpX == Viewport->ViewWdt - 1) && (VpY == 0)) Cursor = C4MC_Cursor_UpRight;
690 if ((VpX == 0) && (VpY == Viewport->ViewHgt - 1)) Cursor = C4MC_Cursor_DownLeft;
691 if ((VpX == Viewport->ViewWdt - 1) && (VpY == Viewport->ViewHgt - 1)) Cursor = C4MC_Cursor_DownRight;
692}
693
694void C4MouseControl::UpdateTargetRegion()
695{
696 // Assume no region
697 TargetRegion = nullptr;
698 // Find region
699 if (!(TargetRegion = Viewport->Regions.Find(iX: VpX, iY: VpY))) return;
700 // Region found: no target object
701 TargetObject = nullptr;
702 // Cursor
703 Cursor = C4MC_Cursor_Region;
704 // Stop drag selecting (reset down cursor, too)
705 if (Drag == C4MC_Drag_Selecting)
706 {
707 Drag = C4MC_Drag_None; DownCursor = C4MC_Cursor_Nothing;
708 }
709 // Caption
710 Caption = TargetRegion->Caption;
711 IsHelpCaption = false;
712 CaptionBottomY = TargetRegion->Y; KeepCaption = 0;
713 // Help region caption by region target object
714 if (Help)
715 if (TargetRegion->Target)
716 {
717 if (TargetRegion->Target->Def->GetDesc())
718 Caption = std::format(fmt: "{}: {}", args: TargetRegion->Target->GetName(), args: TargetRegion->Target->Def->GetDesc());
719 else
720 Caption = TargetRegion->Target->GetName();
721 IsHelpCaption = true;
722 }
723 // MoveOverCom (on region change)
724 static int32_t iLastRegionX, iLastRegionY;
725 if (TargetRegion->MoveOverCom)
726 {
727 if ((TargetRegion->X != iLastRegionX) || (TargetRegion->Y != iLastRegionY))
728 {
729 iLastRegionX = TargetRegion->X; iLastRegionY = TargetRegion->Y;
730
731 // Control queue
732 Game.Input.Add(eType: CID_PlrControl,
733 pCtrl: new C4ControlPlayerControl(Player, TargetRegion->MoveOverCom, TargetRegion->Data));
734 }
735 }
736 else
737 {
738 iLastRegionX = iLastRegionY = -1;
739 }
740}
741
742bool C4MouseControl::UpdatePutTarget(bool fVehicle)
743{
744 // Target object
745 uint32_t ocf = OCF_Container;
746 StdStrBuf sName;
747 if (TargetObject = GetTargetObject(iX: X, iY: Y, dwOCF&: ocf))
748 {
749 // Cursor
750 if (fVehicle) Cursor = C4MC_Cursor_VehiclePut;
751 else Cursor = C4MC_Cursor_Put;
752 // Caption
753 if (Selection.GetObject())
754 {
755 if (Selection.ObjectCount() > 1)
756 // Multiple object name
757 sName.Copy(pnData: std::format(fmt: "{} {}", args: Selection.ObjectCount(), args: LoadResStrChoice(condition: fVehicle, ifTrue: C4ResStrTableKey::IDS_CON_VEHICLES, ifFalse: C4ResStrTableKey::IDS_CON_ITEMS)).c_str());
758 else
759 // Single object name
760 sName.Ref(pnData: Selection.GetObject()->GetName());
761 // Set caption
762 Caption = fVehicle ? LoadResStr(id: C4ResStrTableKey::IDS_CON_VEHICLEPUT, args: sName.getData(), args: TargetObject->GetName()) : LoadResStr(id: C4ResStrTableKey::IDS_CON_PUT, args: sName.getData(), args: TargetObject->GetName());
763 IsHelpCaption = false;
764 }
765 // Put target found
766 return true;
767 }
768
769 return false;
770}
771
772void C4MouseControl::LeftDown()
773{
774 // Set flag
775 LeftButtonDown = true;
776 // Store down values (same MoveRightDown -> use StoreDown)
777 DownX = X; DownY = Y;
778 DownCursor = Cursor;
779 DownTarget = TargetObject;
780 DownRegion.Default();
781 if (TargetRegion)
782 {
783 DownRegion = (*TargetRegion);
784 DownTarget = TargetRegion->Target;
785 DownOffsetX = TargetRegion->X - VpX; DownOffsetY = TargetRegion->Y - VpY;
786
787 // Send Com on mouse button down when using AutoStopControl to send
788 // corresponding release event in LeftUpDragNone. Only do this for normal
789 // coms, single and double coms are handled in LeftUpDragNone.
790 if (!Help && pPlayer && pPlayer->ControlStyle && (DownRegion.Com & (COM_Double | COM_Single)) == 0)
791 SendControl(iCom: DownRegion.Com, iData: DownRegion.Data);
792 }
793}
794
795void C4MouseControl::DragSelect()
796{
797 // don't select into FoW - simply don't update selection
798 if (FogOfWar) return;
799 switch (DragSelecting)
800 {
801 case C4MC_Selecting_Unknown:
802 // Determine selection type
803 if (UpdateCrewSelection()) { DragSelecting = C4MC_Selecting_Crew; break; }
804 if (UpdateObjectSelection()) { DragSelecting = C4MC_Selecting_Objects; break; }
805 break;
806 case C4MC_Selecting_Crew:
807 // Select crew
808 UpdateCrewSelection();
809 break;
810 case C4MC_Selecting_Objects:
811 // Select objects
812 UpdateObjectSelection();
813 break;
814 }
815}
816
817void C4MouseControl::LeftUp()
818{
819 // Update status flag
820 LeftButtonDown = false;
821 // Ignore left up after double click
822 if (LeftDoubleIgnoreUp) { LeftDoubleIgnoreUp = false; return; }
823 // Evaluate by drag status
824 switch (Drag)
825 {
826 case C4MC_Drag_None: LeftUpDragNone(); break;
827 case C4MC_Drag_Selecting: ButtonUpDragSelecting(); break;
828 case C4MC_Drag_Moving: ButtonUpDragMoving(); break;
829 case C4MC_Drag_Construct: ButtonUpDragConstruct(); break;
830 }
831}
832
833void C4MouseControl::DragMoving()
834{
835 ShowPointX = ShowPointY = -1;
836
837 // do not drag objects into FoW
838 if (FogOfWar) { Cursor = C4MC_Cursor_Nothing; return; }
839
840 // Carryable object
841 if (Selection.GetObject() && (Selection.GetObject()->OCF & OCF_Carryable))
842 {
843 // Default object cursor
844 Cursor = C4MC_Cursor_Object;
845 // Check for put target
846 if (ControlDown)
847 if (UpdatePutTarget(fVehicle: false))
848 return;
849 // In liquid: drop
850 if (GBackLiquid(x: X, y: Y))
851 {
852 Cursor = C4MC_Cursor_Drop; return;
853 }
854 // In free: drop or throw
855 if (!GBackSolid(x: X, y: Y))
856 {
857 // Check drop
858 int32_t iX = X, iY = Y;
859 while ((iY < GBackHgt) && !GBackSolid(x: iX, y: iY)) iY++;
860 if (Inside<int32_t>(ival: X - iX, lbound: -5, rbound: +5) && Inside<int32_t>(ival: Y - iY, lbound: -5, rbound: +5))
861 {
862 Cursor = C4MC_Cursor_Drop; return;
863 }
864 // Throwing physical
865 C4Fixed fixThrow = ValByPhysical(iPercent: 400, iPhysical: 50000);
866 if (pPlayer->Cursor) fixThrow = ValByPhysical(iPercent: 400, iPhysical: pPlayer->Cursor->GetPhysical()->Throw);
867 // Preferred throwing direction
868 int32_t iDir = +1; if (pPlayer->Cursor) if (pPlayer->Cursor->x > X) iDir = -1;
869 // Throwing height
870 int32_t iHeight = 20; if (pPlayer->Cursor) iHeight = pPlayer->Cursor->Shape.Hgt;
871 // Check throw
872 if (FindThrowingPosition(iTx: X, iTy: Y, fXDir: fixThrow * iDir, fYDir: -fixThrow, iHeight, rX&: iX, rY&: iY)
873 || FindThrowingPosition(iTx: X, iTy: Y, fXDir: fixThrow * (iDir *= -1), fYDir: -fixThrow, iHeight, rX&: iX, rY&: iY))
874 {
875 Cursor = (iDir == -1) ? C4MC_Cursor_ThrowLeft : C4MC_Cursor_ThrowRight;
876 ShowPointX = iX; ShowPointY = iY;
877 return;
878 }
879 }
880 }
881
882 // Vehicle
883 else
884 {
885 // PushTo
886 Cursor = C4MC_Cursor_Vehicle;
887 // Check for put target
888 if (ControlDown)
889 UpdatePutTarget(fVehicle: true);
890 }
891}
892
893void C4MouseControl::DragNone()
894{
895 // Holding left down
896 if (LeftButtonDown)
897 {
898 switch (Cursor)
899 {
900 // Hold down on region
901 case C4MC_Cursor_Region:
902 if (!Tick5)
903 if (DownRegion.HoldCom)
904 SendControl(iCom: DownRegion.HoldCom);
905 break;
906 }
907 }
908
909 // Button down: begin drag
910 if ((LeftButtonDown || RightButtonDown)
911 && ((Abs(val: X - DownX) > C4MC_DragSensitivity) || (Abs(val: Y - DownY) > C4MC_DragSensitivity)))
912 {
913 // don't begin dragging from FoW; unless it's a menu
914 if (FogOfWar && DownCursor != C4MC_Cursor_Region) return;
915 switch (DownCursor)
916 {
917 // Drag start selecting in landscape
918 case C4MC_Cursor_Crosshair: case C4MC_Cursor_Dig:
919 Selection.Clear();
920 Drag = C4MC_Drag_Selecting; DragSelecting = C4MC_Selecting_Unknown;
921 break;
922 // Drag object from landscape
923 case C4MC_Cursor_Object: case C4MC_Cursor_DigObject:
924 if (DownTarget)
925 {
926 Drag = C4MC_Drag_Moving;
927 // Down target is not part of selection: drag single object
928 if (!Selection.GetLink(pObj: DownTarget))
929 {
930 Selection.Clear(); Selection.Add(nObj: DownTarget, eSort: C4ObjectList::stNone);
931 }
932 }
933 break;
934 // Drag vehicle from landscape
935 case C4MC_Cursor_Grab: case C4MC_Cursor_Ungrab:
936 if (DownTarget)
937 if (DownTarget->Def->Grab == 1)
938 {
939 Drag = C4MC_Drag_Moving; Selection.Clear(); Selection.Add(nObj: DownTarget, eSort: C4ObjectList::stNone);
940 }
941 break;
942 // Drag from region
943 case C4MC_Cursor_Region:
944 // Drag object(s) or vehicle(s)
945 if (DownRegion.Target
946 && ((DownRegion.Target->OCF & OCF_Carryable) || (DownRegion.Target->Def->Grab == 1)) && !FogOfWar)
947 {
948 Drag = C4MC_Drag_Moving;
949 Selection.Clear();
950 // Multiple object selection from container
951 if (RightButtonDown && DownRegion.Target->Contained && (DownRegion.Target->Contained->Contents.ObjectCount(id: DownRegion.Target->id) > 1))
952 {
953 for (C4ObjectLink *cLnk = DownRegion.Target->Contained->Contents.First; cLnk && cLnk->Obj; cLnk = cLnk->Next)
954 if (cLnk->Obj->id == DownRegion.Target->id)
955 Selection.Add(nObj: cLnk->Obj, eSort: C4ObjectList::stNone);
956 }
957 // Single object selection
958 else
959 Selection.Add(nObj: DownRegion.Target, eSort: C4ObjectList::stNone);
960 break;
961 }
962 // Drag id (construction)
963 C4Def *pDef;
964 if (DownRegion.id)
965 if ((pDef = C4Id2Def(id: DownRegion.id)) && pDef->Constructable)
966 {
967 StartConstructionDrag(id: DownRegion.id); break;
968 }
969 break;
970 // Help: no dragging
971 case C4MC_Cursor_Help:
972 break;
973 }
974 }
975
976 // Cursor movement
977 UpdateCursorTarget();
978 // Update selection
979 UpdateSingleSelection();
980}
981
982void C4MouseControl::LeftDouble()
983{
984 // Update status flag
985 LeftButtonDown = false;
986 // Set ignore flag for next left up
987 LeftDoubleIgnoreUp = true;
988 // Evaluate left double by drag status (can only be C4MC_Drag_None really)
989 switch (Drag)
990 {
991 case C4MC_Drag_None:
992 // Double left click (might be on a target)
993 switch (Cursor)
994 {
995 case C4MC_Cursor_Attack: SendCommand(iCommand: C4CMD_Attack, iX: X, iY: Y, pTarget: TargetObject); break;
996 case C4MC_Cursor_Grab: SendCommand(iCommand: C4CMD_Grab, iX: 0, iY: 0, pTarget: TargetObject); break; // grab at zero-offset!
997 case C4MC_Cursor_Ungrab: SendCommand(iCommand: C4CMD_UnGrab, iX: X, iY: Y, pTarget: TargetObject); break;
998 case C4MC_Cursor_Build: SendCommand(iCommand: C4CMD_Build, iX: X, iY: Y, pTarget: TargetObject); break;
999 case C4MC_Cursor_Chop: SendCommand(iCommand: C4CMD_Chop, iX: X, iY: Y, pTarget: TargetObject); break;
1000 case C4MC_Cursor_Enter: SendCommand(iCommand: C4CMD_Enter, iX: X, iY: Y, pTarget: TargetObject); break;
1001 case C4MC_Cursor_Object: case C4MC_Cursor_DigObject: SendCommand(iCommand: C4CMD_Get, iX: 0, iY: 0, pTarget: TargetObject); break;
1002 case C4MC_Cursor_Dig: SendCommand(iCommand: C4CMD_Dig, iX: X, iY: Y, pTarget: nullptr); break;
1003 case C4MC_Cursor_DigMaterial: SendCommand(iCommand: C4CMD_Dig, iX: X, iY: Y, pTarget: nullptr, pTarget2: nullptr, iData: true); break;
1004 }
1005 break;
1006 }
1007}
1008
1009void C4MouseControl::RightDown()
1010{
1011 // Update status flag
1012 RightButtonDown = true;
1013 // Store down values (same MoveLeftDown -> use StoreDown)
1014 DownX = X; DownY = Y;
1015 DownCursor = Cursor;
1016 DownTarget = TargetObject;
1017 DownRegion.Default();
1018 if (TargetRegion)
1019 {
1020 DownRegion = (*TargetRegion);
1021 DownTarget = TargetRegion->Target;
1022 DownOffsetX = TargetRegion->X - VpX; DownOffsetY = TargetRegion->Y - VpY;
1023 }
1024}
1025
1026void C4MouseControl::RightUp()
1027{
1028 // Update status flag
1029 RightButtonDown = false;
1030 // Evaluate by drag status
1031 switch (Drag)
1032 {
1033 case C4MC_Drag_None: RightUpDragNone(); break;
1034 case C4MC_Drag_Selecting: ButtonUpDragSelecting(); break;
1035 case C4MC_Drag_Moving: ButtonUpDragMoving(); break;
1036 case C4MC_Drag_Construct: ButtonUpDragConstruct(); break;
1037 }
1038}
1039
1040void C4MouseControl::Wheel(uint32_t dwFlags)
1041{
1042 short iDelta = static_cast<short>(dwFlags >> 16);
1043
1044 if (iDelta > 0) Game.LocalPlayerControl(iPlayer: Player, iCom: COM_WheelUp);
1045 if (iDelta < 0) Game.LocalPlayerControl(iPlayer: Player, iCom: COM_WheelDown);
1046}
1047
1048bool C4MouseControl::SendControl(int32_t iCom, int32_t iData)
1049{
1050 // Help
1051 if (iCom == COM_Help)
1052 {
1053 Help = true;
1054 return true;
1055 }
1056 // Activate player menu / fullscreen main menu (local control)
1057 if (iCom == COM_PlayerMenu)
1058 {
1059 if (IsPassive() && FullScreen.Active)
1060 FullScreen.ActivateMenuMain();
1061 else
1062 pPlayer->ActivateMenuMain();
1063 return true;
1064 }
1065 // Open chat
1066 if (iCom == COM_Chat)
1067 {
1068 C4ChatDlg::ShowChat();
1069 return true;
1070 }
1071 // other controls not valid in passive mode
1072 if (IsPassive()) return false;
1073 // Player control queue
1074 Game.Input.Add(eType: CID_PlrControl,
1075 pCtrl: new C4ControlPlayerControl(Player, iCom, iData));
1076 // Done
1077 return true;
1078}
1079
1080void C4MouseControl::CreateDragImage(C4ID id)
1081{
1082 // Clear old image
1083 DragImage.Clear(); DragImage.Default();
1084 // Get definition
1085 C4Def *pDef = C4Id2Def(id); if (!pDef) return;
1086 // in newgfx, it's just the base image, drawn differently...
1087 if (pDef->DragImagePicture)
1088 DragImage.Set(nsfc: pDef->Graphics.GetBitmap(), nx: pDef->PictureRect.x, ny: pDef->PictureRect.y, nwdt: pDef->PictureRect.Wdt, nhgt: pDef->PictureRect.Hgt);
1089 else
1090 DragImage = pDef->GetMainFace(pGraphics: &pDef->Graphics);
1091}
1092
1093void C4MouseControl::DragConstruct()
1094{
1095 Cursor = C4MC_Cursor_Construct;
1096 // Check site
1097 DragImagePhase = 1;
1098 if (!FogOfWar && ConstructionCheck(id: DragID, iX: X, iY: Y)) DragImagePhase = 0;
1099}
1100
1101void C4MouseControl::LeftUpDragNone()
1102{
1103 // Player pressed down a region button and uses AutoStopControl. Send now
1104 // a corresponding release event, not caring about where the cursor may have
1105 // been moved.
1106 if (DownCursor == C4MC_Cursor_Region && !Help && pPlayer && pPlayer->ControlStyle && (DownRegion.Com & (COM_Double | COM_Single)) == 0)
1107 {
1108 // + 16 for release, there is no COM_Release to be |ed...
1109 SendControl(iCom: DownRegion.Com + 16, iData: DownRegion.Data);
1110 return;
1111 }
1112
1113 // Single left click (might be on a target)
1114 switch (Cursor)
1115 {
1116 // Region
1117 case C4MC_Cursor_Region:
1118 // Help click on region: ignore
1119 if (Help) break;
1120 // Region com & data
1121 SendControl(iCom: DownRegion.Com, iData: DownRegion.Data);
1122 break;
1123 // Selection
1124 case C4MC_Cursor_Select:
1125 // Crew selection to control queue
1126 if (!IsPassive()) Game.Input.Add(eType: CID_PlrSelect,
1127 pCtrl: new C4ControlPlayerSelect(Player, Selection));
1128 break;
1129 // Jump
1130 case C4MC_Cursor_JumpLeft: case C4MC_Cursor_JumpRight:
1131 SendCommand(iCommand: C4CMD_Jump, iX: X, iY: Y, pTarget: nullptr);
1132 break;
1133 // Help
1134 case C4MC_Cursor_Help:
1135 if (DownTarget)
1136 {
1137 if (DownTarget->Def->GetDesc())
1138 Caption = std::format(fmt: "{}: {}", args: DownTarget->GetName(), args: DownTarget->Def->GetDesc());
1139 else
1140 Caption = DownTarget->GetName();
1141 KeepCaption = Caption.size() / 2;
1142 IsHelpCaption = true;
1143 }
1144 break;
1145 // Nothing
1146 case C4MC_Cursor_Nothing:
1147 break;
1148 // Movement
1149 default:
1150 // MoveTo command to control queue
1151 SendCommand(iCommand: C4CMD_MoveTo, iX: X, iY: Y, pTarget: nullptr);
1152 break;
1153 }
1154 // Clear selection
1155 Selection.Clear();
1156}
1157
1158void C4MouseControl::ButtonUpDragSelecting()
1159{
1160 // Finish drag
1161 Drag = C4MC_Drag_None;
1162 // Crew selection to control queue
1163 if (DragSelecting == C4MC_Selecting_Crew)
1164 {
1165 Game.Input.Add(eType: CID_PlrSelect,
1166 pCtrl: new C4ControlPlayerSelect(Player, Selection));
1167 Selection.Clear();
1168 }
1169}
1170
1171void C4MouseControl::ButtonUpDragMoving()
1172{
1173 // Finish drag
1174 Drag = C4MC_Drag_None;
1175 // Evaluate to command by cursor for each selected object
1176 C4ObjectLink *pLnk; C4Object *pObj;
1177 int32_t iCommand; C4Object *pTarget1, *pTarget2;
1178 int32_t iAddMode; iAddMode = C4P_Command_Set;
1179 int32_t iX = X, iY = Y;
1180 for (pLnk = Selection.First; pLnk && (pObj = pLnk->Obj); pLnk = pLnk->Next)
1181 {
1182 iCommand = C4CMD_None; pTarget1 = pTarget2 = nullptr;
1183 switch (Cursor)
1184 {
1185 case C4MC_Cursor_ThrowLeft: case C4MC_Cursor_ThrowRight:
1186 iCommand = C4CMD_Throw; pTarget1 = pObj; ShowPointX = ShowPointY = -1; break;
1187 case C4MC_Cursor_Drop:
1188 iCommand = C4CMD_Drop; pTarget1 = pObj; break;
1189 case C4MC_Cursor_Put:
1190 iCommand = C4CMD_Put; pTarget1 = TargetObject; iX = 0; iY = 0; pTarget2 = pObj; break;
1191 case C4MC_Cursor_Vehicle:
1192 iCommand = C4CMD_PushTo; pTarget1 = pObj; break;
1193 case C4MC_Cursor_VehiclePut:
1194 iCommand = C4CMD_PushTo; pTarget1 = pObj; pTarget2 = TargetObject; break;
1195 }
1196 // Set first command, append all following
1197 SendCommand(iCommand, iX, iY, pTarget: pTarget1, pTarget2, iData: 0, iAddMode);
1198 iAddMode = C4P_Command_Append;
1199 }
1200 // Clear selection
1201 Selection.Clear();
1202}
1203
1204void C4MouseControl::ButtonUpDragConstruct()
1205{
1206 // Finish drag
1207 Drag = C4MC_Drag_None;
1208 DragImage.Clear(); DragImage.Default();
1209 // Command
1210 if (DragImagePhase == 0) // if ConstructionCheck was okay (check again?)
1211 SendCommand(iCommand: C4CMD_Construct, iX: X, iY: Y, pTarget: nullptr, pTarget2: nullptr, iData: DragID);
1212 // Clear selection (necessary?)
1213 Selection.Clear();
1214}
1215
1216void C4MouseControl::SendCommand(int32_t iCommand, int32_t iX, int32_t iY, C4Object *pTarget, C4Object *pTarget2, int32_t iData, int32_t iAddMode)
1217{
1218 // no commands in passive mode
1219 if (IsPassive()) return;
1220 // no commands if player is eliminated or doesn't exist any more
1221 C4Player *pPlr = Game.Players.Get(iPlayer: Player);
1222 if (!pPlr || pPlr->Eliminated) return;
1223 // User add multiple command mode
1224 if (ShiftDown) iAddMode |= C4P_Command_Append;
1225 // Command to control queue
1226 Game.Input.Add(eType: CID_PlrCommand,
1227 pCtrl: new C4ControlPlayerCommand(Player, iCommand, iX, iY, pTarget, pTarget2, iData, iAddMode));
1228}
1229
1230void C4MouseControl::RightUpDragNone()
1231{
1232 // Region: send control
1233 if (Cursor == C4MC_Cursor_Region)
1234 {
1235 SendControl(iCom: DownRegion.RightCom); return;
1236 }
1237
1238 // Help: end
1239 if (Help)
1240 {
1241 Help = false; KeepCaption = 0; return;
1242 }
1243
1244 // Selection: send selection (not exclusive)
1245 if (Cursor == C4MC_Cursor_Select)
1246 Game.Input.Add(eType: CID_PlrSelect,
1247 pCtrl: new C4ControlPlayerSelect(Player, Selection));
1248
1249 // Check for any secondary context target objects
1250 uint32_t ocf = OCF_All;
1251 if (!TargetObject) TargetObject = GetTargetObject(iX: X, iY: Y, dwOCF&: ocf);
1252 // Avoid stinkin' Windrad - cheaper goes it not
1253 if (TargetObject && (TargetObject->id == C4Id(str: "WWNG"))) TargetObject = GetTargetObject(iX: X, iY: Y, dwOCF&: ocf, pExclude: TargetObject);
1254
1255 // Target object: context menu
1256 if (TargetObject)
1257 {
1258 SendCommand(iCommand: C4CMD_Context, iX: X - Viewport->ViewX, iY: Y - Viewport->ViewY, pTarget: nullptr, pTarget2: TargetObject, iData: 0, iAddMode: C4P_Command_Add);
1259 return;
1260 }
1261
1262 // Free click: select next clonk
1263 SendPlayerSelectNext();
1264}
1265
1266void C4MouseControl::UpdateFogOfWar()
1267{
1268 // Assume no fog of war
1269 FogOfWar = false;
1270 // Check for fog of war
1271 if ((pPlayer->fFogOfWar && !pPlayer->FoWIsVisible(x: X, y: Y)) || X < 0 || Y < 0 || X >= GBackWdt || Y >= GBackHgt)
1272 {
1273 FogOfWar = true;
1274 // allow dragging, scrolling, region selection and manipulations of objects not affected by FoW
1275 if (!TargetRegion && !Scrolling && (!TargetObject || !(TargetObject->Category & C4D_IgnoreFoW)))
1276 {
1277 Cursor = C4MC_Cursor_Nothing;
1278 ShowPointX = ShowPointY = -1;
1279 // dragging will reset the cursor
1280 }
1281 }
1282}
1283
1284void C4MouseControl::SendPlayerSelectNext()
1285{
1286 C4ObjectLink *cLnk;
1287 if (cLnk = pPlayer->Crew.GetLink(pObj: pPlayer->Cursor))
1288 for (cLnk = cLnk->Next; cLnk; cLnk = cLnk->Next)
1289 if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break;
1290 if (!cLnk)
1291 for (cLnk = pPlayer->Crew.First; cLnk; cLnk = cLnk->Next)
1292 if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break;
1293 if (cLnk)
1294 {
1295 // Crew selection to control queue
1296 Selection.Clear(); Selection.Add(nObj: cLnk->Obj, eSort: C4ObjectList::stNone);
1297 Game.Input.Add(eType: CID_PlrSelect,
1298 pCtrl: new C4ControlPlayerSelect(Player, Selection));
1299 Selection.Clear();
1300 }
1301}
1302
1303void C4MouseControl::ShowCursor()
1304{
1305 Visible = true;
1306}
1307
1308void C4MouseControl::HideCursor()
1309{
1310 Visible = false;
1311}
1312
1313const char *C4MouseControl::GetCaption()
1314{
1315 return Caption.empty() ? nullptr : Caption.c_str();
1316}
1317
1318C4Object *C4MouseControl::GetTargetObject(int32_t iX, int32_t iY, uint32_t &dwOCF, C4Object *pExclude)
1319{
1320 // find object
1321 C4Object *pObj = Game.FindVisObject(tx: ViewX, ty: ViewY, iPlr: Player, fctViewport, iX, iY, iWdt: 0, iHgt: 0, ocf: dwOCF, pExclude);
1322 if (!pObj) return nullptr;
1323 // adjust OCF
1324 pObj->GetOCFForPos(ctx: iX, cty: iY, ocf&: dwOCF);
1325 return pObj;
1326}
1327
1328bool C4MouseControl::IsPassive()
1329{
1330 return Game.Control.isReplay() || Player <= NO_OWNER;
1331}
1332
1333void C4MouseControl::ScrollView(int32_t iX, int32_t iY, int32_t ViewWdt, int32_t ViewHgt)
1334{
1335 // player assigned: scroll player view
1336 if (pPlayer)
1337 pPlayer->ScrollView(iX, iY, ViewWdt, ViewHgt);
1338 else if (Viewport)
1339 {
1340 // no player: Scroll fullscreen viewport
1341 Viewport->ViewX = Viewport->ViewX + iX;
1342 Viewport->ViewY = Viewport->ViewY + iY;
1343 Viewport->UpdateViewPosition();
1344 }
1345}
1346
1347bool C4MouseControl::IsDragging()
1348{
1349 // no selection drag; return true for object drag only
1350 return Active && (Drag == C4MC_Drag_Moving || Drag == C4MC_Drag_Construct);
1351}
1352
1353void C4MouseControl::StartConstructionDrag(C4ID id)
1354{
1355 Drag = C4MC_Drag_Construct;
1356 DragID = id;
1357 CreateDragImage(id: DragID);
1358 Selection.Clear();
1359}
1360
1361template<C4ResStrTableKey Id, typename T>
1362void C4MouseControl::SetCaption(T *const nameSource, const bool isDouble)
1363{
1364 Caption = LoadResStr(Id, nameSource ? nameSource->GetName() : "");
1365 if (isDouble)
1366 {
1367 Caption += '|';
1368 Caption += LoadResStr(id: C4ResStrTableKey::IDS_CON_DOUBLECLICK);
1369 }
1370 IsHelpCaption = false;
1371}
1372
1373template<C4ResStrTableKey Id>
1374void C4MouseControl::SetCaption(const bool isDouble)
1375{
1376 Caption = LoadResStr(id: Id);
1377 if (isDouble)
1378 {
1379 Caption += '|';
1380 Caption += LoadResStr(id: C4ResStrTableKey::IDS_CON_DOUBLECLICK);
1381 }
1382 IsHelpCaption = false;
1383}
1384