1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2008, Sven2
6 * Copyright (c) 2017-2022, The LegacyClonk 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// Menus attached to objects; script created or internal
19
20#include <C4Include.h>
21#include <C4ObjectMenu.h>
22
23#include <C4Object.h>
24#include <C4ObjectCom.h>
25#include <C4Wrappers.h>
26#include <C4Player.h>
27#include <C4Viewport.h>
28
29#include <cassert>
30#include <format>
31
32// C4ObjectMenu
33
34C4ObjectMenu::C4ObjectMenu() : C4Menu()
35{
36 Default();
37}
38
39C4ObjectMenu::~C4ObjectMenu()
40{
41 if (ClearObjectPtr)
42 {
43 *ClearObjectPtr = nullptr;
44 }
45}
46
47void C4ObjectMenu::Default()
48{
49 C4Menu::Default();
50 eCallbackType = CB_None;
51 Object = ParentObject = RefillObject = nullptr;
52 RefillObjectContentsCount = 0;
53 UserMenu = false;
54 CloseQuerying = false;
55}
56
57bool C4ObjectMenu::IsCloseDenied()
58{
59 // abort if menu is permanented by script; stop endless recursive calls if user opens a new menu by CloseQuerying-flag
60 if (UserMenu && !CloseQuerying)
61 {
62 CloseQuerying = true;
63 bool fResult = false;
64 C4AulParSet pars(C4VInt(iVal: Selection), C4VObj(pObj: ParentObject));
65 if (eCallbackType == CB_Object)
66 {
67 if (Object) fResult = static_cast<bool>(Object->Call(PSF_MenuQueryCancel, pPars: pars));
68 }
69 else if (eCallbackType == CB_Scenario)
70 fResult = static_cast<bool>(Game.Script.Call(PSF_MenuQueryCancel, pPars: pars));
71 CloseQuerying = false;
72 if (fResult) return true;
73 }
74 // close OK
75 return false;
76}
77
78void C4ObjectMenu::LocalInit(C4Object *pObject, bool fUserMenu)
79{
80 Object = pObject;
81 UserMenu = fUserMenu;
82 ParentObject = GetParentObject();
83 if (pObject) eCallbackType = CB_Object; else eCallbackType = CB_Scenario;
84}
85
86bool C4ObjectMenu::Init(C4FacetExSurface &fctSymbol, const char *szEmpty, C4Object *pObject, int32_t iExtra, int32_t iExtraData, int32_t iId, int32_t iStyle, bool fUserMenu)
87{
88 if (!DoInit(fctSymbol, szEmpty, iExtra, iExtraData, iId, iStyle)) return false;
89 LocalInit(pObject, fUserMenu);
90 return true;
91}
92
93void C4ObjectMenu::OnSelectionChanged(int32_t iNewSelection)
94{
95 // do selection callback
96 if (UserMenu)
97 {
98 C4AulParSet pars(C4VInt(iVal: iNewSelection), C4VObj(pObj: ParentObject));
99 if (eCallbackType == CB_Object && Object)
100 Object->Call(PSF_MenuSelection, pPars: pars);
101 else if (eCallbackType == CB_Scenario)
102 Game.Script.Call(PSF_MenuSelection, pPars: pars);
103 }
104}
105
106void C4ObjectMenu::ClearPointers(C4Object *pObj)
107{
108 if (Object == pObj) { Object = nullptr; }
109 if (ParentObject == pObj) ParentObject = nullptr; // Reason for menu close anyway.
110 if (RefillObject == pObj) RefillObject = nullptr;
111 if (ClearObjectPtr && *ClearObjectPtr == pObj) *ClearObjectPtr = nullptr;
112 C4Menu::ClearPointers(pObj);
113}
114
115C4Object *C4ObjectMenu::GetParentObject()
116{
117 C4Object *cObj; C4ObjectLink *cLnk;
118 for (cLnk = Game.Objects.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next)
119 if (cObj->Menu == this)
120 return cObj;
121 return nullptr;
122}
123
124void C4ObjectMenu::SetRefillObject(C4Object *pObj)
125{
126 RefillObject = pObj;
127 NeedRefill = true;
128 Refill();
129}
130
131bool C4ObjectMenu::DoRefillInternal(bool &rfRefilled)
132{
133 const auto symbolSize = GetSymbolSize();
134
135 // Variables
136 C4FacetExSurface fctSymbol;
137 C4Object *pObj;
138 std::string caption;
139 std::string command;
140 std::string command2;
141 int32_t cnt, iCount;
142 C4Def *pDef;
143 C4Player *pPlayer;
144 C4Object *pTarget;
145 C4Facet fctTarget;
146
147 const auto selectedItem = GetSelectedItem();
148 const auto checkIDSelection = [selectedID = selectedItem ? selectedItem->GetC4ID() : C4ID_None, this]
149 {
150 if (selectedID == C4ID_None) return;
151 if (const auto newSelectedItem = GetSelectedItem(); newSelectedItem && newSelectedItem->GetC4ID() == selectedID)
152 {
153 return;
154 }
155
156 for (std::int32_t i = 0; i < GetItemCount(); ++i)
157 {
158 const auto item = GetItem(iIndex: i);
159 if (item && item->GetC4ID() == selectedID)
160 {
161 Selection = i;
162 return;
163 }
164 }
165 };
166
167 // Refill
168 switch (Identification)
169 {
170 case C4MN_Activate:
171 // Clear items
172 ClearItems();
173 // Refill target
174 if (!(pTarget = RefillObject)) return false;
175 {
176 // Add target contents items
177 C4ObjectListIterator iter(pTarget->Contents);
178 while (pObj = iter.GetNext(piCount: &iCount, dwCategory: C4D_Activate))
179 {
180 pDef = pObj->Def;
181 if (pDef->NoGet) continue;
182 // Prefer fully constructed objects
183 if (~pObj->OCF & OCF_FullCon)
184 {
185 // easy way: only if first concat check matches
186 // this doesn't catch all possibilities, but that will rarely matter
187 C4Object *pObj2 = pTarget->Contents.Find(id: pDef->id, iOwner: ANY_OWNER, dwOCF: OCF_FullCon);
188 if (pObj2) if (pObj2->CanConcatPictureWith(pOtherObject: pObj)) pObj = pObj2;
189 }
190 // Caption
191 caption = LoadResStr(id: C4ResStrTableKey::IDS_MENU_ACTIVATE, args: pObj->GetName());
192 // Picture
193 fctSymbol.Set(nsfc: fctSymbol.Surface, nx: 0, ny: 0, nwdt: symbolSize, nhgt: symbolSize);
194 pObj->Picture2Facet(cgo&: fctSymbol);
195 // Commands
196 command = std::format(fmt: "SetCommand(this,\"Activate\",Object({}))&&ExecuteCommand()", args&: pObj->Number);
197 command2 = std::format(fmt: "SetCommand(this,\"Activate\", ,{},0,Object({}),{})&&ExecuteCommand()", args: pTarget->Contents.ObjectCount(id: pDef->id), args&: pTarget->Number, args: C4IdText(id: pDef->id));
198 // Add menu item
199 Add(szCaption: caption.c_str(), fctSymbol, szCommand: command.c_str(), iCount, pObject: pObj, szInfoCaption: pDef->GetDesc(), idID: pDef->id, szCommand2: command2.c_str(), fOwnValue: true, iValue: pObj->GetValue(pInBase: pTarget, iForPlayer: NO_OWNER));
200 // facet taken over (arrg!)
201 fctSymbol.Default();
202 }
203 checkIDSelection();
204 }
205 break;
206
207 case C4MN_Buy:
208 {
209 // Clear items
210 ClearItems();
211 // Base buying disabled? Fail.
212 if (~Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Buy) return false;
213 // Refill target
214 if (!(pTarget = RefillObject)) return false;
215 // Add base owner's homebase material
216 if (!(pPlayer = Game.Players.Get(iPlayer: pTarget->Base))) return false;
217 C4Player *pBuyPlayer = Object ? Game.Players.Get(iPlayer: Object->Owner) : nullptr;
218 C4ID idDef;
219 for (cnt = 0; idDef = pPlayer->HomeBaseMaterial.GetID(rDefs&: Game.Defs, dwCategory: C4D_All, index: cnt, ipCount: &iCount); cnt++)
220 {
221 pDef = C4Id2Def(id: idDef);
222 if (!pDef) continue; // skip invalid defs
223 // Caption
224 caption = LoadResStr(id: C4ResStrTableKey::IDS_MENU_BUY, args: pDef->GetName());
225 // Picture
226 pDef->Picture2Facet(cgo&: fctSymbol, color: pBuyPlayer ? pBuyPlayer->ColorDw : 0);
227 // Command
228 command = std::format(fmt: "AppendCommand(this,\"Buy\",Object({}),{},0,,0,{})&&ExecuteCommand()", args&: pTarget->Number, args: 1, args: C4IdText(id: pDef->id));
229 command2 = std::format(fmt: "AppendCommand(this,\"Buy\",Object({}),{},0,,0,{})&&ExecuteCommand()", args&: pTarget->Number, args&: iCount, args: C4IdText(id: pDef->id));
230 // Buying value
231 int32_t iBuyValue = pDef->GetValue(pInBase: pTarget, iBuyPlayer: pPlayer->Number);
232 // Add menu item
233 Add(szCaption: caption.c_str(), fctSymbol, szCommand: command.c_str(), iCount, pObject: nullptr, szInfoCaption: pDef->GetDesc(), idID: pDef->id, szCommand2: command2.c_str(), fOwnValue: true, iValue: iBuyValue);
234 }
235 break;
236 }
237
238 case C4MN_Sell:
239 // Clear items
240 ClearItems();
241 // Base sale disabled? Fail.
242 if (~Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Sell) return false;
243 // Refill target
244 if (!(pTarget = RefillObject)) return false;
245 {
246 // Add target contents items
247 C4ObjectListIterator iter(pTarget->Contents);
248 while (pObj = iter.GetNext(piCount: &iCount, dwCategory: C4D_Sell))
249 {
250 pDef = pObj->Def;
251 if (pDef->NoSell) continue;
252 // Prefer fully constructed objects
253 if (~pObj->OCF & OCF_FullCon)
254 {
255 // easy way: only if first concat check matches
256 // this doesn't catch all possibilities, but that will rarely matter
257 C4Object *pObj2 = pTarget->Contents.Find(id: pDef->id, iOwner: ANY_OWNER, dwOCF: OCF_FullCon);
258 if (pObj2) if (pObj2->CanConcatPictureWith(pOtherObject: pObj)) pObj = pObj2;
259 }
260 // Caption
261 caption = LoadResStr(id: C4ResStrTableKey::IDS_MENU_SELL, args: pObj->GetName());
262 // Picture
263 fctSymbol.Set(nsfc: fctSymbol.Surface, nx: 0, ny: 0, nwdt: symbolSize, nhgt: symbolSize);
264 pObj->Picture2Facet(cgo&: fctSymbol);
265 // Commands
266 command = std::format(fmt: "AppendCommand(this,\"Sell\",Object({}),{},0,Object({}),0,{})&&ExecuteCommand()", args&: pTarget->Number, args: 1, args&: pObj->Number, args: C4IdText(id: pDef->id));
267 command2 = std::format(fmt: "AppendCommand(this,\"Sell\",Object({}),{},0,,0,{})&&ExecuteCommand()", args&: pTarget->Number, args: pTarget->Contents.ObjectCount(id: pDef->id), args: C4IdText(id: pDef->id));
268 // Selling value
269 int32_t iSellValue = pObj->GetValue(pInBase: pTarget, iForPlayer: Object ? Object->Owner : NO_OWNER);
270 // Add menu item
271 Add(szCaption: caption.c_str(), fctSymbol, szCommand: command.c_str(), iCount, pObject: nullptr, szInfoCaption: pDef->GetDesc(), idID: pDef->id, szCommand2: command2.c_str(), fOwnValue: true, iValue: iSellValue);
272 fctSymbol.Default();
273 }
274 checkIDSelection();
275 }
276 // Success
277 break;
278
279 case C4MN_Get:
280 case C4MN_Contents:
281 // Clear items
282 ClearItems();
283 // Refill target
284 if (!(pTarget = RefillObject)) return false;
285 {
286 // Add target contents items
287 C4ObjectListIterator iter(pTarget->Contents);
288 while (pObj = iter.GetNext(piCount: &iCount, dwCategory: C4D_Get))
289 {
290 pDef = pObj->Def;
291 if (pDef->NoGet) continue;
292 // Prefer fully constructed objects
293 if (~pObj->OCF & OCF_FullCon)
294 {
295 // easy way: only if first concat check matches
296 // this doesn't catch all possibilities, but that will rarely matter
297 C4Object *pObj2 = pTarget->Contents.Find(id: pDef->id, iOwner: ANY_OWNER, dwOCF: OCF_FullCon);
298 if (pObj2) if (pObj2->CanConcatPictureWith(pOtherObject: pObj)) pObj = pObj2;
299 }
300 // Determine whether to get or activate
301 bool fGet = true;
302 if (!(pObj->OCF & OCF_Carryable)) fGet = false; // not a carryable item
303 if (Identification == C4MN_Contents)
304 {
305 if (Object && Object->Def->CollectionLimit && (Object->Contents.ObjectCount() >= Object->Def->CollectionLimit)) fGet = false; // collection limit reached
306 if (Object && Object->Call(PSF_RejectCollection, pPars: {C4VID(idVal: pObj->Def->id), C4VObj(pObj)})) fGet = false; // collection rejected
307 }
308 if (!(pTarget->OCF & OCF_Entrance)) fGet = true; // target object has no entrance: cannot activate - force get
309 // Caption
310 caption = LoadResStrChoice(condition: fGet, ifTrue: C4ResStrTableKey::IDS_MENU_GET, ifFalse: C4ResStrTableKey::IDS_MENU_ACTIVATE, args: pObj->GetName());
311 // Picture
312 fctSymbol.Set(nsfc: fctSymbol.Surface, nx: 0, ny: 0, nwdt: symbolSize, nhgt: symbolSize);
313 pObj->Picture2Facet(cgo&: fctSymbol);
314 // Primary command: get/activate single object
315 command = std::format(fmt: "SetCommand(this, \"{}\", Object({})) && ExecuteCommand()", args: fGet ? "Get" : "Activate", args&: pObj->Number);
316 // Secondary command: get/activate all objects of the chosen type
317 int32_t iAllCount;
318 if ((iAllCount = pTarget->Contents.ObjectCount(id: pDef->id)) > 1)
319 command2 = std::format(fmt: "SetCommand(this, \"{}\", , {},0, Object({}), {}) && ExecuteCommand()", args: fGet ? "Get" : "Activate", args&: iAllCount, args&: pTarget->Number, args: C4IdText(id: pDef->id));
320 // Add menu item (with object)
321 Add(szCaption: caption.c_str(), fctSymbol, szCommand: command.c_str(), iCount, pObject: pObj, szInfoCaption: pDef->GetDesc(), idID: pDef->id, szCommand2: command2.c_str());
322 fctSymbol.Default();
323 }
324 }
325 checkIDSelection();
326 break;
327
328 case C4MN_Context:
329 {
330 // Clear items
331 ClearItems();
332 if (!(pTarget = RefillObject)) return false;
333 if (!Object) return false;
334
335 // Put - if target is a container...
336 if (pTarget->OCF & OCF_Container)
337 // ...and we have something to put...
338 if (Object->Contents.GetObject(Index: 0))
339 // ...and if we are in that container
340 if ((pTarget == Object->Contained)
341 // ...or if we are grabbing the container and it has grab-put enabled
342 || ((Object->GetProcedure() == DFA_PUSH) && (Object->Action.Target == pTarget) && (pTarget->Def->GrabPutGet & C4D_Grab_Put)))
343 {
344 // Primary command: put first inventory item (all selected clonks)
345 command = std::format(fmt: "PlayerObjectCommand({}, \"Put\", Object({}), 0, 0) && ExecuteCommand()", args&: Object->Owner, args&: pTarget->Number);
346 // Secondary command: put all inventory items (all selected clonks)
347 if ((Object->Contents.ObjectCount() > 1) || (Game.Players.Get(iPlayer: Object->Owner)->GetSelectedCrewCount() > 1))
348 command2 = std::format(fmt: "PlayerObjectCommand({}, \"Put\", Object({}), 1000, 0) && ExecuteCommand()", args&: Object->Owner, args&: pTarget->Number); // Workaround: specifying a really high put count here; will be adjusted later by C4Menu::ObjectCommand...
349 // Create symbol
350 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize);
351 fctTarget = fctSymbol.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top);
352 Object->Contents.GetObject(Index: 0)->DrawPicture(cgo&: fctTarget);
353 fctTarget = fctSymbol.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom);
354 Game.GraphicsResource.fctHand.Draw(cgo&: fctTarget, fAspect: true, iPhaseX: 0);
355 // Add menu item
356 Add(szCaption: LoadResStr(id: C4ResStrTableKey::IDS_CON_PUT2), fctSymbol, szCommand: command.c_str(), iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: nullptr, idID: C4ID_None, szCommand2: command2.c_str());
357 // Preserve symbol
358 fctSymbol.Default();
359 }
360
361 // Contents - if target is a container...
362 if (pTarget->OCF & OCF_Container)
363 // ...and if we are in that container
364 if ((pTarget == Object->Contained)
365 // ...or if we are grabbing the container and it has grab-get enabled
366 || ((Object->GetProcedure() == DFA_PUSH) && (Object->Action.Target == pTarget) && (pTarget->Def->GrabPutGet & C4D_Grab_Get))
367 // ...or if the container is owned by us or a friendly player - this is for remote mouse-button-clicks
368 || (ValidPlr(plr: pTarget->Owner) && !Hostile(plr1: pTarget->Owner, plr2: Object->Owner)))
369 {
370 command = std::format(fmt: "SetCommand(this,\"Get\",Object({}),0,0,,2)&&ExecuteCommand()", args&: pTarget->Number);
371 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize); pTarget->DrawPicture(cgo&: fctSymbol);
372 Add(szCaption: LoadResStr(id: C4ResStrTableKey::IDS_CON_CONTENTS), fctSymbol, szCommand: command.c_str());
373 fctSymbol.Default();
374 }
375
376 // These ought to be moved into a flag/homebase script...
377 // Homebase buy/sell (if friendly base)
378 if (ValidPlr(plr: pTarget->Base) && !Hostile(plr1: pTarget->Base, plr2: Object->Owner))
379 {
380 // Buy
381 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Buy)
382 {
383 command = std::format(fmt: "SetCommand(this,\"Buy\",Object({}))&&ExecuteCommand()", args&: pTarget->Number);
384 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize); DrawMenuSymbol(iMenu: C4MN_Buy, cgo&: fctSymbol, iOwner: pTarget->Base, cObj: pTarget);
385 Add(szCaption: LoadResStr(id: C4ResStrTableKey::IDS_CON_BUY), fctSymbol, szCommand: command.c_str());
386 fctSymbol.Default();
387 }
388 // Sell
389 if (Game.C4S.Game.Realism.BaseFunctionality & BASEFUNC_Sell)
390 {
391 command = std::format(fmt: "SetCommand(this,\"Sell\",Object({}))&&ExecuteCommand()", args&: pTarget->Number);
392 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize); DrawMenuSymbol(iMenu: C4MN_Sell, cgo&: fctSymbol, iOwner: pTarget->Base, cObj: pTarget);
393 Add(szCaption: LoadResStr(id: C4ResStrTableKey::IDS_CON_SELL), fctSymbol, szCommand: command.c_str());
394 fctSymbol.Default();
395 }
396 }
397
398 // Scripted context functions
399 AddContextFunctions(pTarget);
400
401 // Show needed material (if construction site)
402 if (pTarget->OCF & OCF_Construct && Object->r == 0 && (Game.Rules & C4RULE_ConstructionNeedsMaterial))
403 {
404 command = std::format(fmt: "PlayerMessage(GetOwner(), Object({})->GetNeededMatStr(), Object({}))", args&: pTarget->Number, args&: pTarget->Number);
405 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize); GfxR->fctConstruction.Draw(cgo&: fctSymbol, fAspect: true);
406 Add(szCaption: LoadResStr(id: C4ResStrTableKey::IDS_CON_BUILDINFO), fctSymbol, szCommand: command.c_str());
407 fctSymbol.Default();
408 }
409
410 // Target info (if desc available)
411 if (pTarget->Def->GetDesc() && *pTarget->Def->GetDesc())
412 {
413 // Symbol
414 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize);
415 fctTarget = fctSymbol.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Left, alignY: C4FCT_Bottom);
416 pTarget->DrawPicture(cgo&: fctTarget);
417 C4Facet fctTarget = fctSymbol.GetFraction(percentWdt: 85, percentHgt: 85, alignX: C4FCT_Right, alignY: C4FCT_Top);
418 GfxR->fctOKCancel.Draw(cgo&: fctTarget, fAspect: true, iPhaseX: 0, iPhaseY: 1);
419 // Command
420 command = std::format(fmt: "ShowInfo(Object({}))", args&: pTarget->Number);
421 // Add item
422 Add(szCaption: LoadResStr(id: C4ResStrTableKey::IDS_CON_INFO), fctSymbol, szCommand: command.c_str());
423 fctSymbol.Default();
424 }
425
426 // Exit (if self contained in target container)
427 if (pTarget->OCF & OCF_Container)
428 if (pTarget == Object->Contained)
429 {
430 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize);
431 Game.GraphicsResource.fctExit.Draw(cgo&: fctSymbol);
432 Add(szCaption: LoadResStr(id: C4ResStrTableKey::IDS_COMM_EXIT), fctSymbol, szCommand: "PlayerObjectCommand(GetOwner(), \"Exit\") && ExecuteCommand()"); // Exit all selected clonks...
433 fctSymbol.Default();
434 }
435 }
436 break;
437
438 default:
439 // Not an internal menu
440 return true;
441 }
442
443 // Successfull internal refill
444 rfRefilled = true;
445 return true;
446}
447
448void C4ObjectMenu::Execute()
449{
450 if (!IsActive()) return;
451 // Immediate refill check by RefillObject contents count check
452 if (RefillObject)
453 if (RefillObject->Contents.ObjectCount() != RefillObjectContentsCount)
454 {
455 NeedRefill = true; RefillObjectContentsCount = RefillObject->Contents.ObjectCount();
456 }
457 // inherited
458 C4Menu::Execute();
459}
460
461void C4ObjectMenu::OnUserSelectItem(int32_t Player, int32_t iIndex)
462{
463 // queue....
464 Game.Input.Add(eType: CID_PlrControl, pCtrl: new C4ControlPlayerControl(Player, COM_MenuSelect, iIndex | C4MN_AdjustPosition));
465}
466
467void C4ObjectMenu::OnUserEnter(int32_t Player, int32_t iIndex, bool fRight)
468{
469 // object menu: Through queue
470 Game.Input.Add(eType: CID_PlrControl, pCtrl: new C4ControlPlayerControl(Player, fRight ? COM_MenuEnterAll : COM_MenuEnter, iIndex));
471}
472
473void C4ObjectMenu::OnUserClose()
474{
475 // Queue
476 Game.Input.Add(eType: CID_PlrControl, pCtrl: new C4ControlPlayerControl(Game.MouseControl.GetPlayer(), COM_MenuClose, 0));
477}
478
479bool C4ObjectMenu::IsReadOnly()
480{
481 // get viewport
482 C4Viewport *pVP = GetViewport();
483 if (!pVP) return false;
484 // is it an observer viewport?
485 if (pVP->fIsNoOwnerViewport)
486 // is this a synced menu?
487 if (eCallbackType == CB_Object || eCallbackType == CB_Scenario)
488 // then don't control it!
489 return true;
490 // if the player is eliminated, do not control either!
491 if (!pVP->fIsNoOwnerViewport)
492 {
493 C4Player *pPlr = Game.Players.Get(iPlayer: Game.MouseControl.GetPlayer());
494 if (pPlr && pPlr->Eliminated) return true;
495 }
496 return false;
497}
498
499int32_t C4ObjectMenu::GetControllingPlayer()
500{
501 // menu controlled by object controller
502 return Object ? Object->Controller : NO_OWNER;
503}
504
505bool C4ObjectMenu::MenuCommand(const char *szCommand, bool fIsCloseCommand)
506{
507 // backup parameters to local stack because menu callback may delete this
508 bool l_Permanent = !!Permanent;
509 C4Object *l_Object = Object;
510 int32_t l_LastSelection = LastSelection;
511 if (l_Object)
512 {
513 ClearObjectPtr = &l_Object;
514 }
515 const auto deletionTracker = TrackDeletion();
516
517 switch (eCallbackType)
518 {
519 case CB_Object:
520 // Object menu
521 if (Object) Object->MenuCommand(szCommand);
522 break;
523
524 case CB_Scenario:
525 // Object menu with scenario script callback
526 Game.Script.DirectExec(pObj: nullptr, szScript: szCommand, szContext: "MenuCommand", fPassErrors: false, Strict: Game.Script.Strict);
527 break;
528
529 case CB_None:
530 assert(!"Callback type is CB_None");
531 break;
532 }
533
534 if ((!l_Permanent || fIsCloseCommand) && l_Object) l_Object->AutoContextMenu(iMenuSelect: l_LastSelection);
535
536 if (!deletionTracker.IsDeleted())
537 {
538 ClearObjectPtr = nullptr;
539 }
540
541 return true;
542}
543
544int32_t C4ObjectMenu::AddContextFunctions(C4Object *pTarget, bool fCountOnly)
545{
546 const auto symbolSize = GetSymbolSize();
547
548 int32_t iFunction, iResult = 0;
549 C4AulScriptFunc *pFunction, *pFunction2;
550 C4Object *cObj; C4ObjectLink *clnk;
551 const char *strDescText;
552 std::string command;
553 C4Def *pDef;
554 C4IDList ListItems;
555 C4Facet fctTarget;
556 C4FacetExSurface fctSymbol;
557
558 // ActionContext functions of target's action target (for first target only, because otherwise strange stuff can happen with outdated Target2s...)
559 if (pTarget->Action.Act > ActIdle)
560 if (cObj = pTarget->Action.Target)
561 for (iFunction = 0; pFunction = cObj->Def->Script.GetSFunc(iIndex: iFunction, szPattern: "ActionContext"); iFunction++)
562 if (!pFunction->OverloadedBy)
563 if (!pFunction->Condition || pFunction->Condition->Exec(pObj: cObj, pPars: {C4VObj(pObj: Object), C4VID(idVal: pFunction->idImage), C4VObj(pObj: pTarget)}))
564 if (!fCountOnly)
565 {
566 command = std::format(fmt: "ProtectedCall(Object({}),\"{}\",this,Object({}))", args&: cObj->Number, args: +pFunction->Name, args&: pTarget->Number);
567 if (pDef = C4Id2Def(id: pFunction->idImage)) pDef->Picture2Facet(cgo&: fctSymbol, color: 0, xPhase: pFunction->iImagePhase);
568 Add(szCaption: pFunction->DescText.getData(), fctSymbol, szCommand: command.c_str(), iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: pFunction->DescLong.getData());
569 iResult++;
570 }
571 else
572 iResult++;
573
574 // Effect context functions of target's effects
575 for (C4Effect *pEff = pTarget->pEffects; pEff; pEff = pEff->pNext)
576 if (pEff->IsActive())
577 {
578 C4AulScript *pEffScript = pEff->GetCallbackScript();
579 if (pEffScript)
580 for (iFunction = 0; pFunction = pEffScript->GetSFunc(iIndex: iFunction, szPattern: std::format(PSF_FxCustom, args: +pEff->Name, args: "Context").c_str()); iFunction++)
581 if (!pFunction->OverloadedBy)
582 if (!pFunction->Condition || pFunction->Condition->Exec(pObj: pEff->pCommandTarget, pPars: {C4VObj(pObj: pTarget), C4VInt(iVal: pEff->iNumber), C4VObj(pObj: Object), C4VID(idVal: pFunction->idImage)}))
583 if (!fCountOnly)
584 {
585 if (pEff->pCommandTarget)
586 {
587 command = std::format(fmt: "ProtectedCall(Object({}),\"{}\",Object({}),{},Object({}),{})", args&: pEff->pCommandTarget->Number, args&: pFunction->Name, args&: pTarget->Number, args: static_cast<int>(pEff->iNumber), args&: Object->Number, args: C4IdText(id: pFunction->idImage));
588 }
589 else if (pEff->idCommandTarget)
590 {
591 command = std::format(fmt: "DefinitionCall({}, \"{}\", Object({}),{},Object({}),{})", args: C4IdText(id: pEff->idCommandTarget), args&: pFunction->Name, args&: pTarget->Number, args: static_cast<int>(pEff->iNumber), args&: Object->Number, args: C4IdText(id: pFunction->idImage));
592 }
593 else
594 {
595 command = std::format(fmt: "global->~{}(Object({}),{},Object({}),{})", args&: pFunction->Name, args&: pTarget->Number, args: static_cast<int>(pEff->iNumber), args&: Object->Number, args: C4IdText(id: pFunction->idImage));
596 }
597 if (pDef = C4Id2Def(id: pFunction->idImage)) pDef->Picture2Facet(cgo&: fctSymbol, color: 0, xPhase: pFunction->iImagePhase);
598 Add(szCaption: pFunction->DescText.getData(), fctSymbol, szCommand: command.c_str(), iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: pFunction->DescLong.getData());
599 fctSymbol.Default();
600 iResult++;
601 }
602 else
603 iResult++;
604 }
605
606 // Script context functions of any objects attached to target (search global list, because attachment objects might be moved just about anywhere...)
607 for (clnk = Game.Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next)
608 if (cObj->Status && cObj->Action.Target == pTarget)
609 if (cObj->Action.Act > ActIdle)
610 if (cObj->Def->ActMap[cObj->Action.Act].Procedure == DFA_ATTACH)
611 for (iFunction = 0; pFunction = cObj->Def->Script.GetSFunc(iIndex: iFunction, szPattern: "AttachContext"); iFunction++)
612 if (!pFunction->OverloadedBy)
613 if (!pFunction->Condition || pFunction->Condition->Exec(pObj: cObj, pPars: {C4VObj(pObj: Object), C4VID(idVal: pFunction->idImage), C4VObj(pObj: pTarget)}))
614 if (!fCountOnly)
615 {
616 command = std::format(fmt: "ProtectedCall(Object({}),\"{}\",this,Object({}))", args&: cObj->Number, args: +pFunction->Name, args&: pTarget->Number);
617 if (pDef = C4Id2Def(id: pFunction->idImage)) pDef->Picture2Facet(cgo&: fctSymbol, color: 0, xPhase: pFunction->iImagePhase);
618 Add(szCaption: pFunction->DescText.getData(), fctSymbol, szCommand: command.c_str(), iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: pFunction->DescLong.getData());
619 fctSymbol.Default();
620 iResult++;
621 }
622 else
623 iResult++;
624
625 // 'Activate' and 'ControlDigDouble' script functions of target
626 const char *func, *funcs[] = { "Activate", "ControlDigDouble", nullptr };
627 for (int i = 0; func = funcs[i]; i++)
628 // 'Activate' function only if in clonk's inventory; 'ControlDigDouble' function only if pushed by clonk
629 if ((SEqual(szStr1: func, szStr2: "Activate") && (pTarget->Contained == Object)) || (SEqual(szStr1: func, szStr2: "ControlDigDouble") && (Object->GetProcedure() == DFA_PUSH) && (Object->Action.Target == pTarget)))
630 // Find function
631 if (pFunction = pTarget->Def->Script.GetSFunc(pIdtf: func))
632 // Find function not overloaded
633 if (!pFunction->OverloadedBy)
634 // Function condition valid
635 if (!pFunction->Condition || pFunction->Condition->Exec(pObj: pTarget, pPars: {C4VObj(pObj: Object), C4VID(idVal: pFunction->idImage)}))
636 {
637 // Get function text
638 strDescText = pFunction->DescText.getData() ? pFunction->DescText.getData() : pTarget->GetName();
639 // Check if there is a scripted context function doing exactly the same
640 bool fDouble = false;
641 for (iFunction = 0; pFunction2 = pTarget->Def->Script.GetSFunc(iIndex: iFunction, szPattern: "Context"); iFunction++)
642 if (!pFunction2->OverloadedBy)
643 if (!pFunction2->Condition || pFunction2->Condition->Exec(pObj: pTarget, pPars: {C4VObj(pObj: Object), C4VID(idVal: pFunction2->idImage)}))
644 if (SEqual(szStr1: strDescText, szStr2: pFunction2->DescText.getData()))
645 fDouble = true;
646 // If so, skip this function to prevent duplicate entries
647 if (fDouble) continue;
648 // Count this function
649 iResult++;
650 // Count only: don't actually add
651 if (fCountOnly) continue;
652 // Command
653 command = std::format(fmt: "ProtectedCall(Object({}),\"{}\",this)", args&: pTarget->Number, args: +pFunction->Name);
654 // Symbol
655 if (pDef = C4Id2Def(id: pFunction->idImage))
656 {
657 pDef->Picture2Facet(cgo&: fctSymbol, color: 0, xPhase: pFunction->iImagePhase);
658 }
659 else
660 {
661 fctSymbol.Create(iWdt: symbolSize, iHgt: symbolSize);
662 pTarget->DrawPicture(cgo&: fctSymbol);
663 }
664 // Add menu item
665 Add(szCaption: strDescText, fctSymbol, szCommand: command.c_str(), iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: pFunction->DescLong.getData());
666 // Preserve symbol
667 fctSymbol.Default();
668 }
669
670 // Script context functions of target
671 if (!(pTarget->OCF & OCF_CrewMember) || (pTarget->Owner == Object->Owner)) // Crew member: only allow if owned by ourself
672 if (!(pTarget->Category & C4D_Living) || pTarget->GetAlive()) // No dead livings
673 for (iFunction = 0; pFunction = pTarget->Def->Script.GetSFunc(iIndex: iFunction, szPattern: "Context"); iFunction++)
674 if (!pFunction->OverloadedBy)
675 if (!pFunction->Condition || pFunction->Condition->Exec(pObj: pTarget, pPars: {C4VObj(pObj: Object), C4VID(idVal: pFunction->idImage)}))
676 if (!fCountOnly)
677 {
678 command = std::format(fmt: "ProtectedCall(Object({}),\"{}\",this)", args&: pTarget->Number, args: +pFunction->Name);
679 if (pDef = C4Id2Def(id: pFunction->idImage)) pDef->Picture2Facet(cgo&: fctSymbol, color: 0, xPhase: pFunction->iImagePhase);
680 Add(szCaption: pFunction->DescText.getData(), fctSymbol, szCommand: command.c_str(), iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: pFunction->DescLong.getData());
681 fctSymbol.Default();
682 iResult++;
683 }
684 else
685 iResult++;
686
687 // Context functions of the menu clonk itself (if not same as target)
688 if (Object != pTarget)
689 // Only if clonk is inside or grabbing or containing target (this excludes remote mouse-right-click context menus)
690 if ((Object->Contained == pTarget) || ((Object->GetProcedure() == DFA_PUSH) && (Object->Action.Target == pTarget)) || (pTarget->Contained == Object))
691 // No dead livings
692 if (!(Object->Category & C4D_Living) || Object->GetAlive())
693 {
694 // Context menu for a building or grabbed vehicle: move the clonk functions into a submenu if more than two functions
695 int32_t iSubMenuThreshold = 2;
696 // Context menu for an inventory item: no threshold, always display clonk functions directly with target object functions
697 if (pTarget->Contained == Object) iSubMenuThreshold = -1;
698 // First count the available entries
699 int32_t iFunctions = AddContextFunctions(pTarget: Object, fCountOnly: true);
700 // Less than threshold or no threshold: display directly
701 if ((iFunctions <= iSubMenuThreshold) || (iSubMenuThreshold == -1))
702 iResult += AddContextFunctions(pTarget: Object);
703 // Above threshold: create sub-menu entry for the clonk
704 else if (!fCountOnly)
705 {
706 Object->Def->Picture2Facet(cgo&: fctSymbol, color: Object->Color);
707 Add(szCaption: Object->Def->GetName(), fctSymbol, szCommand: "SetCommand(this,\"Context\",,0,0,this)&&ExecuteCommand()", iCount: C4MN_Item_NoCount, pObject: nullptr, szInfoCaption: LoadResStr(id: C4ResStrTableKey::IDS_MENU_CONTEXTSUBCLONKDESC));
708 fctSymbol.Default();
709 iResult++;
710 }
711 else
712 iResult++;
713 }
714
715 // Done
716 return iResult;
717}
718