1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, Sven2
6 * Copyright (c) 2017-2021, 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// handles input dialogs, last-message-buffer, MessageBoard-commands
19
20#include "C4GuiEdit.h"
21#include "C4GuiResource.h"
22#include <C4Include.h>
23#include <C4MessageInput.h>
24
25#include <C4Game.h>
26#include <C4Object.h>
27#include <C4Script.h>
28#include <C4Gui.h>
29#include <C4Console.h>
30#include <C4Application.h>
31#include <C4Network2Dialogs.h>
32#include <C4Log.h>
33#include <C4Player.h>
34#include <C4GameLobby.h>
35
36// C4ChatInputDialog
37
38// singleton
39C4ChatInputDialog *C4ChatInputDialog::pInstance = nullptr;
40
41// helper func: Determine whether input text is good for a chat-style-layout dialog
42bool IsSmallInputQuery(const char *szInputQuery)
43{
44 if (!szInputQuery) return true;
45 int32_t w, h;
46 if (SCharCount(cTarget: '|', szInStr: szInputQuery)) return false;
47 if (!C4GUI::GetRes()->TextFont.GetTextExtent(szText: szInputQuery, rsx&: w, rsy&: h, fCheckMarkup: true))
48 return false; // ???
49 return w < C4GUI::GetScreenWdt() / 5;
50}
51
52C4ChatInputDialog::C4ChatInputDialog(bool fObjInput, C4Object *pScriptTarget, bool fUppercase, Mode mode, int32_t iPlr, const StdStrBuf &rsInputQuery)
53 : C4GUI::InputDialog(fObjInput ? rsInputQuery.getData() : LoadResStrNoAmp(id: C4ResStrTableKey::IDS_CTL_CHAT).c_str(), nullptr, C4GUI::Ico_None, nullptr, !fObjInput || IsSmallInputQuery(szInputQuery: rsInputQuery.getData())),
54 fObjInput(fObjInput), fUppercase(fUppercase), pTarget(pScriptTarget), BackIndex(-1), iPlr(iPlr), fProcessed(false)
55{
56 // singleton-var
57 pInstance = this;
58 // set custom edit control
59 SetCustomEdit(new C4GUI::CallbackEdit<C4ChatInputDialog>(C4Rect(0, 0, 10, 10), this, &C4ChatInputDialog::OnChatInput, &C4ChatInputDialog::OnChatCancel));
60 // key bindings
61 pKeyHistoryUp = new C4KeyBinding(C4KeyCodeEx(K_UP), "ChatHistoryUp", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<C4ChatInputDialog, bool>(*this, true, &C4ChatInputDialog::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
62 pKeyHistoryDown = new C4KeyBinding(C4KeyCodeEx(K_DOWN), "ChatHistoryDown", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<C4ChatInputDialog, bool>(*this, false, &C4ChatInputDialog::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
63 pKeyAbort = new C4KeyBinding(C4KeyCodeEx(K_F2), "ChatAbort", KEYSCOPE_Gui, new C4GUI::DlgKeyCB<C4GUI::Dialog>(*this, &C4GUI::Dialog::KeyEscape), C4CustomKey::PRIO_CtrlOverride);
64 pKeyNickComplete = new C4KeyBinding(C4KeyCodeEx(K_TAB), "ChatNickComplete", KEYSCOPE_Gui, new C4GUI::DlgKeyCB<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyCompleteNick), C4CustomKey::PRIO_CtrlOverride);
65 pKeyPlrControl = new C4KeyBinding(C4KeyCodeEx(KEY_Any, KEYS_Control), "ChatForwardPlrCtrl", KEYSCOPE_Gui, new C4GUI::DlgKeyCBPassKey<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyPlrControl), C4CustomKey::PRIO_Dlg);
66 pKeyGamepadControl = new C4KeyBinding(C4KeyCodeEx(KEY_Any), "ChatForwardGamepadCtrl", KEYSCOPE_Gui, new C4GUI::DlgKeyCBPassKey<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyGamepadControlDown, &C4ChatInputDialog::KeyGamepadControlUp, &C4ChatInputDialog::KeyGamepadControlPressed), C4CustomKey::PRIO_PlrControl);
67 pKeyBackClose = new C4KeyBinding(C4KeyCodeEx(K_BACK), "ChatBackspaceClose", KEYSCOPE_Gui, new C4GUI::DlgKeyCB<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyBackspaceClose), C4CustomKey::PRIO_CtrlOverride);
68 // free when closed...
69 SetDelOnClose();
70
71 // initial text
72 switch (mode)
73 {
74 case Allies:
75 pEdit->InsertText(text: "/team ", fUser: true);
76 break;
77
78 case Say:
79 pEdit->InsertText(text: "\"", fUser: true);
80 break;
81
82 case All:
83 break;
84 }
85}
86
87C4ChatInputDialog::~C4ChatInputDialog()
88{
89 delete pKeyHistoryUp;
90 delete pKeyHistoryDown;
91 delete pKeyAbort;
92 delete pKeyNickComplete;
93 delete pKeyPlrControl;
94 delete pKeyGamepadControl;
95 delete pKeyBackClose;
96 if (this == pInstance) pInstance = nullptr;
97}
98
99void C4ChatInputDialog::OnChatCancel()
100{
101 // abort chat: Make sure msg board query is aborted
102 fProcessed = true;
103 if (fObjInput)
104 {
105 // check if the target input is still valid
106 C4Player *pPlr = Game.Players.Get(iPlayer: iPlr);
107 if (!pPlr) return;
108 if (pPlr->MarkMessageBoardQueryAnswered(pForObj: pTarget))
109 {
110 // there was an associated query - it must be removed on all clients synchronized via queue
111 // do this by calling OnMessageBoardAnswer without an answer
112 Game.Control.DoInput(eCtrlType: CID_MessageBoardAnswer, pPkt: new C4ControlMessageBoardAnswer(pTarget ? pTarget->Number : 0, iPlr, ""), eDelivery: CDT_Decide);
113 }
114 }
115}
116
117void C4ChatInputDialog::OnClosed(bool fOK)
118{
119 // make sure chat input is processed, even if closed by other means than Enter on edit
120 if (!fProcessed)
121 if (fOK)
122 OnChatInput(edt: pEdit, fPasting: false, fPastingMore: false); else OnChatCancel();
123 else
124 OnChatCancel();
125 Game.Players.ClearLocalPlayerPressedComs();
126 typedef C4GUI::InputDialog BaseDlg;
127 BaseDlg::OnClosed(fOK);
128}
129
130C4GUI::InputResult C4ChatInputDialog::OnChatInput(C4GUI::Edit *pEdt, bool fPasting, bool fPastingMore)
131{
132 // no double processing
133 if (fProcessed) return C4GUI::IR_CloseDlg;
134 // get edit text
135 char *szInputText = const_cast<char *>(pEdt->GetText());
136 // Store to back buffer
137 Game.MessageInput.StoreBackBuffer(szMessage: szInputText);
138 // script queried input?
139 if (fObjInput)
140 {
141 fProcessed = true;
142 // check if the target input is still valid
143 C4Player *pPlr = Game.Players.Get(iPlayer: iPlr);
144 if (!pPlr) return C4GUI::IR_CloseDlg;
145 if (!pPlr->MarkMessageBoardQueryAnswered(pForObj: pTarget))
146 {
147 // there was no associated query!
148 return C4GUI::IR_CloseDlg;
149 }
150 // then do a script callback, incorporating the input into the answer
151 if (fUppercase) SCapitalize(szString: szInputText);
152 Game.Control.DoInput(eCtrlType: CID_MessageBoardAnswer, pPkt: new C4ControlMessageBoardAnswer(pTarget ? pTarget->Number : 0, iPlr, szInputText), eDelivery: CDT_Decide);
153 return C4GUI::IR_CloseDlg;
154 }
155 else
156 // reroute to message input class
157 Game.MessageInput.ProcessInput(szText: szInputText);
158 // safety: message board commands may do strange things
159 if (!C4GUI::IsGUIValid() || this != pInstance) return C4GUI::IR_Abort;
160 // select all text to be removed with next keypress
161 // just for pasting mode; usually the dlg will be closed now anyway
162 pEdt->SelectAll();
163 // avoid dlg close, if more content is to be pasted
164 if (fPastingMore) return C4GUI::IR_None;
165 fProcessed = true;
166 return C4GUI::IR_CloseDlg;
167}
168
169bool C4ChatInputDialog::KeyHistoryUpDown(bool fUp)
170{
171 // browse chat history
172 pEdit->SelectAll(); pEdit->DeleteSelection();
173 const char *szPrevInput = Game.MessageInput.GetBackBuffer(iIndex: fUp ? (++BackIndex) : (--BackIndex));
174 if (!szPrevInput || !*szPrevInput)
175 BackIndex = -1;
176 else
177 {
178 pEdit->InsertText(text: szPrevInput, fUser: true);
179 pEdit->SelectAll();
180 }
181 return true;
182}
183
184bool C4ChatInputDialog::KeyPlrControl(C4KeyCodeEx key)
185{
186 // Control pressed while doing this key: Reroute this key as a player-control
187 Game.DoKeyboardInput(vk_code: uint16_t(key.Key), eEventType: KEYEV_Down, fAlt: !!(key.dwShift & KEYS_Alt), fCtrl: false, fShift: !!(key.dwShift & KEYS_Shift), fRepeated: key.IsRepeated(), pForDialog: nullptr, fPlrCtrlOnly: true);
188 // mark as processed, so it won't get any double processing
189 return true;
190}
191
192bool C4ChatInputDialog::KeyGamepadControlDown(C4KeyCodeEx key)
193{
194 // filter gamepad control
195 if (!Key_IsGamepad(key: key.Key)) return false;
196 // forward it
197 Game.DoKeyboardInput(vk_code: key.Key, eEventType: KEYEV_Down, fAlt: false, fCtrl: false, fShift: false, fRepeated: key.IsRepeated(), pForDialog: nullptr, fPlrCtrlOnly: true);
198 return true;
199}
200
201bool C4ChatInputDialog::KeyGamepadControlUp(C4KeyCodeEx key)
202{
203 // filter gamepad control
204 if (!Key_IsGamepad(key: key.Key)) return false;
205 // forward it
206 Game.DoKeyboardInput(vk_code: key.Key, eEventType: KEYEV_Up, fAlt: false, fCtrl: false, fShift: false, fRepeated: key.IsRepeated(), pForDialog: nullptr, fPlrCtrlOnly: true);
207 return true;
208}
209
210bool C4ChatInputDialog::KeyGamepadControlPressed(C4KeyCodeEx key)
211{
212 // filter gamepad control
213 if (!Key_IsGamepad(key: key.Key)) return false;
214 // forward it
215 Game.DoKeyboardInput(vk_code: key.Key, eEventType: KEYEV_Pressed, fAlt: false, fCtrl: false, fShift: false, fRepeated: key.IsRepeated(), pForDialog: nullptr, fPlrCtrlOnly: true);
216 return true;
217}
218
219bool C4ChatInputDialog::KeyBackspaceClose()
220{
221 // close if chat text box is empty (on backspace)
222 if (pEdit->GetText() && *pEdit->GetText()) return false;
223 Close(fOK: false);
224 return true;
225}
226
227bool C4ChatInputDialog::KeyCompleteNick()
228{
229 if (!pEdit) return false;
230 char IncompleteNick[256 + 1];
231 // get current word in edit
232 if (!pEdit->GetCurrentWord(szTargetBuf: IncompleteNick, iMaxTargetBufLen: 256)) return false;
233 if (!*IncompleteNick) return false;
234 C4Player *plr = Game.Players.First;
235 while (plr)
236 {
237 // Compare name and input
238 if (SEqualNoCase(szStr1: plr->GetName(), szStr2: IncompleteNick, iLen: SLen(sptr: IncompleteNick)))
239 {
240 pEdit->InsertText(text: plr->GetName() + SLen(sptr: IncompleteNick), fUser: true);
241 return true;
242 }
243 else
244 plr = plr->Next;
245 }
246 // no match found
247 return false;
248}
249
250// C4MessageInput
251
252bool C4MessageInput::Init()
253{
254 // add default commands
255 if (Commands.empty())
256 {
257 AddCommand(strCommand: SPEED, strScript: "SetGameSpeed(%d)");
258 }
259 return true;
260}
261
262void C4MessageInput::Default()
263{
264 // clear backlog
265 for (int32_t cnt = 0; cnt < C4MSGB_BackBufferMax; cnt++) BackBuffer[cnt][0] = 0;
266}
267
268void C4MessageInput::Clear()
269{
270 // close any dialog
271 CloseTypeIn();
272 Commands.clear();
273}
274
275bool C4MessageInput::CloseTypeIn()
276{
277 // close dialog if present and valid
278 C4ChatInputDialog *pDlg = GetTypeIn();
279 if (!pDlg || !C4GUI::IsGUIValid()) return false;
280 pDlg->Close(fOK: false);
281 return true;
282}
283
284bool C4MessageInput::StartTypeIn(bool fObjInput, C4Object *pObj, bool fUpperCase, C4ChatInputDialog::Mode mode, int32_t iPlr, const StdStrBuf &rsInputQuery)
285{
286 if (!C4GUI::IsGUIValid()) return false;
287
288 // existing dialog? only close if empty
289 if (C4ChatInputDialog *const dialog{GetTypeIn()}; dialog)
290 {
291 if (dialog->IsEmpty())
292 {
293 dialog->Close(fOK: false);
294 }
295 else
296 {
297 return false;
298 }
299 }
300
301 // start new
302 return Game.pGUI->ShowRemoveDlg(pDlg: new C4ChatInputDialog(fObjInput, pObj, fUpperCase, mode, iPlr, rsInputQuery));
303}
304
305bool C4MessageInput::KeyStartTypeIn(C4ChatInputDialog::Mode mode)
306{
307 // fullscreen only
308 if (!Application.isFullScreen) return false;
309 // OK, start typing
310 return StartTypeIn(fObjInput: false, pObj: nullptr, fUpperCase: false, mode);
311}
312
313bool C4MessageInput::IsTypeIn()
314{
315 // check GUI and dialog
316 return C4GUI::IsGUIValid() && C4ChatInputDialog::IsShown();
317}
318
319bool C4MessageInput::ProcessInput(const char *szText)
320{
321 // helper variables
322 C4ControlMessageType eMsgType;
323 const char *szMsg = nullptr;
324 int32_t iToPlayer = -1;
325 std::string tmpString;
326
327 // Starts with '^', "team:" or "/team ": Team message
328 if (szText[0] == '^' || SEqual2NoCase(szStr1: szText, szStr2: "team:") || SEqual2NoCase(szStr1: szText, szStr2: "/team "))
329 {
330 if (!Game.Teams.IsTeamVisible())
331 {
332 // team not known; can't send!
333 Log(id: C4ResStrTableKey::IDS_MSG_CANTSENDTEAMMESSAGETEAMSN);
334 return false;
335 }
336 else
337 {
338 eMsgType = C4CMT_Team;
339 szMsg = szText[0] == '^' ? szText + 1 :
340 szText[0] == '/' ? szText + 6 : szText + 5;
341 }
342 }
343 // Starts with "/private ": Private message (running game only)
344 else if (Game.IsRunning && SEqual2NoCase(szStr1: szText, szStr2: "/private "))
345 {
346 // get target name
347 char szTargetPlr[C4MaxName + 1];
348 SCopyUntil(szSource: szText + 9, sTarget: szTargetPlr, cUntil: ' ', iMaxL: C4MaxName);
349 // search player
350 C4Player *pToPlr = Game.Players.GetByName(szName: szTargetPlr);
351 if (!pToPlr) return false;
352 // set
353 eMsgType = C4CMT_Private;
354 iToPlayer = pToPlr->Number;
355 szMsg = szText + 10 + SLen(sptr: szTargetPlr);
356 if (szMsg > szText + SLen(sptr: szText)) return false;
357 }
358 // Starts with "/me ": Me-Message
359 else if (SEqual2NoCase(szStr1: szText, szStr2: "/me "))
360 {
361 eMsgType = C4CMT_Me;
362 szMsg = szText + 4;
363 }
364 // Starts with "/sound ": Sound-Message
365 else if (SEqual2NoCase(szStr1: szText, szStr2: "/sound "))
366 {
367 eMsgType = C4CMT_Sound;
368 szMsg = szText + 7;
369 }
370 // Starts with "/alert": Taskbar flash (message optional)
371 else if (SEqual2NoCase(szStr1: szText, szStr2: "/alert ") || SEqualNoCase(szStr1: szText, szStr2: "/alert"))
372 {
373 eMsgType = C4CMT_Alert;
374 szMsg = szText + 6;
375 if (*szMsg) ++szMsg;
376 }
377 // Starts with '"': Let the clonk say it
378 else if (Game.IsRunning && szText[0] == '"')
379 {
380 eMsgType = C4CMT_Say;
381 // Append '"', if neccessary
382 tmpString = szText;
383 if (tmpString.back() != '"') tmpString.push_back(c: '"');
384 szMsg = tmpString.c_str();
385 }
386 // Starts with '/': Command
387 else if (szText[0] == '/')
388 return ProcessCommand(szCommand: szText);
389 // Regular message
390 else
391 {
392 eMsgType = C4CMT_Normal;
393 szMsg = szText;
394 }
395
396 // message?
397 if (szMsg)
398 {
399 char szMessage[C4MaxMessage + 1];
400 // go over whitespaces, check empty message
401 while (IsWhiteSpace(cChar: *szMsg)) szMsg++;
402 if (!*szMsg)
403 {
404 if (eMsgType != C4CMT_Alert) return true;
405 *szMessage = '\0';
406 }
407 else
408 {
409 // trim right
410 const char *szEnd = szMsg + SLen(sptr: szMsg) - 1;
411 while (IsWhiteSpace(cChar: *szEnd) && szEnd >= szMsg) szEnd--;
412 // Say: Strip quotation marks in cinematic film mode
413 if (Game.C4S.Head.Film == C4SFilm_Cinematic)
414 {
415 if (eMsgType == C4CMT_Say) { ++szMsg; szEnd--; }
416 }
417 // get message
418 SCopy(szSource: szMsg, sTarget: szMessage, iMaxL: std::min<unsigned long>(a: C4MaxMessage, b: szEnd - szMsg + 1));
419 }
420 // get sending player (if any)
421 C4Player *pPlr = Game.IsRunning ? Game.Players.GetLocalByIndex(iIndex: 0) : nullptr;
422 // send
423 Game.Control.DoInput(eCtrlType: CID_Message,
424 pPkt: new C4ControlMessage(eMsgType, szMessage, pPlr ? pPlr->Number : -1, iToPlayer),
425 eDelivery: CDT_Private);
426 }
427
428 return true;
429}
430
431bool C4MessageInput::ProcessCommand(const char *szCommand)
432{
433 C4GameLobby::MainDlg *pLobby = Game.Network.GetLobby();
434 // command
435 char szCmdName[C4MaxName + 1];
436 SCopyUntil(szSource: szCommand + 1, sTarget: szCmdName, cUntil: ' ', iMaxL: C4MaxName);
437 // parameter
438 const char *pCmdPar = SSearch(szString: szCommand, szIndex: " ");
439 if (!pCmdPar) pCmdPar = "";
440
441 // dev-scripts
442 if (SEqual(szStr1: szCmdName, szStr2: "help"))
443 {
444 Log(id: C4ResStrTableKey::IDS_TEXT_COMMANDSAVAILABLEDURINGGA);
445 LogNTr(fmt: "/private [player] [message] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SENDAPRIVATEMESSAGETOTHES));
446 LogNTr(fmt: "/team [message] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SENDAPRIVATEMESSAGETOYOUR));
447 LogNTr(fmt: "/me [action] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PERFORMANACTIONINYOURNAME));
448 LogNTr(fmt: "/sound [sound] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PLAYASOUNDFROMTHEGLOBALSO));
449 LogNTr(fmt: "/mute [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_MUTESOUNDCOMMANDSBYTHESPE));
450 LogNTr(fmt: "/unmute [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_UNMUTESOUNDCOMMANDSBYTHESP));
451 LogNTr(fmt: "/kick [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_KICKTHESPECIFIEDCLIENT));
452 LogNTr(fmt: "/observer [client] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETTHESPECIFIEDCLIENTTOOB));
453 LogNTr(fmt: "/fast [x] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETTOFASTMODESKIPPINGXFRA));
454 LogNTr(fmt: "/slow - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETTONORMALSPEEDMODE));
455 LogNTr(fmt: "/chart - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_DISPLAYNETWORKSTATISTICS));
456 LogNTr(fmt: "/nodebug - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_PREVENTDEBUGMODEINTHISROU));
457 LogNTr(fmt: "/set comment [comment] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETANEWNETWORKCOMMENT));
458 LogNTr(fmt: "/set password [password] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETANEWNETWORKPASSWORD));
459 LogNTr(fmt: "/set faircrew [on/off] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_ENABLEORDISABLEFAIRCREW));
460 LogNTr(fmt: "/set maxplayer [4] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_SETANEWMAXIMUMNUMBEROFPLA));
461 LogNTr(fmt: "/script [script] - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_EXECUTEASCRIPTCOMMAND));
462 LogNTr(fmt: "/clear - {}", args: LoadResStr(id: C4ResStrTableKey::IDS_MSG_CLEARTHEMESSAGEBOARD));
463 return true;
464 }
465 // dev-scripts
466 if (SEqual(szStr1: szCmdName, szStr2: "script"))
467 {
468 if (!Game.IsRunning) return false;
469 if (!Game.DebugMode) return false;
470 if (Game.Network.isEnabled() && !Game.Network.isHost()) return false;
471
472 Game.Control.DoInput(eCtrlType: CID_Script, pPkt: new C4ControlScript(pCmdPar, C4ControlScript::SCOPE_Console, Config.Developer.ConsoleScriptStrictness), eDelivery: CDT_Decide);
473 return true;
474 }
475 // set runtimte properties
476 if (SEqual(szStr1: szCmdName, szStr2: "set"))
477 {
478 if (SEqual2(szStr1: pCmdPar, szStr2: "maxplayer "))
479 {
480 if (Game.Control.isCtrlHost())
481 {
482 if (atoi(nptr: pCmdPar + 10) == 0 && !SEqual(szStr1: pCmdPar + 10, szStr2: "0"))
483 {
484 LogNTr(message: "Syntax: /set maxplayer count");
485 return false;
486 }
487 Game.Control.DoInput(eCtrlType: CID_Set,
488 pPkt: new C4ControlSet(C4CVT_MaxPlayer, atoi(nptr: pCmdPar + 10)),
489 eDelivery: CDT_Decide);
490 return true;
491 }
492 }
493 if (SEqual2(szStr1: pCmdPar, szStr2: "comment ") || SEqual(szStr1: pCmdPar, szStr2: "comment"))
494 {
495 if (!Game.Network.isEnabled() || !Game.Network.isHost()) return false;
496 // Set in configuration, update reference
497 Config.Network.Comment.CopyValidated(szFromVal: pCmdPar[7] ? (pCmdPar + 8) : "");
498 Game.Network.InvalidateReference();
499 Log(id: C4ResStrTableKey::IDS_NET_COMMENTCHANGED);
500 return true;
501 }
502 if (SEqual2(szStr1: pCmdPar, szStr2: "password ") || SEqual(szStr1: pCmdPar, szStr2: "password"))
503 {
504 if (!Game.Network.isEnabled() || !Game.Network.isHost()) return false;
505 Game.Network.SetPassword(pCmdPar[8] ? (pCmdPar + 9) : nullptr);
506 if (pLobby) pLobby->UpdatePassword();
507 return true;
508 }
509 if (SEqual2(szStr1: pCmdPar, szStr2: "faircrew "))
510 {
511 if (!Game.Control.isCtrlHost() || Game.Parameters.isLeague()) return false;
512 C4ControlSet *pSet = nullptr;
513 if (SEqual(szStr1: pCmdPar + 9, szStr2: "on"))
514 pSet = new C4ControlSet(C4CVT_FairCrew, Config.General.FairCrewStrength);
515 else if (SEqual(szStr1: pCmdPar + 9, szStr2: "off"))
516 pSet = new C4ControlSet(C4CVT_FairCrew, -1);
517 else if (isdigit(static_cast<unsigned char>(pCmdPar[9])))
518 pSet = new C4ControlSet(C4CVT_FairCrew, atoi(nptr: pCmdPar + 9));
519 else
520 return false;
521 Game.Control.DoInput(eCtrlType: CID_Set, pPkt: pSet, eDelivery: CDT_Decide);
522 return true;
523 }
524 // unknown property
525 return false;
526 }
527 // get szen from network folder - not in lobby; use res tab there
528 if (SEqual(szStr1: szCmdName, szStr2: "netgetscen"))
529 {
530 if (Game.Network.isEnabled() && !Game.Network.isHost() && !pLobby)
531 {
532 const C4Network2ResCore *pResCoreScen = Game.Parameters.Scenario.getResCore();
533 if (pResCoreScen)
534 {
535 C4Network2Res::Ref pScenario = Game.Network.ResList.getRefRes(iResID: pResCoreScen->getID());
536 if (pScenario)
537 if (C4Group_CopyItem(szSource: pScenario->getFile(), szTarget: Config.AtExePath(szFilename: GetFilename(path: Game.ScenarioFilename))))
538 {
539 Log(id: C4ResStrTableKey::IDS_MSG_CMD_NETGETSCEN_SAVED, args: Config.AtExePath(szFilename: GetFilename(path: Game.ScenarioFilename)));
540 return true;
541 }
542 }
543 }
544 return false;
545 }
546 // clear message board
547 if (SEqual(szStr1: szCmdName, szStr2: "clear"))
548 {
549 // lobby
550 if (pLobby)
551 {
552 pLobby->ClearLog();
553 }
554 // fullscreen
555 else if (Game.GraphicsSystem.MessageBoard.Active)
556 Game.GraphicsSystem.MessageBoard.ClearLog();
557 else
558 {
559 // EM mode
560 Console.ClearLog();
561 }
562 return true;
563 }
564 // kick client
565 if (SEqual(szStr1: szCmdName, szStr2: "kick"))
566 {
567 if (Game.Network.isEnabled() && Game.Network.isHost())
568 {
569 // find client
570 C4Client *pClient = Game.Clients.getClientByName(szName: pCmdPar);
571 if (!pClient)
572 {
573 Log(id: C4ResStrTableKey::IDS_MSG_CMD_NOCLIENT, args&: pCmdPar);
574 return false;
575 }
576 // league: Kick needs voting
577 if (Game.Parameters.isLeague() && Game.Players.GetAtClient(iClient: pClient->getID()))
578 Game.Network.Vote(eType: VT_Kick, fApprove: true, iData: pClient->getID());
579 else
580 // add control
581 Game.Clients.CtrlRemove(pClient, szReason: LoadResStr(id: C4ResStrTableKey::IDS_MSG_KICKFROMMSGBOARD));
582 }
583 return true;
584 }
585 // set fast mode
586 if (SEqual(szStr1: szCmdName, szStr2: "fast"))
587 {
588 if (!Game.IsRunning) return false;
589 if (Game.Parameters.isLeague())
590 {
591 Log(id: C4ResStrTableKey::IDS_LOG_COMMANDNOTALLOWEDINLEAGUE);
592 return false;
593 }
594 int32_t iFS;
595 if ((iFS = atoi(nptr: pCmdPar)) == 0) return false;
596 // set frameskip and fullspeed flag
597 Game.FrameSkip = BoundBy<int32_t>(bval: iFS, lbound: 1, rbound: 500);
598 Game.FullSpeed = true;
599 // start calculation immediatly
600 Application.NextTick(fYield: false);
601 return true;
602 }
603 // reset fast mode
604 if (SEqual(szStr1: szCmdName, szStr2: "slow"))
605 {
606 if (!Game.IsRunning) return false;
607 Game.FullSpeed = false;
608 Game.FrameSkip = 1;
609 return true;
610 }
611
612 if (SEqual(szStr1: szCmdName, szStr2: "nodebug"))
613 {
614 if (!Game.IsRunning) return false;
615 Game.Control.DoInput(eCtrlType: CID_Set, pPkt: new C4ControlSet(C4CVT_DisableDebug, 0), eDelivery: CDT_Decide);
616 return true;
617 }
618
619 if (SEqual(szStr1: szCmdName, szStr2: "msgboard"))
620 {
621 if (!Game.IsRunning) return false;
622 // get line cnt
623 int32_t iLineCnt = BoundBy(bval: atoi(nptr: pCmdPar), lbound: 0, rbound: 20);
624 if (iLineCnt == 0)
625 Game.GraphicsSystem.MessageBoard.ChangeMode(inMode: 2);
626 else if (iLineCnt == 1)
627 Game.GraphicsSystem.MessageBoard.ChangeMode(inMode: 0);
628 else
629 {
630 Game.GraphicsSystem.MessageBoard.iLines = iLineCnt;
631 Game.GraphicsSystem.MessageBoard.ChangeMode(inMode: 1);
632 }
633 return true;
634 }
635
636 // kick/activate/deactivate/observer
637 if (SEqual(szStr1: szCmdName, szStr2: "activate") || SEqual(szStr1: szCmdName, szStr2: "deactivate") || SEqual(szStr1: szCmdName, szStr2: "observer"))
638 {
639 if (!Game.Network.isEnabled() || !Game.Network.isHost())
640 {
641 Log(id: C4ResStrTableKey::IDS_MSG_CMD_HOSTONLY); return false;
642 }
643 // search for client
644 C4Client *pClient = Game.Clients.getClientByName(szName: pCmdPar);
645 if (!pClient)
646 {
647 Log(id: C4ResStrTableKey::IDS_MSG_CMD_NOCLIENT, args&: pCmdPar);
648 return false;
649 }
650 // what to do?
651 C4ControlClientUpdate *pCtrl = nullptr;
652 if (szCmdName[0] == 'a') // activate
653 pCtrl = new C4ControlClientUpdate(pClient->getID(), CUT_Activate, true);
654 else if (szCmdName[0] == 'd' && !Game.Parameters.isLeague()) // deactivate
655 pCtrl = new C4ControlClientUpdate(pClient->getID(), CUT_Activate, false);
656 else if (szCmdName[0] == 'o' && !Game.Parameters.isLeague()) // observer
657 pCtrl = new C4ControlClientUpdate(pClient->getID(), CUT_SetObserver);
658 // perform it
659 if (pCtrl)
660 Game.Control.DoInput(eCtrlType: CID_ClientUpdate, pPkt: pCtrl, eDelivery: CDT_Sync);
661 else
662 Log(id: C4ResStrTableKey::IDS_LOG_COMMANDNOTALLOWEDINLEAGUE);
663 return true;
664 }
665
666 // control mode
667 if (SEqual(szStr1: szCmdName, szStr2: "centralctrl") || SEqual(szStr1: szCmdName, szStr2: "decentralctrl") || SEqual(szStr1: szCmdName, szStr2: "asyncctrl"))
668 {
669 if (!Game.Network.isEnabled() || !Game.Network.isHost())
670 {
671 Log(id: C4ResStrTableKey::IDS_MSG_CMD_HOSTONLY); return false;
672 }
673 if (Game.Parameters.isLeague() && *szCmdName == 'a')
674 {
675 Log(id: C4ResStrTableKey::IDS_LOG_COMMANDNOTALLOWEDINLEAGUE); return false;
676 }
677 Game.Network.SetCtrlMode(*szCmdName == 'c' ? CNM_Central : *szCmdName == 'd' ? CNM_Decentral : CNM_Async);
678 return true;
679 }
680
681 // mute
682 if (SEqual(szStr1: szCmdName, szStr2: "mute"))
683 {
684 if (auto *client = Game.Clients.getClientByName(szName: pCmdPar); client)
685 client->SetMuted(true);
686 return true;
687 }
688
689 // unmute
690 if (SEqual(szStr1: szCmdName, szStr2: "unmute"))
691 {
692 if (auto *client = Game.Clients.getClientByName(szName: pCmdPar); client)
693 client->SetMuted(false);
694 return true;
695 }
696
697 // show chart
698 if (Game.IsRunning) if (SEqual(szStr1: szCmdName, szStr2: "chart"))
699 return Game.ToggleChart();
700
701 // custom command
702 if (Game.IsRunning && GetCommand(strName: szCmdName))
703 {
704 const auto *const pLocalPlr = Game.Players.GetLocalByIndex(iIndex: 0);
705 const std::int32_t localPlr = pLocalPlr ? pLocalPlr->Number : NO_OWNER;
706 // add custom command call
707 Game.Control.DoInput(eCtrlType: CID_CustomCommand, pPkt: new C4ControlCustomCommand(localPlr, szCmdName, pCmdPar), eDelivery: CDT_Decide);
708 // ok
709 return true;
710 }
711
712 // unknown command
713 const std::string error{LoadResStr(id: C4ResStrTableKey::IDS_ERR_UNKNOWNCMD, args&: szCmdName)};
714 if (pLobby) pLobby->OnError(szErrMsg: error.c_str()); else LogNTr(message: error);
715 return false;
716}
717
718void C4MessageInput::AddCommand(const std::string &strCommand, const std::string &strScript, C4MessageBoardCommand::Restriction eRestriction)
719{
720 if (GetCommand(strName: strCommand)) return;
721 // create entry
722 Commands[strCommand] = {strScript, eRestriction};
723}
724
725C4MessageBoardCommand *C4MessageInput::GetCommand(const std::string &strName)
726{
727 auto command = Commands.find(x: strName);
728 if (command != Commands.end()) return &command->second;
729 return nullptr;
730}
731
732void C4MessageInput::RemoveCommand(const std::string &command)
733{
734 Commands.erase(x: command);
735}
736
737void C4MessageInput::ClearPointers(C4Object *pObj)
738{
739 // target object loose? stop input
740 C4ChatInputDialog *pDlg = GetTypeIn();
741 if (pDlg && pDlg->GetScriptTargetObject() == pObj) CloseTypeIn();
742}
743
744void C4MessageInput::AbortMsgBoardQuery(C4Object *pObj, int32_t iPlr)
745{
746 // close typein if it is used for the given parameters
747 C4ChatInputDialog *pDlg = GetTypeIn();
748 if (pDlg && pDlg->IsScriptQueried() && pDlg->GetScriptTargetObject() == pObj && pDlg->GetScriptTargetPlayer() == iPlr) CloseTypeIn();
749}
750
751void C4MessageInput::StoreBackBuffer(const char *szMessage)
752{
753 if (!szMessage || !szMessage[0]) return;
754 int32_t i, cnt;
755 // Check: Remove doubled buffer
756 for (i = 0; i < C4MSGB_BackBufferMax - 1; ++i)
757 if (SEqual(szStr1: BackBuffer[i], szStr2: szMessage))
758 break;
759 // Move up buffers
760 for (cnt = i; cnt > 0; cnt--) SCopy(szSource: BackBuffer[cnt - 1], sTarget: BackBuffer[cnt]);
761 // Add message
762 SCopy(szSource: szMessage, sTarget: BackBuffer[0], iMaxL: C4MaxMessage);
763}
764
765const char *C4MessageInput::GetBackBuffer(int32_t iIndex)
766{
767 if (!Inside<int32_t>(ival: iIndex, lbound: 0, rbound: C4MSGB_BackBufferMax - 1)) return nullptr;
768 return BackBuffer[iIndex];
769}
770
771void C4MessageBoardCommand::CompileFunc(StdCompiler *pComp)
772{
773 pComp->Value(rString&: script);
774 pComp->Separator(eSep: StdCompiler::SEP_SEP);
775
776 constexpr StdEnumEntry<Restriction> restrictions[] =
777 {
778 {.Name: "Escaped", .Val: C4MSGCMDR_Escaped},
779 {.Name: "Plain", .Val: C4MSGCMDR_Plain},
780 {.Name: "Identifier", .Val: C4MSGCMDR_Identifier}
781 };
782 pComp->Value(rStruct: mkEnumAdaptT<int>(rVal&: restriction, pNames: restrictions));
783}
784
785bool C4MessageBoardCommand::operator==(const C4MessageBoardCommand &other) const
786{
787 return script == other.script && restriction == other.restriction;
788}
789
790void C4MessageBoardQuery::CompileFunc(StdCompiler *pComp)
791{
792 // note that this CompileFunc does not save the fAnswered-flag, so pending message board queries will be re-asked when resuming SaveGames
793 pComp->Separator(eSep: StdCompiler::SEP_START); // '('
794 // callback object number
795 pComp->Value(rStruct&: pCallbackObj); pComp->Separator();
796 // input query string
797 pComp->Value(rStruct&: sInputQuery); pComp->Separator();
798 // options
799 pComp->Value(rBool&: fIsUppercase);
800 // list end
801 pComp->Separator(eSep: StdCompiler::SEP_END); // ')'
802}
803
804bool C4ChatInputDialog::IsEmpty() const
805{
806 return !*pEdit->GetText();
807}
808