1 /*
2  * OpenClonk, http://www.openclonk.org
3  *
4  * Copyright (c) 2005-2009, RedWolf Design GmbH, http://www.clonk.de/
5  * Copyright (c) 2009-2016, The OpenClonk 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 // handles input dialogs, last-message-buffer, MessageBoard-commands
17 
18 #include "C4Include.h"
19 #include "gui/C4MessageInput.h"
20 
21 #include "control/C4GameControl.h"
22 #include "editor/C4Console.h"
23 #include "game/C4Application.h"
24 #include "game/C4GraphicsSystem.h"
25 #include "graphics/C4GraphicsResource.h"
26 #include "gui/C4Gui.h"
27 #include "gui/C4GameLobby.h"
28 #include "object/C4Object.h"
29 #include "player/C4Player.h"
30 #include "player/C4PlayerList.h"
31 
32 // --------------------------------------------------
33 // C4ChatInputDialog
34 
35 // singleton
36 C4ChatInputDialog *C4ChatInputDialog::pInstance = nullptr;
37 
38 // helper func: Determine whether input text is good for a chat-style-layout dialog
IsSmallInputQuery(const char * szInputQuery)39 bool IsSmallInputQuery(const char *szInputQuery)
40 {
41 	if (!szInputQuery) return true;
42 	int32_t w,h;
43 	if (SCharCount('|', szInputQuery)) return false;
44 	if (!::GraphicsResource.TextFont.GetTextExtent(szInputQuery, w,h, true))
45 		return false; // ???
46 	return w<C4GUI::GetScreenWdt()/5;
47 }
48 
C4ChatInputDialog(bool fObjInput,C4Object * pScriptTarget,bool fUppercase,bool fTeam,int32_t iPlr,const StdStrBuf & rsInputQuery)49 C4ChatInputDialog::C4ChatInputDialog(bool fObjInput, C4Object *pScriptTarget, bool fUppercase, bool fTeam, int32_t iPlr, const StdStrBuf &rsInputQuery)
50 		: C4GUI::InputDialog(fObjInput ? rsInputQuery.getData() : LoadResStrNoAmp("IDS_CTL_CHAT"), nullptr, C4GUI::Ico_None, nullptr, !fObjInput || IsSmallInputQuery(rsInputQuery.getData())),
51 		fObjInput(fObjInput), fUppercase(fUppercase), pTarget(pScriptTarget), iPlr(iPlr), BackIndex(-1), fProcessed(false)
52 {
53 	// singleton-var
54 	pInstance = this;
55 	// set custom edit control
56 	SetCustomEdit(new C4GUI::CallbackEdit<C4ChatInputDialog>(C4Rect(0,0,10,10), this, &C4ChatInputDialog::OnChatInput, &C4ChatInputDialog::OnChatCancel));
57 	// key bindings
58 	pKeyHistoryUp  = new C4KeyBinding(C4KeyCodeEx(K_UP  ), "ChatHistoryUp"  , KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<C4ChatInputDialog, bool>(*this, true , &C4ChatInputDialog::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
59 	pKeyHistoryDown= new C4KeyBinding(C4KeyCodeEx(K_DOWN), "ChatHistoryDown", KEYSCOPE_Gui, new C4GUI::DlgKeyCBEx<C4ChatInputDialog, bool>(*this, false, &C4ChatInputDialog::KeyHistoryUpDown), C4CustomKey::PRIO_CtrlOverride);
60 	pKeyAbort = new C4KeyBinding(C4KeyCodeEx(K_F2), "ChatAbort", KEYSCOPE_Gui, new C4GUI::DlgKeyCB<C4GUI::Dialog>(*this, &C4GUI::Dialog::KeyEscape), C4CustomKey::PRIO_CtrlOverride);
61 	pKeyNickComplete = new C4KeyBinding(C4KeyCodeEx(K_TAB), "ChatNickComplete", KEYSCOPE_Gui, new C4GUI::DlgKeyCB<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyCompleteNick), C4CustomKey::PRIO_CtrlOverride);
62 	pKeyPlrControl = new C4KeyBinding(C4KeyCodeEx(KEY_Any, KEYS_Control), "ChatForwardPlrCtrl", KEYSCOPE_Gui, new C4GUI::DlgKeyCBPassKey<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyPlrControl), C4CustomKey::PRIO_Dlg);
63 	pKeyGamepadControl = new C4KeyBinding(C4KeyCodeEx(KEY_Any), "ChatForwardGamepadCtrl", KEYSCOPE_Gui, new C4GUI::DlgKeyCBPassKey<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyGamepadControlDown, &C4ChatInputDialog::KeyGamepadControlUp, &C4ChatInputDialog::KeyGamepadControlPressed), C4CustomKey::PRIO_PlrControl);
64 	pKeyBackClose = new C4KeyBinding(C4KeyCodeEx(K_BACK), "ChatBackspaceClose", KEYSCOPE_Gui, new C4GUI::DlgKeyCB<C4ChatInputDialog>(*this, &C4ChatInputDialog::KeyBackspaceClose), C4CustomKey::PRIO_CtrlOverride);
65 	// free when closed...
66 	SetDelOnClose();
67 	// initial team text
68 	if (fTeam) pEdit->InsertText("/team ", true);
69 }
70 
~C4ChatInputDialog()71 C4ChatInputDialog::~C4ChatInputDialog()
72 {
73 	delete pKeyHistoryUp;
74 	delete pKeyHistoryDown;
75 	delete pKeyAbort;
76 	delete pKeyNickComplete;
77 	delete pKeyPlrControl;
78 	delete pKeyGamepadControl;
79 	delete pKeyBackClose;
80 	if (this==pInstance) pInstance=nullptr;
81 }
82 
OnChatCancel()83 void C4ChatInputDialog::OnChatCancel()
84 {
85 	// abort chat: Make sure msg board query is aborted
86 	fProcessed = true;
87 	if (fObjInput)
88 	{
89 		// check if the target input is still valid
90 		C4Player *pPlr = ::Players.Get(iPlr);
91 		if (!pPlr) return;
92 		if (pPlr->MarkMessageBoardQueryAnswered(pTarget))
93 		{
94 			// there was an associated query - it must be removed on all clients synchronized via queue
95 			::Control.DoInput(CID_MsgBoardReply, new C4ControlMsgBoardReply(nullptr, pTarget ? pTarget->Number : 0, iPlr), CDT_Decide);
96 		}
97 	}
98 }
99 
OnClosed(bool fOK)100 void C4ChatInputDialog::OnClosed(bool fOK)
101 {
102 	// make sure chat input is processed, even if closed by other means than Enter on edit
103 	if (!fProcessed)
104 		if (fOK)
105 				OnChatInput(pEdit, false, false); else OnChatCancel();
106 	else
107 		OnChatCancel();
108 	typedef C4GUI::InputDialog BaseDlg;
109 	BaseDlg::OnClosed(fOK);
110 }
111 
OnChatInput(C4GUI::Edit * edt,bool fPasting,bool fPastingMore)112 C4GUI::Edit::InputResult C4ChatInputDialog::OnChatInput(C4GUI::Edit *edt, bool fPasting, bool fPastingMore)
113 {
114 	// no double processing
115 	if (fProcessed) return C4GUI::Edit::IR_CloseDlg;
116 	// get edit text
117 	auto *pEdt = reinterpret_cast<C4GUI::Edit *>(edt);
118 	auto *szInputText = const_cast<char *>(pEdt->GetText());
119 	// Store to back buffer
120 	::MessageInput.StoreBackBuffer(szInputText);
121 	// script queried input?
122 	if (fObjInput)
123 	{
124 		fProcessed = true;
125 		// check if the target input is still valid
126 		C4Player *pPlr = ::Players.Get(iPlr);
127 		if (!pPlr) return C4GUI::Edit::IR_CloseDlg;
128 		if (!pPlr->MarkMessageBoardQueryAnswered(pTarget))
129 		{
130 			// there was no associated query!
131 			return C4GUI::Edit::IR_CloseDlg;
132 		}
133 		// then do a script callback, incorporating the input into the answer
134 		if (fUppercase) SCapitalize(szInputText);
135 		::Control.DoInput(CID_MsgBoardReply, new C4ControlMsgBoardReply(szInputText, pTarget ? pTarget->Number : 0, iPlr), CDT_Decide);
136 		return C4GUI::Edit::IR_CloseDlg;
137 	}
138 	else
139 		// reroute to message input class
140 		::MessageInput.ProcessInput(szInputText);
141 	// safety: message board commands may do strange things
142 	if (this!=pInstance) return C4GUI::Edit::IR_Abort;
143 	// select all text to be removed with next keypress
144 	// just for pasting mode; usually the dlg will be closed now anyway
145 	pEdt->SelectAll();
146 	// avoid dlg close, if more content is to be pasted
147 	if (fPastingMore) return C4GUI::Edit::IR_None;
148 	fProcessed = true;
149 	return C4GUI::Edit::IR_CloseDlg;
150 }
151 
KeyHistoryUpDown(bool fUp)152 bool C4ChatInputDialog::KeyHistoryUpDown(bool fUp)
153 {
154 	// browse chat history
155 	pEdit->SelectAll(); pEdit->DeleteSelection();
156 	const char *szPrevInput = ::MessageInput.GetBackBuffer(fUp ? (++BackIndex) : (--BackIndex));
157 	if (!szPrevInput || !*szPrevInput)
158 		BackIndex = -1;
159 	else
160 	{
161 		pEdit->InsertText(szPrevInput, true);
162 		pEdit->SelectAll();
163 	}
164 	return true;
165 }
166 
KeyPlrControl(const C4KeyCodeEx & key)167 bool C4ChatInputDialog::KeyPlrControl(const C4KeyCodeEx &key)
168 {
169 	// Control pressed while doing this key: Reroute this key as a player-control
170 	Game.DoKeyboardInput(WORD(key.Key), KEYEV_Down, !!(key.dwShift & KEYS_Alt), false, !!(key.dwShift & KEYS_Shift), key.IsRepeated(), nullptr, true);
171 	// mark as processed, so it won't get any double processing
172 	return true;
173 }
174 
KeyGamepadControlDown(const C4KeyCodeEx & key)175 bool C4ChatInputDialog::KeyGamepadControlDown(const C4KeyCodeEx &key)
176 {
177 	// filter gamepad control
178 	if (!Key_IsGamepad(key.Key)) return false;
179 	// forward it
180 	Game.DoKeyboardInput(key.Key, KEYEV_Down, false, false, false, key.IsRepeated(), nullptr, true);
181 	return true;
182 }
183 
KeyGamepadControlUp(const C4KeyCodeEx & key)184 bool C4ChatInputDialog::KeyGamepadControlUp(const C4KeyCodeEx &key)
185 {
186 	// filter gamepad control
187 	if (!Key_IsGamepad(key.Key)) return false;
188 	// forward it
189 	Game.DoKeyboardInput(key.Key, KEYEV_Up, false, false, false, key.IsRepeated(), nullptr, true);
190 	return true;
191 }
192 
KeyGamepadControlPressed(const C4KeyCodeEx & key)193 bool C4ChatInputDialog::KeyGamepadControlPressed(const C4KeyCodeEx &key)
194 {
195 	// filter gamepad control
196 	if (!Key_IsGamepad(key.Key)) return false;
197 	// forward it
198 	Game.DoKeyboardInput(key.Key, KEYEV_Pressed, false, false, false, key.IsRepeated(), nullptr, true);
199 	return true;
200 }
201 
KeyBackspaceClose()202 bool C4ChatInputDialog::KeyBackspaceClose()
203 {
204 	// close if chat text box is empty (on backspace)
205 	if (pEdit->GetText() && *pEdit->GetText()) return false;
206 	Close(false);
207 	return true;
208 }
209 
KeyCompleteNick()210 bool C4ChatInputDialog::KeyCompleteNick()
211 {
212 	if (!pEdit) return false;
213 	char IncompleteNick[256+1];
214 	// get current word in edit
215 	if (!pEdit->GetCurrentWord(IncompleteNick, 256)) return false;
216 	if (!*IncompleteNick) return false;
217 	C4Player *plr = ::Players.First;
218 	while (plr)
219 	{
220 		// Compare name and input
221 		if (SEqualNoCase(plr->GetName(), IncompleteNick, SLen(IncompleteNick)))
222 		{
223 			pEdit->InsertText(plr->GetName() + SLen(IncompleteNick), true);
224 			return true;
225 		}
226 		else
227 			plr = plr->Next;
228 	}
229 	// no match found
230 	return false;
231 }
232 
233 
234 // --------------------------------------------------
235 // C4MessageInput
236 
Init()237 bool C4MessageInput::Init()
238 {
239 	// add default commands
240 	if (!pCommands)
241 	{
242 		AddCommand("speed", "SetGameSpeed(%d)");
243 	}
244 	return true;
245 }
246 
Default()247 void C4MessageInput::Default()
248 {
249 	// clear backlog
250 	for (auto & cnt : BackBuffer) cnt[0]=0;
251 }
252 
Clear()253 void C4MessageInput::Clear()
254 {
255 	// close any dialog
256 	CloseTypeIn();
257 	// free messageboard-commands
258 	C4MessageBoardCommand *pCmd;
259 	while ((pCmd = pCommands))
260 	{
261 		pCommands = pCmd->Next;
262 		delete pCmd;
263 	}
264 }
265 
CloseTypeIn()266 bool C4MessageInput::CloseTypeIn()
267 {
268 	// close dialog if present and valid
269 	C4ChatInputDialog *pDlg = GetTypeIn();
270 	if (!pDlg) return false;
271 	pDlg->Close(false);
272 	return true;
273 }
274 
StartTypeIn(bool fObjInput,C4Object * pObj,bool fUpperCase,bool fTeam,int32_t iPlr,const StdStrBuf & rsInputQuery)275 bool C4MessageInput::StartTypeIn(bool fObjInput, C4Object *pObj, bool fUpperCase, bool fTeam, int32_t iPlr, const StdStrBuf &rsInputQuery)
276 {
277 	// close any previous
278 	if (IsTypeIn()) CloseTypeIn();
279 	// start new
280 	return ::pGUI->ShowRemoveDlg(new C4ChatInputDialog(fObjInput, pObj, fUpperCase, fTeam, iPlr, rsInputQuery));
281 }
282 
KeyStartTypeIn(bool fTeam)283 bool C4MessageInput::KeyStartTypeIn(bool fTeam)
284 {
285 	// fullscreen only
286 	if (Application.isEditor) return false;
287 	// OK, start typing
288 	return StartTypeIn(false, nullptr, false, fTeam);
289 }
290 
ToggleTypeIn()291 bool C4MessageInput::ToggleTypeIn()
292 {
293 	// toggle off?
294 	if (IsTypeIn())
295 	{
296 		// no accidental close of script queried dlgs by chat request
297 		if (GetTypeIn()->IsScriptQueried()) return false;
298 		return CloseTypeIn();
299 	}
300 	else
301 		// toggle on!
302 		return StartTypeIn();
303 }
304 
IsTypeIn()305 bool C4MessageInput::IsTypeIn()
306 {
307 	// check GUI and dialog
308 	return C4ChatInputDialog::IsShown();
309 }
310 
ProcessInput(const char * szText)311 bool C4MessageInput::ProcessInput(const char *szText)
312 {
313 	// helper variables
314 	char OSTR[402]; // cba
315 	C4ControlMessageType eMsgType;
316 	const char *szMsg = nullptr;
317 	int32_t iToPlayer = -1;
318 
319 	// Starts with '^', "team:" or "/team ": Team message
320 	if (szText[0] == '^' || SEqual2NoCase(szText, "team:") || SEqual2NoCase(szText, "/team "))
321 	{
322 		if (!Game.Teams.IsTeamVisible())
323 		{
324 			// team not known; can't send!
325 			Log(LoadResStr("IDS_MSG_CANTSENDTEAMMESSAGETEAMSN"));
326 			return false;
327 		}
328 		else
329 		{
330 			eMsgType = C4CMT_Team;
331 			szMsg = szText[0] == '^' ? szText+1 :
332 			        szText[0] == '/' ? szText+6 : szText+5;
333 		}
334 	}
335 	// Starts with "/private ": Private message (running game only)
336 	else if (Game.IsRunning && SEqual2NoCase(szText, "/private "))
337 	{
338 		// get target name
339 		char szTargetPlr[C4MaxName + 1];
340 		SCopyUntil(szText + 9, szTargetPlr, ' ', C4MaxName);
341 		// search player
342 		C4Player *pToPlr = ::Players.GetByName(szTargetPlr);
343 		if (!pToPlr) return false;
344 		// set
345 		eMsgType = C4CMT_Private;
346 		iToPlayer = pToPlr->Number;
347 		szMsg = szText + 10 + SLen(szText);
348 		if (szMsg > szText + SLen(szText)) return false;
349 	}
350 	// Starts with "/me ": Me-Message
351 	else if (SEqual2NoCase(szText, "/me "))
352 	{
353 		eMsgType = C4CMT_Me;
354 		szMsg = szText+4;
355 	}
356 	// Starts with "/sound ": Sound-Message
357 	else if (SEqual2NoCase(szText, "/sound "))
358 	{
359 		eMsgType = C4CMT_Sound;
360 		szMsg = szText+7;
361 	}
362 	// Disabled due to spamming
363 	// Starts with "/alert": Taskbar flash (message optional)
364 	else if (SEqual2NoCase(szText, "/alert ") || SEqualNoCase(szText, "/alert"))
365 	{
366 		eMsgType = C4CMT_Alert;
367 		szMsg = szText+6;
368 		if (*szMsg) ++szMsg;
369 	}
370 	// Starts with '"': Let the clonk say it
371 	else if (Game.IsRunning && szText[0] == '"')
372 	{
373 		eMsgType = C4CMT_Say;
374 		// Append '"', if neccessary
375 		StdStrBuf text(szText);
376 		SCopy(szText, OSTR, 400);
377 		char *pEnd = OSTR + SLen(OSTR) - 1;
378 		if (*pEnd != '"') { *++pEnd = '"'; *++pEnd = 0; }
379 		szMsg = OSTR;
380 	}
381 	// Starts with '/': Command
382 	else if (szText[0] == '/')
383 		return ProcessCommand(szText);
384 	// Regular message
385 	else
386 	{
387 		eMsgType = C4CMT_Normal;
388 		szMsg = szText;
389 	}
390 
391 	// message?
392 	if (szMsg)
393 	{
394 		char szMessage[C4MaxMessage + 1];
395 		// go over whitespaces, check empty message
396 		while (IsWhiteSpace(*szMsg)) szMsg++;
397 		if (!*szMsg)
398 		{
399 			if (eMsgType != C4CMT_Alert) return true;
400 			*szMessage = '\0';
401 		}
402 		else
403 		{
404 			// trim right
405 			const char *szEnd = szMsg + SLen(szMsg) - 1;
406 			while (IsWhiteSpace(*szEnd) && szEnd >= szMsg) szEnd--;
407 			// Say: Strip quotation marks in cinematic film mode
408 			if (Game.C4S.Head.Film == C4SFilm_Cinematic)
409 			{
410 				if (eMsgType == C4CMT_Say) { ++szMsg; szEnd--; }
411 			}
412 			// get message
413 			SCopy(szMsg, szMessage, std::min<ptrdiff_t>(C4MaxMessage, szEnd - szMsg + 1));
414 		}
415 		// get sending player (if any)
416 		C4Player *pPlr = Game.IsRunning ? ::Players.GetLocalByIndex(0) : nullptr;
417 		// send
418 		::Control.DoInput(CID_Message,
419 		                  new C4ControlMessage(eMsgType, szMessage, pPlr ? pPlr->Number : -1, iToPlayer),
420 		                  CDT_Private);
421 	}
422 
423 	return true;
424 }
425 
ProcessCommand(const char * szCommand)426 bool C4MessageInput::ProcessCommand(const char *szCommand)
427 {
428 	C4GameLobby::MainDlg *pLobby = ::Network.GetLobby();
429 	// command
430 	// must be 1 char longer than the longest command only. If given commands are longer, they will be truncated, and such a command won't exist anyway
431 	const int32_t MaxCommandLen = 20;
432 	char szCmdName[MaxCommandLen + 1];
433 	SCopyUntil(szCommand + 1, szCmdName, ' ', MaxCommandLen);
434 	// parameter
435 	const char *pCmdPar = SSearch(szCommand, " ");
436 	if (!pCmdPar) pCmdPar = "";
437 
438 	// CAUTION when implementing special commands (like /quit) here:
439 	// those must not be executed when text is pasted, because that could crash the GUI system
440 	// when there are additional lines to paste, but the edit field is destructed by the command
441 
442 	// lobby-only commands
443 	if (!Game.IsRunning && SEqualNoCase(szCmdName, "joinplr"))
444 	{
445 		// compose path from given filename
446 		StdStrBuf plrPath;
447 		plrPath.Format("%s%s", Config.General.UserDataPath, pCmdPar);
448 		// player join - check filename
449 		if (!ItemExists(plrPath.getData()))
450 		{
451 			C4GameLobby::LobbyError(FormatString(LoadResStr("IDS_MSG_CMD_JOINPLR_NOFILE"), plrPath.getData()).getData());
452 		}
453 		else
454 			::Network.Players.JoinLocalPlayer(plrPath.getData());
455 		return true;
456 	}
457 	if (!Game.IsRunning && SEqualNoCase(szCmdName, "plrclr"))
458 	{
459 		// get player name from input text
460 		int iSepPos = SCharPos(' ', pCmdPar, 0);
461 		C4PlayerInfo *pNfo=nullptr;
462 		int32_t idLocalClient = -1;
463 		if (::Network.Clients.GetLocal()) idLocalClient = ::Network.Clients.GetLocal()->getID();
464 		if (iSepPos>0)
465 		{
466 			// a player name is given: Parse it
467 			StdStrBuf sPlrName;
468 			sPlrName.Copy(pCmdPar, iSepPos);
469 			pCmdPar += iSepPos+1; int32_t id=0;
470 			while ((pNfo = Game.PlayerInfos.GetNextPlayerInfoByID(id)))
471 			{
472 				id = pNfo->GetID();
473 				if (WildcardMatch(sPlrName.getData(), pNfo->GetName())) break;
474 			}
475 		}
476 		else
477 			// no player name: Set local player
478 			pNfo = Game.PlayerInfos.GetPrimaryInfoByClientID(idLocalClient);
479 		C4ClientPlayerInfos *pCltNfo=nullptr;
480 		if (pNfo) pCltNfo = Game.PlayerInfos.GetClientInfoByPlayerID(pNfo->GetID());
481 		if (!pCltNfo)
482 		{
483 			C4GameLobby::LobbyError(LoadResStr("IDS_MSG_CMD_PLRCLR_NOPLAYER"));
484 		}
485 		else
486 		{
487 			// may color of this client be set?
488 			if (pCltNfo->GetClientID() != idLocalClient && !::Network.isHost())
489 			{
490 				C4GameLobby::LobbyError(LoadResStr("IDS_MSG_CMD_PLRCLR_NOACCESS"));
491 			}
492 			else
493 			{
494 				// get color to set
495 				uint32_t dwNewClr;
496 				if (sscanf(pCmdPar, "%x", &dwNewClr) != 1)
497 				{
498 					C4GameLobby::LobbyError(LoadResStr("IDS_MSG_CMD_PLRCLR_USAGE"));
499 				}
500 				else
501 				{
502 					// color validation
503 					dwNewClr |= 0xff000000;
504 					if (!dwNewClr) ++dwNewClr;
505 					// request a color change to this color
506 					C4ClientPlayerInfos LocalInfoRequest = *pCltNfo;
507 					C4PlayerInfo *pPlrInfo = LocalInfoRequest.GetPlayerInfoByID(pNfo->GetID());
508 					assert(pPlrInfo);
509 					if (pPlrInfo)
510 					{
511 						pPlrInfo->SetOriginalColor(dwNewClr); // set this as a new color wish
512 						::Network.Players.RequestPlayerInfoUpdate(LocalInfoRequest);
513 					}
514 				}
515 			}
516 		}
517 		return true;
518 	}
519 	if (!Game.IsRunning && SEqualNoCase(szCmdName, "start"))
520 	{
521 		// timeout given?
522 		int32_t iTimeout = Config.Lobby.CountdownTime;
523 		if (!::Network.isHost())
524 			C4GameLobby::LobbyError(LoadResStr("IDS_MSG_CMD_HOSTONLY"));
525 		else if (pCmdPar && *pCmdPar && (!sscanf(pCmdPar, "%d", &iTimeout) || iTimeout<0))
526 			C4GameLobby::LobbyError(LoadResStr("IDS_MSG_CMD_START_USAGE"));
527 		else if (pLobby)
528 		{
529 			// abort previous countdown
530 			if (::Network.isLobbyCountDown()) ::Network.AbortLobbyCountdown();
531 			// start new countdown (aborts previous if necessary)
532 			pLobby->Start(iTimeout);
533 		}
534 		else
535 		{
536 			if (iTimeout)
537 				::Network.StartLobbyCountdown(iTimeout);
538 			else
539 				::Network.Start();
540 		}
541 		return true;
542 	}
543 	if (!Game.IsRunning && SEqualNoCase(szCmdName, "abort"))
544 	{
545 		if (!::Network.isHost())
546 			C4GameLobby::LobbyError(LoadResStr("IDS_MSG_CMD_HOSTONLY"));
547 		else if (::Network.isLobbyCountDown())
548 			::Network.AbortLobbyCountdown();
549 		else
550 			C4GameLobby::LobbyError(LoadResStr("IDS_MSG_CMD_ABORT_NOCOUNTDOWN"));
551 		return true;
552 	}
553 
554 	if (SEqual(szCmdName, "help"))
555 	{
556 		Log(LoadResStr(pLobby ? "IDS_TEXT_COMMANDSAVAILABLEDURINGLO" : "IDS_TEXT_COMMANDSAVAILABLEDURINGGA"));
557 		if (!Game.IsRunning)
558 		{
559 			LogF("/start [time] - %s", LoadResStr("IDS_TEXT_STARTTHEROUNDWITHSPECIFIE"));
560 			LogF("/abort - %s", LoadResStr("IDS_TEXT_ABORTSTARTCOUNTDOWN"));
561 			LogF("/alert - %s", LoadResStr("IDS_TEXT_ALERTTHEHOSTIFTHEHOSTISAW"));
562 			LogF("/joinplr [filename] - %s", LoadResStr("IDS_TEXT_JOINALOCALPLAYERFROMTHESP"));
563 			LogF("/plrclr [player] [RGB] - %s", LoadResStr("IDS_TEXT_CHANGETHECOLOROFTHESPECIF"));
564 			LogF("/plrclr [RGB] - %s", LoadResStr("IDS_TEXT_CHANGEYOUROWNPLAYERCOLOR"));
565 		}
566 		else
567 		{
568 			LogF("/fast [x] - %s", LoadResStr("IDS_TEXT_SETTOFASTMODESKIPPINGXFRA"));
569 			LogF("/slow - %s", LoadResStr("IDS_TEXT_SETTONORMALSPEEDMODE"));
570 			LogF("/chart - %s", LoadResStr("IDS_TEXT_DISPLAYNETWORKSTATISTICS"));
571 			LogF("/nodebug - %s", LoadResStr("IDS_TEXT_PREVENTDEBUGMODEINTHISROU"));
572 			LogF("/script [script] - %s", LoadResStr("IDS_TEXT_EXECUTEASCRIPTCOMMAND"));
573 			LogF("/screenshot [zoom] - %s", LoadResStr("IDS_TEXT_SAFEZOOMEDFULLSCREENSHOT"));
574 		}
575 		LogF("/kick [client] - %s", LoadResStr("IDS_TEXT_KICKTHESPECIFIEDCLIENT"));
576 		LogF("/observer [client] - %s", LoadResStr("IDS_TEXT_SETTHESPECIFIEDCLIENTTOOB"));
577 		LogF("/me [action] - %s", LoadResStr("IDS_TEXT_PERFORMANACTIONINYOURNAME"));
578 		LogF("/sound [sound] - %s", LoadResStr("IDS_TEXT_PLAYASOUNDFROMTHEGLOBALSO"));
579 		if (Game.IsRunning) LogF("/private [player] [message] - %s", LoadResStr("IDS_MSG_SENDAPRIVATEMESSAGETOTHES"));
580 		LogF("/team [message] - %s", LoadResStr("IDS_MSG_SENDAPRIVATEMESSAGETOYOUR"));
581 		LogF("/set comment [comment] - %s", LoadResStr("IDS_TEXT_SETANEWNETWORKCOMMENT"));
582 		LogF("/set password [password] - %s", LoadResStr("IDS_TEXT_SETANEWNETWORKPASSWORD"));
583 		LogF("/set maxplayer [number] - %s", LoadResStr("IDS_TEXT_SETANEWMAXIMUMNUMBEROFPLA"));
584 		LogF("/todo [text] - %s", LoadResStr("IDS_TEXT_ADDTODO"));
585 		LogF("/clear - %s", LoadResStr("IDS_MSG_CLEARTHEMESSAGEBOARD"));
586 		return true;
587 	}
588 	// dev-scripts
589 	if (SEqual(szCmdName, "script"))
590 	{
591 		if (!Game.IsRunning) return false;
592 		if (!Game.DebugMode) return false;
593 
594 		::Control.DoInput(CID_Script, new C4ControlScript(pCmdPar, C4ControlScript::SCOPE_Console), CDT_Decide);
595 		return true;
596 	}
597 	// set runtime properties
598 	if (SEqual(szCmdName, "set"))
599 	{
600 		if (SEqual2(pCmdPar, "maxplayer "))
601 		{
602 			if (::Control.isCtrlHost())
603 			{
604 				if (atoi(pCmdPar+10) == 0 && !SEqual(pCmdPar+10, "0"))
605 				{
606 					Log("Syntax: /set maxplayer count");
607 					return false;
608 				}
609 				::Control.DoInput(CID_Set,
610 				                  new C4ControlSet(C4CVT_MaxPlayer, atoi(pCmdPar+10)),
611 				                  CDT_Decide);
612 				return true;
613 			}
614 		}
615 		if (SEqual2(pCmdPar, "comment ") || SEqual(pCmdPar, "comment"))
616 		{
617 			if (!::Network.isEnabled() || !::Network.isHost()) return false;
618 			// Set in configuration, update reference
619 			Config.Network.Comment.CopyValidated(pCmdPar[7] ? (pCmdPar+8) : "");
620 			::Network.InvalidateReference();
621 			Log(LoadResStr("IDS_NET_COMMENTCHANGED"));
622 			return true;
623 		}
624 		if (SEqual2(pCmdPar, "password ") || SEqual(pCmdPar, "password"))
625 		{
626 			if (!::Network.isEnabled() || !::Network.isHost()) return false;
627 			::Network.SetPassword(pCmdPar[8] ? (pCmdPar+9) : nullptr);
628 			if (pLobby) pLobby->UpdatePassword();
629 			return true;
630 		}
631 		// unknown property
632 		return false;
633 	}
634 	// get szen from network folder - not in lobby; use res tab there
635 	if (SEqual(szCmdName, "netgetscen"))
636 	{
637 		if (::Network.isEnabled() && !::Network.isHost() && !pLobby)
638 		{
639 			const C4Network2ResCore *pResCoreScen = Game.Parameters.Scenario.getResCore();
640 			if (pResCoreScen)
641 			{
642 				C4Network2Res::Ref pScenario = ::Network.ResList.getRefRes(pResCoreScen->getID());
643 				if (pScenario)
644 					if (C4Group_CopyItem(pScenario->getFile(), Config.AtUserDataPath(GetFilename(Game.ScenarioFilename))))
645 					{
646 						LogF(LoadResStr("IDS_MSG_CMD_NETGETSCEN_SAVED"), Config.AtUserDataPath(GetFilename(Game.ScenarioFilename)));
647 						return true;
648 					}
649 			}
650 		}
651 		return false;
652 	}
653 	// clear message board
654 	if (SEqual(szCmdName, "clear"))
655 	{
656 		// lobby
657 		if (pLobby)
658 		{
659 			pLobby->ClearLog();
660 		}
661 		// fullscreen
662 		else if (::GraphicsSystem.MessageBoard)
663 			::GraphicsSystem.MessageBoard->ClearLog();
664 		else
665 		{
666 			// EM mode
667 			Console.ClearLog();
668 		}
669 		return true;
670 	}
671 	// kick client
672 	if (SEqual(szCmdName, "kick"))
673 	{
674 		if (::Network.isEnabled() && ::Network.isHost())
675 		{
676 			// find client
677 			C4Client *pClient = Game.Clients.getClientByName(pCmdPar);
678 			if (!pClient)
679 			{
680 				LogF(LoadResStr("IDS_MSG_CMD_NOCLIENT"), pCmdPar);
681 				return false;
682 			}
683 			// league: Kick needs voting
684 			if (Game.Parameters.isLeague() && ::Players.GetAtClient(pClient->getID()))
685 				::Network.Vote(VT_Kick, true, pClient->getID());
686 			else
687 				// add control
688 				Game.Clients.CtrlRemove(pClient, LoadResStr("IDS_MSG_KICKFROMMSGBOARD"));
689 		}
690 		return true;
691 	}
692 	// set fast mode
693 	if (SEqual(szCmdName, "fast"))
694 	{
695 		if (!Game.IsRunning) return false;
696 		int32_t iFS;
697 		if ((iFS=atoi(pCmdPar)) == 0) return false;
698 		// set frameskip and fullspeed flag
699 		Game.FrameSkip=Clamp<int32_t>(iFS,1,500);
700 		Game.FullSpeed=true;
701 		// start calculation immediatly
702 		Application.NextTick();
703 		return true;
704 	}
705 	// reset fast mode
706 	if (SEqual(szCmdName, "slow"))
707 	{
708 		if (!Game.IsRunning) return false;
709 		Game.FullSpeed=false;
710 		Game.FrameSkip=1;
711 		return true;
712 	}
713 
714 	if (SEqual(szCmdName, "nodebug"))
715 	{
716 		if (!Game.IsRunning) return false;
717 		::Control.DoInput(CID_Set, new C4ControlSet(C4CVT_DisableDebug, 0), CDT_Decide);
718 		return true;
719 	}
720 
721 	// kick/activate/deactivate/observer
722 	if (SEqual(szCmdName, "activate") || SEqual(szCmdName, "deactivate") || SEqual(szCmdName, "observer"))
723 	{
724 		if (!::Network.isEnabled() || !::Network.isHost())
725 			{ Log(LoadResStr("IDS_MSG_CMD_HOSTONLY")); return false; }
726 		// search for client
727 		C4Client *pClient = Game.Clients.getClientByName(pCmdPar);
728 		if (!pClient)
729 		{
730 			LogF(LoadResStr("IDS_MSG_CMD_NOCLIENT"), pCmdPar);
731 			return false;
732 		}
733 		// what to do?
734 		C4ControlClientUpdate *pCtrl = nullptr;
735 		if (szCmdName[0] == 'a') // activate
736 			pCtrl = new C4ControlClientUpdate(pClient->getID(), CUT_Activate, true);
737 		else if (szCmdName[0] == 'd' && !Game.Parameters.isLeague()) // deactivate
738 			pCtrl = new C4ControlClientUpdate(pClient->getID(), CUT_Activate, false);
739 		else if (szCmdName[0] == 'o' && !Game.Parameters.isLeague()) // observer
740 			pCtrl = new C4ControlClientUpdate(pClient->getID(), CUT_SetObserver);
741 		// perform it
742 		if (pCtrl)
743 			::Control.DoInput(CID_ClientUpdate, pCtrl, CDT_Sync);
744 		else
745 			Log(LoadResStr("IDS_LOG_COMMANDNOTALLOWEDINLEAGUE"));
746 		return true;
747 	}
748 
749 	// control mode
750 	if (SEqual(szCmdName, "centralctrl") || SEqual(szCmdName, "decentralctrl")  || SEqual(szCmdName, "asyncctrl"))
751 	{
752 		if (!::Network.isEnabled() || !::Network.isHost())
753 			{ Log(LoadResStr("IDS_MSG_CMD_HOSTONLY")); return false; }
754 		::Network.SetCtrlMode(*szCmdName == 'c' ? CNM_Central : *szCmdName == 'd' ? CNM_Decentral : CNM_Async);
755 		return true;
756 	}
757 
758 	// show chart
759 	if (Game.IsRunning)
760 		if (SEqual(szCmdName, "chart"))
761 			return Game.ToggleChart();
762 
763 	// whole map screenshot
764 	if (SEqual(szCmdName, "screenshot"))
765 	{
766 		double zoom = atof(pCmdPar);
767 		if (zoom<=0) zoom = 2;
768 		::GraphicsSystem.SaveScreenshot(true, zoom);
769 		return true;
770 	}
771 
772 	// add to TODO list
773 	if (SEqual(szCmdName, "todo"))
774 	{
775 		// must add something
776 		if (!pCmdPar || !*pCmdPar) return false;
777 		// try writing main file (usually {SCENARIO}/TODO.txt); if access is not possible, e.g. because scenario is packed, write to alternate file
778 		const char *todo_filenames[] = { ::Config.Developer.TodoFilename, ::Config.Developer.AltTodoFilename };
779 		bool success = false;
780 		for (auto & i : todo_filenames)
781 		{
782 			StdCopyStrBuf todo_filename(i);
783 			todo_filename.Replace("{USERPATH}", Config.General.UserDataPath);
784 			int replacements = todo_filename.Replace("{SCENARIO}", Game.ScenarioFile.GetFullName().getData());
785 			// sanity checks for writing scenario TODO file
786 			if (replacements)
787 			{
788 				// entered in editor with no file open?
789 				if (!::Game.ScenarioFile.IsOpen()) continue;
790 				// not into packed
791 				if (::Game.ScenarioFile.IsPacked()) continue;
792 				// not into temp network file
793 				if (::Control.isNetwork() && !::Control.isCtrlHost()) continue;
794 			}
795 			// try to append. May fail e.g. on packed scenario file, name getting too long, etc. Then fallback to alternate location.
796 			CStdFile todo_file;
797 			if (!todo_file.Append(todo_filename.getData())) continue;
798 			if (!todo_file.WriteString(pCmdPar)) continue;
799 			// check on file close because CStdFile may do a delayed write
800 			if (!todo_file.Close()) continue;
801 			success = true;
802 			break;
803 		}
804 		// no message on success to avoid cluttering the chat during debug sessions
805 		if (!success) Log(LoadResStr("IDS_ERR_TODO"));
806 		return true;
807 	}
808 
809 	// custom command
810 	C4MessageBoardCommand *pCmd;
811 	if (Game.IsRunning)
812 		if ((pCmd = GetCommand(szCmdName)))
813 		{
814 			// get player number of first local player; if multiple players
815 			// share one computer, we can't distinguish between them anyway
816 			int32_t player_num = NO_OWNER;
817 			C4Player *player = ::Players.GetLocalByIndex(0);
818 			if (player) player_num = player->Number;
819 
820 			// send command to network
821 			::Control.DoInput(CID_MsgBoardCmd, new C4ControlMsgBoardCmd(szCmdName, pCmdPar, player_num), CDT_Decide);
822 
823 			// ok
824 			return true;
825 		}
826 
827 	// unknown command
828 	StdStrBuf sErr; sErr.Format(LoadResStr("IDS_ERR_UNKNOWNCMD"), szCmdName);
829 	if (pLobby) pLobby->OnError(sErr.getData()); else Log(sErr.getData());
830 	return false;
831 }
832 
AddCommand(const char * strCommand,const char * strScript)833 void C4MessageInput::AddCommand(const char *strCommand, const char *strScript)
834 {
835 	if (GetCommand(strCommand)) return;
836 	// create entry
837 	C4MessageBoardCommand *pCmd = new C4MessageBoardCommand();
838 	SCopy(strCommand, pCmd->Name, C4MaxName);
839 	SCopy(strScript, pCmd->Script, _MAX_FNAME+30);
840 	// add to list
841 	pCmd->Next = pCommands; pCommands = pCmd;
842 }
843 
GetCommand(const char * strName)844 C4MessageBoardCommand *C4MessageInput::GetCommand(const char *strName)
845 {
846 	for (C4MessageBoardCommand *pCmd = pCommands; pCmd; pCmd = pCmd->Next)
847 		if (SEqual(pCmd->Name, strName))
848 			return pCmd;
849 	return nullptr;
850 }
851 
ClearPointers(C4Object * pObj)852 void C4MessageInput::ClearPointers(C4Object *pObj)
853 {
854 	// target object loose? stop input
855 	C4ChatInputDialog *pDlg = GetTypeIn();
856 	if (pDlg && pDlg->GetScriptTargetObject() == pObj) CloseTypeIn();
857 }
858 
AbortMsgBoardQuery(C4Object * pObj,int32_t iPlr)859 void C4MessageInput::AbortMsgBoardQuery(C4Object *pObj, int32_t iPlr)
860 {
861 	// close typein if it is used for the given parameters
862 	C4ChatInputDialog *pDlg = GetTypeIn();
863 	if (pDlg && pDlg->IsScriptQueried() && pDlg->GetScriptTargetObject() == pObj && pDlg->GetScriptTargetPlayer() == iPlr) CloseTypeIn();
864 }
865 
StoreBackBuffer(const char * szMessage)866 void C4MessageInput::StoreBackBuffer(const char *szMessage)
867 {
868 	if (!szMessage || !szMessage[0]) return;
869 	int32_t i,cnt;
870 	// Check: Remove doubled buffer
871 	for (i=0; i<C4MSGB_BackBufferMax-1; ++i)
872 		if (SEqual(BackBuffer[i], szMessage))
873 			break;
874 	// Move up buffers
875 	for (cnt=i; cnt>0; cnt--) SCopy(BackBuffer[cnt-1],BackBuffer[cnt]);
876 	// Add message
877 	SCopy(szMessage,BackBuffer[0], C4MaxMessage);
878 }
879 
GetBackBuffer(int32_t iIndex)880 const char *C4MessageInput::GetBackBuffer(int32_t iIndex)
881 {
882 	if (!Inside<int32_t>(iIndex, 0, C4MSGB_BackBufferMax-1)) return nullptr;
883 	return BackBuffer[iIndex];
884 }
885 
C4MessageBoardCommand()886 C4MessageBoardCommand::C4MessageBoardCommand()
887 {
888 	Name[0] = '\0'; Script[0] = '\0'; Next = nullptr;
889 }
890 
CompileFunc(StdCompiler * pComp)891 void C4MessageBoardQuery::CompileFunc(StdCompiler *pComp)
892 {
893 	// note that this CompileFunc does not save the fAnswered-flag, so pending message board queries will be re-asked when resuming SaveGames
894 	pComp->Separator(StdCompiler::SEP_START); // '('
895 	// callback object number
896 	pComp->Value(CallbackObj); pComp->Separator();
897 	// input query string
898 	pComp->Value(sInputQuery); pComp->Separator();
899 	// options
900 	pComp->Value(fIsUppercase);
901 	// list end
902 	pComp->Separator(StdCompiler::SEP_END); // ')'
903 }
904 
905 C4MessageInput MessageInput;
906