1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Handles engine execution in developer mode */
18
19#include <C4Console.h>
20#include <C4Application.h>
21
22#include <C4GameSave.h>
23#include <C4UserMessages.h>
24#include <C4Version.h>
25#include <C4Log.h>
26#include <C4Player.h>
27#include "C4TextEncoding.h"
28
29#include <StdFile.h>
30
31#ifdef _WIN32
32#include "StdRegistry.h"
33#include "StdStringEncodingConverter.h"
34#include "res/engine_resource.h"
35#endif
36
37#ifdef _WIN32
38
39#include <commdlg.h>
40
41bool SetMenuItemText(HMENU hMenu, WORD id, const char *szText);
42
43#else
44
45namespace
46{
47 const uint32_t OFN_HIDEREADONLY = 1 << 0;
48 const uint32_t OFN_OVERWRITEPROMPT = 1 << 1;
49 const uint32_t OFN_FILEMUSTEXIST = 1 << 2;
50 const uint32_t OFN_ALLOWMULTISELECT = 1 << 3;
51
52 const uint32_t OFN_EXPLORER = 0; // ignored
53}
54
55#ifdef USE_X11
56#include <X11/Xlib.h>
57#include <X11/Xutil.h>
58#endif
59
60#endif // _WIN32
61
62#ifndef WITH_DEVELOPER_MODE
63#define WITH_DEVELOPER_MODE 0
64#endif
65
66#if WITH_DEVELOPER_MODE
67
68#include <gdk/gdkcursor.h>
69#include <gdk/gdkx.h>
70#include <gtk/gtk.h>
71
72#include <res/Play.h>
73#include <res/Halt.h>
74#include <res/Mouse.h>
75#include <res/Cursor.h>
76#include <res/Brush.h>
77
78namespace
79{
80 GtkWidget *CreateImageFromInlinedPixbuf(const guint8 *pixbuf_data)
81 {
82 GdkPixbuf *pixbuf = gdk_pixbuf_new_from_inline(data_length: -1, data: pixbuf_data, FALSE, error: nullptr);
83 GtkWidget *image = gtk_image_new_from_pixbuf(pixbuf);
84 g_object_unref(object: pixbuf);
85 return image;
86 }
87}
88
89#endif
90
91C4Console::C4Console()
92{
93 Active = false;
94 Editing = true;
95 ScriptCounter = 0;
96 FrameCounter = 0;
97 fGameOpen = false;
98
99#ifdef _WIN32
100 hWindow = nullptr;
101 hbmCursor = nullptr;
102 hbmCursor2 = nullptr;
103 hbmBrush = nullptr;
104 hbmBrush2 = nullptr;
105 hbmPlay = nullptr;
106 hbmPlay2 = nullptr;
107 hbmHalt = nullptr;
108 hbmHalt2 = nullptr;
109#elif WITH_DEVELOPER_MODE
110 cursorDefault = nullptr;
111 cursorWait = nullptr;
112 itemNet = nullptr;
113 txtLog = nullptr;
114 txtScript = nullptr;
115#endif // WITH_DEVELOPER_MODE / _WIN32
116
117 MenuIndexFile = 0;
118 MenuIndexComponents = 1;
119 MenuIndexPlayer = 2;
120 MenuIndexViewport = 3;
121 MenuIndexNet = -1;
122 MenuIndexHelp = 4;
123}
124
125C4Console::~C4Console()
126{
127#ifdef _WIN32
128 if (hbmCursor) DeleteObject(hbmCursor);
129 if (hbmCursor2) DeleteObject(hbmCursor2);
130 if (hbmBrush) DeleteObject(hbmBrush);
131 if (hbmBrush2) DeleteObject(hbmBrush2);
132 if (hbmPlay) DeleteObject(hbmPlay);
133 if (hbmPlay2) DeleteObject(hbmPlay2);
134 if (hbmHalt) DeleteObject(hbmHalt);
135 if (hbmHalt2) DeleteObject(hbmHalt2);
136#elif WITH_DEVELOPER_MODE
137 if (cursorDefault) g_object_unref(object: cursorDefault);
138 if (cursorWait) g_object_unref(object: cursorWait);
139#endif // WITH_DEVELOPER_MODE / _WIN32
140}
141
142#ifdef _WIN32
143
144INT_PTR CALLBACK ConsoleDlgProc(HWND hDlg, UINT Msg, WPARAM wParam, LPARAM lParam)
145{
146 switch (Msg)
147 {
148 case WM_ACTIVATEAPP:
149 Application.Active = wParam != 0;
150 return TRUE;
151
152 case WM_TIMER:
153 if (wParam == SEC1_TIMER) { Console.Sec1Timer(); }
154 return TRUE;
155
156 case WM_DESTROY:
157 Console.StorePosition();
158 Application.Quit();
159 return TRUE;
160
161 case WM_CLOSE:
162 Console.Close();
163 return TRUE;
164
165 case WM_INITDIALOG:
166 SendMessage(hDlg, DM_SETDEFID, IDOK, 0);
167 Console.UpdateMenuText(GetMenu(hDlg));
168 return TRUE;
169
170 case WM_COMMAND:
171 // Evaluate command
172 switch (LOWORD(wParam))
173 {
174 case IDOK:
175 {
176 const std::wstring text{C4Console::GetDialogItemText(hDlg, IDC_COMBOINPUT)};
177 if (!text.empty())
178 {
179 Console.In(StdStringEncodingConverter::Utf16ToWinAcp(text).c_str());
180 }
181 return TRUE;
182 }
183
184 case IDC_BUTTONHALT:
185 Console.DoHalt();
186 return TRUE;
187
188 case IDC_BUTTONPLAY:
189 Console.DoPlay();
190 return TRUE;
191
192 case IDC_BUTTONMODEPLAY:
193 Console.EditCursor.SetMode(C4CNS_ModePlay);
194 return TRUE;
195
196 case IDC_BUTTONMODEEDIT:
197 Console.EditCursor.SetMode(C4CNS_ModeEdit);
198 return TRUE;
199
200 case IDC_BUTTONMODEDRAW:
201 Console.EditCursor.SetMode(C4CNS_ModeDraw);
202 return TRUE;
203
204 case IDM_FILE_QUIT: Console.FileQuit(); return TRUE;
205 case IDM_FILE_SAVEAS: Console.FileSaveAs(false); return TRUE;
206 case IDM_FILE_SAVE: Console.FileSave(false); return TRUE;
207 case IDM_FILE_SAVEGAMEAS: Console.FileSaveAs(true); return TRUE;
208 case IDM_FILE_SAVEGAME: Console.FileSave(true); return TRUE;
209 case IDM_FILE_OPEN: Console.FileOpen(); return TRUE;
210 case IDM_FILE_RECORD: Console.FileRecord(); return TRUE;
211 case IDM_FILE_OPENWPLRS: Console.FileOpenWPlrs(); return TRUE;
212 case IDM_FILE_CLOSE: Console.FileClose(); return TRUE;
213 case IDM_HELP_ABOUT: Console.HelpAbout(); return TRUE;
214 case IDM_PLAYER_JOIN: Console.PlayerJoin(); return TRUE;
215 case IDM_VIEWPORT_NEW: Console.ViewportNew(); return TRUE;
216 case IDM_COMPONENTS_TITLE: Console.EditTitle(); return TRUE;
217 case IDM_COMPONENTS_INFO: Console.EditInfo(); return TRUE;
218 case IDM_COMPONENTS_SCRIPT: Console.EditScript(); return TRUE;
219 }
220 // New player viewport
221 if (Inside((int)LOWORD(wParam), IDM_VIEWPORT_NEW1, IDM_VIEWPORT_NEW2))
222 {
223 Game.CreateViewport(LOWORD(wParam) - IDM_VIEWPORT_NEW1);
224 return TRUE;
225 }
226 // Remove player
227 if (Inside((int)LOWORD(wParam), IDM_PLAYER_QUIT1, IDM_PLAYER_QUIT2))
228 {
229 Game.Control.Input.Add(CID_EliminatePlayer, new C4ControlEliminatePlayer(LOWORD(wParam) - IDM_PLAYER_QUIT1));
230 return TRUE;
231 }
232 // Remove client
233 if (Inside((int)LOWORD(wParam), IDM_NET_CLIENT1, IDM_NET_CLIENT2))
234 {
235 if (!Game.Control.isCtrlHost()) return FALSE;
236 Game.Clients.CtrlRemove(Game.Clients.getClientByID(LOWORD(wParam) - IDM_NET_CLIENT1), LoadResStr(C4ResStrTableKey::IDS_MSG_KICKBYMENU));
237 return TRUE;
238 }
239 return FALSE;
240
241 case WM_COPYDATA:
242 COPYDATASTRUCT *pcds = reinterpret_cast<COPYDATASTRUCT *>(lParam);
243 if (pcds->dwData == WM_USER_RELOADFILE)
244 {
245 // get path, ensure proper termination
246 const char *szPath = reinterpret_cast<const char *>(pcds->lpData);
247 if (szPath[pcds->cbData - 1]) break;
248 // reload
249 Game.ReloadFile(szPath);
250 }
251 return FALSE;
252 }
253
254 return FALSE;
255}
256
257#elif defined(USE_X11) && !WITH_DEVELOPER_MODE
258
259void C4Console::HandleMessage(XEvent &e)
260{
261 // Parent handling
262 C4ConsoleBase::HandleMessage(e);
263
264 switch (e.type)
265 {
266 case FocusIn:
267 Application.Active = true;
268 break;
269 case FocusOut:
270 Application.Active = false;
271 break;
272 }
273}
274
275#endif // _WIN32/USE_X11
276
277bool C4Console::Init(CStdApp *const app)
278{
279 return Init(app, title: LoadResStr(id: C4ResStrTableKey::IDS_CNS_CONSOLE));
280}
281
282bool C4Console::Init(CStdApp *const app, const char *const title, const C4Rect &bounds, CStdWindow *const parent)
283{
284 // Active
285 Active = true;
286 // Editing (enable even if network)
287 Editing = true;
288#ifdef _WIN32
289 // Init common controls
290 INITCOMMONCONTROLSEX controls{.dwSize = sizeof(controls), .dwICC = ICC_STANDARD_CLASSES};
291 if (!InitCommonControlsEx(&controls))
292 {
293 spdlog::critical("Error initializing common controls: {}", winrt::to_string(winrt::hresult_error{HRESULT_FROM_WIN32(GetLastError())}.message()));
294 return false;
295 }
296
297 // Create dialog window
298 hWindow = CreateDialog(app->hInstance, MAKEINTRESOURCE(IDD_CONSOLE), nullptr, ConsoleDlgProc);
299 if (!hWindow)
300 {
301 spdlog::critical("Error creating dialog window: {}", winrt::to_string(winrt::hresult_error{HRESULT_FROM_WIN32(GetLastError())}.message()));
302 return false;
303 }
304 // Restore window position
305 RestorePosition();
306 // Set icon
307 SendMessage(hWindow, WM_SETICON, ICON_BIG, reinterpret_cast<LPARAM>(LoadIcon(app->hInstance, MAKEINTRESOURCE(IDI_00_C4X))));
308 SendMessage(hWindow, WM_SETICON, ICON_SMALL, reinterpret_cast<LPARAM>(LoadIcon(app->hInstance, MAKEINTRESOURCE(IDI_00_C4X))));
309 // Set text
310 SetCaption(title);
311 // Load bitmaps
312 hbmMouse = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_MOUSE));
313 hbmMouse2 = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_MOUSE2));
314 hbmCursor = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_CURSOR));
315 hbmCursor2 = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_CURSOR2));
316 hbmBrush = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_BRUSH));
317 hbmBrush2 = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_BRUSH2));
318 hbmPlay = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_PLAY));
319 hbmPlay2 = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_PLAY2));
320 hbmHalt = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_HALT));
321 hbmHalt2 = LoadBitmap(app->hInstance, MAKEINTRESOURCE(IDB_HALT2));
322 // Enable controls
323 UpdateHaltCtrls(true);
324 EnableControls(fGameOpen);
325 ClearViewportMenu();
326 // Show window and set focus
327 ShowWindow(hWindow, SW_SHOWNORMAL);
328 UpdateWindow(hWindow);
329 SetFocus(hWindow);
330 ShowCursor(TRUE);
331 // Success
332 return true;
333#elif WITH_DEVELOPER_MODE
334 if (!C4ConsoleBase::Init(app, title, bounds, parent))
335 {
336 return false;
337 }
338
339 GdkDisplay *const display{gtk_widget_get_display(widget: window)};
340 cursorWait = gdk_cursor_new_for_display(display, cursor_type: GDK_WATCH);
341 cursorDefault = gdk_cursor_new_for_display(display, cursor_type: GDK_ARROW);
342
343 // Calls InitGUI
344 UpdateHaltCtrls(fHalt: true);
345 EnableControls(fEnable: fGameOpen);
346 ClearViewportMenu();
347 return true;
348#else
349 return C4ConsoleBase::Init(app, LoadResStr(C4ResStrTableKey::IDS_CNS_CONSOLE), bounds);
350#endif // WITH_DEVELOPER_MODE / _WIN32
351}
352
353#if WITH_DEVELOPER_MODE
354GtkWidget *C4Console::InitGUI()
355{
356 // Play/Pause and Mode
357
358 GtkWidget *image_play = CreateImageFromInlinedPixbuf(pixbuf_data: play_pixbuf_data);
359 GtkWidget *image_pause = CreateImageFromInlinedPixbuf(pixbuf_data: halt_pixbuf_data);
360
361 GtkWidget *image_mode_play = CreateImageFromInlinedPixbuf(pixbuf_data: mouse_pixbuf_data);
362 GtkWidget *image_mode_edit = CreateImageFromInlinedPixbuf(pixbuf_data: cursor_pixbuf_data);
363 GtkWidget *image_mode_draw = CreateImageFromInlinedPixbuf(pixbuf_data: brush_pixbuf_data);
364
365 btnPlay = gtk_toggle_button_new();
366 btnHalt = gtk_toggle_button_new();
367 btnModePlay = gtk_toggle_button_new();
368 btnModeEdit = gtk_toggle_button_new();
369 btnModeDraw = gtk_toggle_button_new();
370
371 gtk_container_add(GTK_CONTAINER(btnPlay), widget: image_play);
372 gtk_container_add(GTK_CONTAINER(btnHalt), widget: image_pause);
373 gtk_container_add(GTK_CONTAINER(btnModePlay), widget: image_mode_play);
374 gtk_container_add(GTK_CONTAINER(btnModeEdit), widget: image_mode_edit);
375 gtk_container_add(GTK_CONTAINER(btnModeDraw), widget: image_mode_draw);
376
377 GtkWidget *top_hbox = gtk_box_new(orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 18);
378 GtkWidget *play_hbox = gtk_box_new(orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 6);
379 GtkWidget *mode_hbox = gtk_box_new(orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 6);
380
381 gtk_box_pack_start(GTK_BOX(play_hbox), child: btnPlay, FALSE, TRUE, padding: 0);
382 gtk_box_pack_start(GTK_BOX(play_hbox), child: btnHalt, FALSE, TRUE, padding: 0);
383 gtk_box_pack_start(GTK_BOX(mode_hbox), child: btnModePlay, FALSE, TRUE, padding: 0);
384 gtk_box_pack_start(GTK_BOX(mode_hbox), child: btnModeEdit, FALSE, TRUE, padding: 0);
385 gtk_box_pack_start(GTK_BOX(mode_hbox), child: btnModeDraw, FALSE, TRUE, padding: 0);
386
387 lblCursor = gtk_label_new(str: "");
388 gtk_label_set_xalign(GTK_LABEL(lblCursor), xalign: 0.0f);
389
390 gtk_box_pack_start(GTK_BOX(top_hbox), child: lblCursor, TRUE, TRUE, padding: 0);
391 gtk_box_pack_start(GTK_BOX(top_hbox), child: play_hbox, FALSE, TRUE, padding: 0);
392 gtk_box_pack_start(GTK_BOX(top_hbox), child: mode_hbox, FALSE, TRUE, padding: 0);
393
394 // Statusbar
395
396 GtkWidget *statusbar = gtk_box_new(orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 6);
397
398 GtkWidget *status_frame = gtk_frame_new(label: nullptr);
399 gtk_frame_set_shadow_type(GTK_FRAME(status_frame), type: GTK_SHADOW_IN);
400 gtk_container_add(GTK_CONTAINER(status_frame), widget: statusbar);
401
402 lblFrame = gtk_label_new(str: "Frame: 0");
403 lblScript = gtk_label_new(str: "Script: 0");
404 lblTime = gtk_label_new(str: "00:00:00 (0 FPS)");
405
406 gtk_label_set_xalign(GTK_LABEL(lblFrame), xalign: 0.0f);
407 gtk_label_set_xalign(GTK_LABEL(lblScript), xalign: 0.0f);
408 gtk_label_set_xalign(GTK_LABEL(lblTime), xalign: 0.0f);
409
410 GtkWidget *sep1 = gtk_separator_new(orientation: GTK_ORIENTATION_VERTICAL);
411 GtkWidget *sep2 = gtk_separator_new(orientation: GTK_ORIENTATION_VERTICAL);
412
413 gtk_box_pack_start(GTK_BOX(statusbar), child: lblFrame, TRUE, TRUE, padding: 0);
414 gtk_box_pack_start(GTK_BOX(statusbar), child: sep1, FALSE, FALSE, padding: 0);
415 gtk_box_pack_start(GTK_BOX(statusbar), child: lblScript, TRUE, TRUE, padding: 0);
416 gtk_box_pack_start(GTK_BOX(statusbar), child: sep2, FALSE, FALSE, padding: 0);
417 gtk_box_pack_start(GTK_BOX(statusbar), child: lblTime, TRUE, TRUE, padding: 0);
418
419 // Log view and script entry
420
421 GtkWidget *scroll = gtk_scrolled_window_new(hadjustment: nullptr, vadjustment: nullptr);
422
423 txtLog = gtk_text_view_new();
424 txtScript = gtk_entry_new();
425
426 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroll), type: GTK_SHADOW_IN);
427 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), hscrollbar_policy: GTK_POLICY_AUTOMATIC, vscrollbar_policy: GTK_POLICY_AUTOMATIC);
428 gtk_text_view_set_editable(GTK_TEXT_VIEW(txtLog), FALSE);
429
430 gtk_container_add(GTK_CONTAINER(scroll), widget: txtLog);
431
432 // Menu
433
434 menuBar = gtk_menu_bar_new();
435
436 GtkWidget *itemFile = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_FILE).c_str());
437 GtkWidget *itemComponents = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_COMPONENTS).c_str());
438 GtkWidget *itemPlayer = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_PLAYER).c_str());
439 GtkWidget *itemViewport = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_VIEWPORT).c_str());
440 GtkWidget *itemHelp = gtk_menu_item_new_with_label(label: "?");
441
442 gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), child: itemFile);
443 gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), child: itemComponents);
444 gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), child: itemPlayer);
445 gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), child: itemViewport);
446 gtk_menu_shell_append(GTK_MENU_SHELL(menuBar), child: itemHelp);
447
448 GtkWidget *menuFile = gtk_menu_new();
449 GtkWidget *menuComponents = gtk_menu_new();
450 GtkWidget *menuHelp = gtk_menu_new();
451
452 menuPlayer = gtk_menu_new();
453 menuViewport = gtk_menu_new();
454
455 gtk_menu_item_set_submenu(GTK_MENU_ITEM(itemFile), submenu: menuFile);
456 gtk_menu_item_set_submenu(GTK_MENU_ITEM(itemComponents), submenu: menuComponents);
457 gtk_menu_item_set_submenu(GTK_MENU_ITEM(itemPlayer), submenu: menuPlayer);
458 gtk_menu_item_set_submenu(GTK_MENU_ITEM(itemViewport), submenu: menuViewport);
459 gtk_menu_item_set_submenu(GTK_MENU_ITEM(itemHelp), submenu: menuHelp);
460
461 fileOpen = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_OPEN).c_str());
462 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileOpen);
463
464 fileOpenWithPlayers = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_OPENWPLRS).c_str());
465 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileOpenWithPlayers);
466
467 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), GTK_WIDGET(gtk_separator_menu_item_new()));
468
469 fileSave = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_SAVESCENARIO).c_str());
470 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileSave);
471
472 fileSaveAs = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_SAVESCENARIOAS).c_str());
473 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileSaveAs);
474
475 fileSaveGame = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_SAVEGAME).c_str());
476 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileSaveGame);
477
478 fileSaveGameAs = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_SAVEGAMEAS).c_str());
479 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileSaveGameAs);
480
481 fileRecord = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_RECORD).c_str());
482 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileRecord);
483
484 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), GTK_WIDGET(gtk_separator_menu_item_new()));
485
486 fileClose = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_CLOSE).c_str());
487 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileClose);
488
489 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), GTK_WIDGET(gtk_separator_menu_item_new()));
490
491 fileQuit = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_QUIT).c_str());
492 gtk_menu_shell_append(GTK_MENU_SHELL(menuFile), child: fileQuit);
493
494 compObjects = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_BTN_OBJECTS).c_str());
495 gtk_menu_shell_append(GTK_MENU_SHELL(menuComponents), child: compObjects);
496
497 compScript = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_SCRIPT).c_str());
498 gtk_menu_shell_append(GTK_MENU_SHELL(menuComponents), child: compScript);
499
500 compTitle = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_TITLE).c_str());
501 gtk_menu_shell_append(GTK_MENU_SHELL(menuComponents), child: compTitle);
502
503 compInfo = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_INFO).c_str());
504 gtk_menu_shell_append(GTK_MENU_SHELL(menuComponents), child: compInfo);
505
506 plrJoin = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_JOIN).c_str());
507 gtk_menu_shell_append(GTK_MENU_SHELL(menuPlayer), child: plrJoin);
508
509 viewNew = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_NEW).c_str());
510 gtk_menu_shell_append(GTK_MENU_SHELL(menuViewport), child: viewNew);
511
512 helpAbout = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MENU_ABOUT).c_str());
513 gtk_menu_shell_append(GTK_MENU_SHELL(menuHelp), child: helpAbout);
514
515 // Window
516
517 GtkWidget *topbox = gtk_box_new(orientation: GTK_ORIENTATION_VERTICAL, spacing: 0);
518 GtkWidget *midbox = gtk_box_new(orientation: GTK_ORIENTATION_VERTICAL, spacing: 6);
519 gtk_container_set_border_width(GTK_CONTAINER(midbox), border_width: 6);
520
521 gtk_box_pack_start(GTK_BOX(midbox), child: scroll, TRUE, TRUE, padding: 0);
522 gtk_box_pack_start(GTK_BOX(midbox), child: txtScript, FALSE, FALSE, padding: 0);
523 gtk_box_pack_start(GTK_BOX(midbox), child: top_hbox, FALSE, TRUE, padding: 0);
524
525 gtk_box_pack_start(GTK_BOX(topbox), child: menuBar, FALSE, FALSE, padding: 0);
526 gtk_box_pack_start(GTK_BOX(topbox), child: midbox, TRUE, TRUE, padding: 0);
527 gtk_box_pack_start(GTK_BOX(topbox), child: status_frame, FALSE, FALSE, padding: 0);
528
529 gtk_window_set_default_size(GTK_WINDOW(window), width: 320, height: 320);
530
531 gtk_container_add(GTK_CONTAINER(window), widget: topbox);
532
533 // Signals
534
535 handlerPlay = g_signal_connect(G_OBJECT(btnPlay), "toggled", G_CALLBACK(OnPlay), this);
536 handlerHalt = g_signal_connect(G_OBJECT(btnHalt), "toggled", G_CALLBACK(OnHalt), this);
537 handlerModePlay = g_signal_connect(G_OBJECT(btnModePlay), "toggled", G_CALLBACK(OnModePlay), this);
538 handlerModeEdit = g_signal_connect(G_OBJECT(btnModeEdit), "toggled", G_CALLBACK(OnModeEdit), this);
539 handlerModeDraw = g_signal_connect(G_OBJECT(btnModeDraw), "toggled", G_CALLBACK(OnModeDraw), this);
540 g_signal_connect(G_OBJECT(txtScript), "activate", G_CALLBACK(OnScriptEntry), this);
541 g_signal_connect(G_OBJECT(fileOpen), "activate", G_CALLBACK(OnFileOpen), this);
542 g_signal_connect(G_OBJECT(fileOpenWithPlayers), "activate", G_CALLBACK(OnFileOpenWPlrs), this);
543 g_signal_connect(G_OBJECT(fileSave), "activate", G_CALLBACK(OnFileSave), this);
544 g_signal_connect(G_OBJECT(fileSaveAs), "activate", G_CALLBACK(OnFileSaveAs), this);
545 g_signal_connect(G_OBJECT(fileSaveGame), "activate", G_CALLBACK(OnFileSaveGame), this);
546 g_signal_connect(G_OBJECT(fileSaveGameAs), "activate", G_CALLBACK(OnFileSaveGameAs), this);
547 g_signal_connect(G_OBJECT(fileRecord), "activate", G_CALLBACK(OnFileRecord), this);
548 g_signal_connect(G_OBJECT(fileClose), "activate", G_CALLBACK(OnFileClose), this);
549 g_signal_connect(G_OBJECT(fileQuit), "activate", G_CALLBACK(OnFileQuit), this);
550 g_signal_connect(G_OBJECT(compObjects), "activate", G_CALLBACK(OnCompObjects), this);
551 g_signal_connect(G_OBJECT(compScript), "activate", G_CALLBACK(OnCompScript), this);
552 g_signal_connect(G_OBJECT(compTitle), "activate", G_CALLBACK(OnCompTitle), this);
553 g_signal_connect(G_OBJECT(compInfo), "activate", G_CALLBACK(OnCompInfo), this);
554 g_signal_connect(G_OBJECT(plrJoin), "activate", G_CALLBACK(OnPlrJoin), this);
555 g_signal_connect(G_OBJECT(viewNew), "activate", G_CALLBACK(OnViewNew), this);
556 g_signal_connect(G_OBJECT(helpAbout), "activate", G_CALLBACK(OnHelpAbout), this);
557
558 return C4ConsoleBase::InitGUI();
559}
560#endif // WITH_DEVELOPER_MODE
561
562bool C4Console::In(const char *szText)
563{
564 if (!Active || !szText) return false;
565 // begins with '/'? then it's a command
566 if (*szText == '/')
567 {
568 Game.MessageInput.ProcessCommand(szCommand: szText);
569 // done
570 return true;
571 }
572 // begins with '#'? then it's a message. Route cia ProcessInput to allow #/sound
573 if (*szText == '#')
574 {
575 Game.MessageInput.ProcessInput(szText: szText + 1);
576 return true;
577 }
578 // editing enabled?
579 if (!EditCursor.EditingOK()) return false;
580 // pass through network queue
581 Game.Control.DoInput(eCtrlType: CID_Script, pPkt: new C4ControlScript(szText, C4ControlScript::SCOPE_Console, Config.Developer.ConsoleScriptStrictness), eDelivery: CDT_Decide);
582 return true;
583}
584
585bool C4Console::Out(std::string_view text)
586{
587#ifdef _WIN32
588 if (!Active) return false;
589 if (text.empty()) return true;
590
591 const std::wstring textWide{StdStringEncodingConverter::WinAcpToUtf16(text)};
592
593 const bool hasNewline{textWide.ends_with(L"\r\n")};
594
595 std::wstring buffer;
596 const LRESULT dlgItemTextSize{SendDlgItemMessage(hWindow, IDC_EDITOUTPUT, WM_GETTEXTLENGTH, 0, 0)};
597 buffer.resize_and_overwrite(dlgItemTextSize + textWide.size() + (hasNewline ? 0 : 2), [dlgItemTextSize, hasNewline, &textWide, this](wchar_t *const ptr, std::size_t size)
598 {
599 const UINT textSize{GetDlgItemText(hWindow, IDC_EDITOUTPUT, ptr, dlgItemTextSize + 1)};
600 wchar_t *newlinePtr{ptr + textSize + textWide.copy(ptr + textSize, textWide.size())};
601
602 if (!hasNewline)
603 {
604 *newlinePtr++ = '\r';
605 *newlinePtr++ = '\n';
606 }
607
608 return newlinePtr - ptr;
609 });
610
611 const wchar_t *const newText{buffer.size() <= 60000 ? buffer.c_str() : buffer.c_str() + buffer.size() - 60000}; // max log length: Otherwise, discard beginning
612 SetDlgItemText(hWindow, IDC_EDITOUTPUT, newText);
613
614 const auto lines = SendDlgItemMessage(hWindow, IDC_EDITOUTPUT, EM_GETLINECOUNT, 0, 0);
615 SendDlgItemMessage(hWindow, IDC_EDITOUTPUT, EM_LINESCROLL, 0, static_cast<LPARAM>(lines));
616 UpdateWindow(hWindow);
617#elif WITH_DEVELOPER_MODE
618 // Append text to log
619 if (!window) return true;
620
621 GtkTextIter end;
622 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(txtLog));
623 gtk_text_buffer_get_end_iter(buffer, iter: &end);
624
625 gtk_text_buffer_insert(buffer, iter: &end, text: ClonkToGtk(text).c_str(), len: -1);
626
627 if (!text.ends_with(x: '\n'))
628 {
629 gtk_text_buffer_insert(buffer, iter: &end, text: "\n", len: 1);
630 }
631
632 gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(txtLog), mark: gtk_text_buffer_get_insert(buffer), within_margin: 0.0, FALSE, xalign: 0.0, yalign: 0.0);
633
634 // Cheap hack to get the Console window updated while loading
635 while (g_main_context_pending(context: nullptr))
636 g_main_context_iteration(context: nullptr, FALSE);
637#endif // WITH_DEVELOPER_MODE / _WIN32
638 return true;
639}
640
641bool C4Console::ClearLog()
642{
643#ifdef _WIN32
644 SetDlgItemText(hWindow, IDC_EDITOUTPUT, L"");
645 SendDlgItemMessage(hWindow, IDC_EDITOUTPUT, EM_LINESCROLL, 0, 0);
646 UpdateWindow(hWindow);
647#elif WITH_DEVELOPER_MODE
648 gtk_text_buffer_set_text(buffer: gtk_text_view_get_buffer(GTK_TEXT_VIEW(txtLog)), text: "", len: 0);
649#endif // WITH_DEVELOPER_MODE / _WIN32
650 return true;
651}
652
653void C4Console::DoPlay()
654{
655 Game.Unpause();
656}
657
658void C4Console::DoHalt()
659{
660 Game.Pause();
661}
662
663bool C4Console::UpdateStatusBars()
664{
665 if (!Active) return false;
666 // Frame counter
667 if (Game.FrameCounter != FrameCounter)
668 {
669 FrameCounter = Game.FrameCounter;
670#ifdef _WIN32
671 const std::wstring text{std::format(L"Frame: {}", FrameCounter)};
672 SetDlgItemText(hWindow, IDC_STATICFRAME, text.c_str());
673 UpdateWindow(GetDlgItem(hWindow, IDC_STATICFRAME));
674#elif WITH_DEVELOPER_MODE
675 const std::string text{std::format(fmt: "Frame: {}", args&: FrameCounter)};
676 gtk_label_set_label(GTK_LABEL(lblFrame), str: text.c_str());
677#endif // WITH_DEVELOPER_MODE / _WIN32
678 }
679 // Script counter
680 if (Game.Script.Counter != ScriptCounter)
681 {
682 ScriptCounter = Game.Script.Counter;
683#ifdef _WIN32
684 const std::wstring text{std::format(L"Script: {}", ScriptCounter)};
685 SetDlgItemText(hWindow, IDC_STATICSCRIPT, text.c_str());
686 UpdateWindow(GetDlgItem(hWindow, IDC_STATICSCRIPT));
687#elif WITH_DEVELOPER_MODE
688 const std::string text{std::format(fmt: "Script: {}", args&: ScriptCounter)};
689 gtk_label_set_label(GTK_LABEL(lblScript), str: text.c_str());
690#endif // WITH_DEVELOPER_MODE / _WIN32
691 }
692 // Time & FPS
693 if ((Game.Time != Time) || (Game.FPS != FPS))
694 {
695 Time = Game.Time;
696 FPS = Game.FPS;
697#ifdef _WIN32
698 const std::wstring text{std::format(L"{:02}:{:02}:{:02} ({} FPS)", Time / 3600, (Time % 3600) / 60, Time % 60, FPS)};
699 SetDlgItemText(hWindow, IDC_STATICTIME, text.c_str());
700 UpdateWindow(GetDlgItem(hWindow, IDC_STATICTIME));
701#elif WITH_DEVELOPER_MODE
702 const std::string text{std::format(fmt: "{:02}:{:02}:{:02} ({} FPS)", args: Time / 3600, args: (Time % 3600) / 60, args: Time % 60, args&: FPS)};
703 gtk_label_set_label(GTK_LABEL(lblTime), str: text.c_str());
704#endif // WITH_DEVELOPER_MODE
705 }
706 return true;
707}
708
709bool C4Console::UpdateHaltCtrls(bool fHalt)
710{
711 if (!Active) return false;
712#ifdef _WIN32
713 SendDlgItemMessage(hWindow, IDC_BUTTONPLAY, BM_SETSTATE, !fHalt, 0);
714 UpdateWindow(GetDlgItem(hWindow, IDC_BUTTONPLAY));
715 SendDlgItemMessage(hWindow, IDC_BUTTONHALT, BM_SETSTATE, fHalt, 0);
716 UpdateWindow(GetDlgItem(hWindow, IDC_BUTTONHALT));
717#elif WITH_DEVELOPER_MODE
718 // Prevents recursion
719 g_signal_handler_block(instance: btnPlay, handler_id: handlerPlay);
720 g_signal_handler_block(instance: btnHalt, handler_id: handlerHalt);
721
722 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btnPlay), is_active: !fHalt);
723 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btnHalt), is_active: fHalt);
724
725 g_signal_handler_unblock(instance: btnPlay, handler_id: handlerPlay);
726 g_signal_handler_unblock(instance: btnHalt, handler_id: handlerHalt);
727
728#endif // WITH_DEVELOPER_MODE / _WIN32
729 return true;
730}
731
732bool C4Console::SaveGame(bool fSaveGame)
733{
734 // Network hosts only
735 if (Game.Network.isEnabled() && !Game.Network.isHost())
736 {
737 Message(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_GAME_NOCLIENTSAVE)); return false;
738 }
739
740 // Can't save to child groups
741 if (Game.ScenarioFile.GetMother())
742 {
743 Message(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_CNS_NOCHILDSAVE, args: GetFilename(path: Game.ScenarioFile.GetName())).c_str());
744 return false;
745 }
746
747 // Save game to open scenario file
748 bool fOkay = true;
749#ifdef _WIN32
750 SetCursor(LoadCursor(0, IDC_WAIT));
751#elif WITH_DEVELOPER_MODE
752 // Seems not to work. Don't know why...
753 gdk_window_set_cursor(window: gtk_widget_get_window(widget: window), cursor: cursorWait);
754#endif
755
756 C4GameSave *pGameSave;
757 if (fSaveGame)
758 pGameSave = new C4GameSaveSavegame();
759 else
760 pGameSave = new C4GameSaveScenario(!Console.Active || Game.Landscape.Mode == C4LSC_Exact, false);
761 if (!pGameSave->Save(hToGroup&: Game.ScenarioFile, fKeepGroup: false))
762 {
763 Out(text: "Game::Save failed"); fOkay = false;
764 }
765 delete pGameSave;
766
767 // Close and reopen scenario file to fix file changes
768 if (!Game.ScenarioFile.Close())
769 {
770 Out(text: "ScenarioFile::Close failed"); fOkay = false;
771 }
772 if (!Game.ScenarioFile.Open(szGroupName: Game.ScenarioFilename))
773 {
774 Out(text: "ScenarioFile::Open failed"); fOkay = false;
775 }
776
777#ifdef _WIN32
778 SetCursor(LoadCursor(0, IDC_ARROW));
779#elif WITH_DEVELOPER_MODE
780 gdk_window_set_cursor(window: gtk_widget_get_window(widget: window), cursor: nullptr);
781#endif
782
783 // Initialize/script notification
784 if (Game.fScriptCreatedObjects)
785 if (!fSaveGame)
786 {
787 Message(szMessage: (std::string{LoadResStr(id: C4ResStrTableKey::IDS_CNS_SCRIPTCREATEDOBJECTS)} + LoadResStr(id: C4ResStrTableKey::IDS_CNS_WARNDOUBLE)).c_str());
788 Game.fScriptCreatedObjects = false;
789 }
790
791 // Status report
792 if (!fOkay) Message(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_CNS_SAVERROR));
793 else Out(text: LoadResStrChoice(condition: fSaveGame, ifTrue: C4ResStrTableKey::IDS_CNS_GAMESAVED, ifFalse: C4ResStrTableKey::IDS_CNS_SCENARIOSAVED));
794
795 return fOkay;
796}
797
798bool C4Console::FileSave(bool fSaveGame)
799{
800 // Don't quicksave games over scenarios
801 if (fSaveGame)
802 if (!Game.C4S.Head.SaveGame)
803 {
804 Message(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_CNS_NOGAMEOVERSCEN));
805 return false;
806 }
807 // Save game
808 return SaveGame(fSaveGame);
809}
810
811bool C4Console::FileSaveAs(bool fSaveGame)
812{
813 // Do save-as dialog
814 char filename[512 + 1];
815 SCopy(szSource: Game.ScenarioFile.GetName(), sTarget: filename);
816 if (!FileSelect(sFilename: filename, iSize: 512,
817 szFilter: "Clonk 4 Scenario\0*.c4s\0\0",
818 dwFlags: OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY,
819 fSave: true)) return false;
820 DefaultExtension(szFileName: filename, szExtension: "c4s");
821 bool fOkay = true;
822 // Close current scenario file
823 if (!Game.ScenarioFile.Close()) fOkay = false;
824 // Copy current scenario file to target
825 if (!C4Group_CopyItem(szSource: Game.ScenarioFilename, szTarget: filename)) fOkay = false;
826 // Open new scenario file
827 SCopy(szSource: filename, sTarget: Game.ScenarioFilename);
828
829 SetCaption(GetFilename(path: Game.ScenarioFilename));
830 if (!Game.ScenarioFile.Open(szGroupName: Game.ScenarioFilename)) fOkay = false;
831 // Failure message
832 if (!fOkay)
833 {
834 Message(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_CNS_SAVEASERROR, args&: Game.ScenarioFilename).c_str());
835 return false;
836 }
837 // Save game
838 return SaveGame(fSaveGame);
839}
840
841bool C4Console::Message(const char *szMessage, bool fQuery)
842{
843 if (!Active) return false;
844#ifdef _WIN32
845 return (IDOK == MessageBox(hWindow, StdStringEncodingConverter::WinAcpToUtf16(szMessage).c_str(), _CRT_WIDE(C4ENGINECAPTION), fQuery ? (MB_OKCANCEL | MB_ICONEXCLAMATION) : MB_ICONEXCLAMATION));
846#elif WITH_DEVELOPER_MODE
847 GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), flags: GTK_DIALOG_MODAL, type: GTK_MESSAGE_INFO, buttons: fQuery ? (GTK_BUTTONS_OK_CANCEL) : (GTK_BUTTONS_OK), message_format: "%s", szMessage);
848 int response = gtk_dialog_run(GTK_DIALOG(dialog));
849 gtk_widget_destroy(widget: dialog);
850 return response == GTK_RESPONSE_OK;
851#endif
852 return false;
853}
854
855void C4Console::EnableControls(bool fEnable)
856{
857 if (!Active) return;
858 // disable Editing if no input allowed
859 Editing &= !Game.Control.NoInput();
860
861#ifdef _WIN32
862 // Set button images (edit modes & halt controls)
863 SendDlgItemMessage(hWindow, IDC_BUTTONMODEPLAY, BM_SETIMAGE, IMAGE_BITMAP, reinterpret_cast<LPARAM>(fEnable ? hbmMouse : hbmMouse2));
864 SendDlgItemMessage(hWindow, IDC_BUTTONMODEEDIT, BM_SETIMAGE, IMAGE_BITMAP, reinterpret_cast<LPARAM>((fEnable && Editing) ? hbmCursor : hbmCursor2));
865 SendDlgItemMessage(hWindow, IDC_BUTTONMODEDRAW, BM_SETIMAGE, IMAGE_BITMAP, reinterpret_cast<LPARAM>((fEnable && Editing) ? hbmBrush : hbmBrush2));
866 SendDlgItemMessage(hWindow, IDC_BUTTONPLAY, BM_SETIMAGE, IMAGE_BITMAP, reinterpret_cast<LPARAM>(Game.Network.isLobbyActive() || fEnable ? hbmPlay : hbmPlay2));
867 SendDlgItemMessage(hWindow, IDC_BUTTONHALT, BM_SETIMAGE, IMAGE_BITMAP, reinterpret_cast<LPARAM>(Game.Network.isLobbyActive() || fEnable ? hbmHalt : hbmHalt2));
868
869 // OK
870 EnableWindow(GetDlgItem(hWindow, IDOK), fEnable);
871
872 // Halt controls
873 EnableWindow(GetDlgItem(hWindow, IDC_BUTTONPLAY), Game.Network.isLobbyActive() || fEnable);
874 EnableWindow(GetDlgItem(hWindow, IDC_BUTTONHALT), Game.Network.isLobbyActive() || fEnable);
875
876 // Edit modes
877 EnableWindow(GetDlgItem(hWindow, IDC_BUTTONMODEPLAY), (fEnable));
878 EnableWindow(GetDlgItem(hWindow, IDC_BUTTONMODEEDIT), (fEnable && Editing));
879 EnableWindow(GetDlgItem(hWindow, IDC_BUTTONMODEDRAW), (fEnable && Editing));
880
881 // Console input
882 EnableWindow(GetDlgItem(hWindow, IDC_COMBOINPUT), fEnable);
883
884 // File menu
885 // C4Network2 will have to handle that cases somehow (TODO: test)
886 EnableMenuItem(GetMenu(hWindow), IDM_FILE_OPEN, MF_BYCOMMAND | MF_ENABLED);
887 EnableMenuItem(GetMenu(hWindow), IDM_FILE_OPENWPLRS, MF_BYCOMMAND | MF_ENABLED);
888 EnableMenuItem(GetMenu(hWindow), IDM_FILE_RECORD, MF_BYCOMMAND | ((Game.IsRunning && Game.Control.IsRuntimeRecordPossible()) ? MF_ENABLED : MF_GRAYED));
889 EnableMenuItem(GetMenu(hWindow), IDM_FILE_SAVEGAME, MF_BYCOMMAND | ((fEnable && Game.Players.GetCount()) ? MF_ENABLED : MF_GRAYED));
890 EnableMenuItem(GetMenu(hWindow), IDM_FILE_SAVEGAMEAS, MF_BYCOMMAND | ((fEnable && Game.Players.GetCount()) ? MF_ENABLED : MF_GRAYED));
891 EnableMenuItem(GetMenu(hWindow), IDM_FILE_SAVE, MF_BYCOMMAND | (fEnable ? MF_ENABLED : MF_GRAYED));
892 EnableMenuItem(GetMenu(hWindow), IDM_FILE_SAVEAS, MF_BYCOMMAND | (fEnable ? MF_ENABLED : MF_GRAYED));
893 EnableMenuItem(GetMenu(hWindow), IDM_FILE_CLOSE, MF_BYCOMMAND | (fEnable ? MF_ENABLED : MF_GRAYED));
894
895 // Components menu
896 EnableMenuItem(GetMenu(hWindow), IDM_COMPONENTS_SCRIPT, MF_BYCOMMAND | ((fEnable && Editing) ? MF_ENABLED : MF_GRAYED));
897 EnableMenuItem(GetMenu(hWindow), IDM_COMPONENTS_INFO, MF_BYCOMMAND | ((fEnable && Editing) ? MF_ENABLED : MF_GRAYED));
898 EnableMenuItem(GetMenu(hWindow), IDM_COMPONENTS_TITLE, MF_BYCOMMAND | ((fEnable && Editing) ? MF_ENABLED : MF_GRAYED));
899
900 // Player & viewport menu
901 EnableMenuItem(GetMenu(hWindow), IDM_PLAYER_JOIN, MF_BYCOMMAND | ((fEnable && Editing) ? MF_ENABLED : MF_GRAYED));
902 EnableMenuItem(GetMenu(hWindow), IDM_VIEWPORT_NEW, MF_BYCOMMAND | (fEnable ? MF_ENABLED : MF_GRAYED));
903#elif WITH_DEVELOPER_MODE
904 // Halt controls
905 gtk_widget_set_sensitive(widget: btnPlay, sensitive: Game.Network.isLobbyActive() || fEnable);
906 gtk_widget_set_sensitive(widget: btnHalt, sensitive: Game.Network.isLobbyActive() || fEnable);
907
908 // Edit modes
909 gtk_widget_set_sensitive(widget: btnModePlay, sensitive: Game.Network.isLobbyActive() || fEnable);
910 gtk_widget_set_sensitive(widget: btnModeEdit, sensitive: Game.Network.isLobbyActive() || fEnable);
911 gtk_widget_set_sensitive(widget: btnModeDraw, sensitive: Game.Network.isLobbyActive() || fEnable);
912
913 // Console input
914 gtk_widget_set_sensitive(widget: txtScript, sensitive: Game.Network.isLobbyActive() || fEnable);
915
916 // File menu
917 // C4Network2 will have to handle that cases somehow (TODO: test)
918 gtk_widget_set_sensitive(widget: fileRecord, sensitive: Game.IsRunning && Game.Control.IsRuntimeRecordPossible());
919 gtk_widget_set_sensitive(widget: fileSaveGame, sensitive: fEnable && Game.Players.GetCount());
920 gtk_widget_set_sensitive(widget: fileSaveGameAs, sensitive: fEnable && Game.Players.GetCount());
921 gtk_widget_set_sensitive(widget: fileSave, sensitive: fEnable);
922 gtk_widget_set_sensitive(widget: fileSaveAs, sensitive: fEnable);
923 gtk_widget_set_sensitive(widget: fileClose, sensitive: fEnable);
924
925 // Components menu
926 gtk_widget_set_sensitive(widget: compObjects, sensitive: fEnable && Editing);
927 gtk_widget_set_sensitive(widget: compScript, sensitive: fEnable && Editing);
928 gtk_widget_set_sensitive(widget: compInfo, sensitive: fEnable && Editing);
929 gtk_widget_set_sensitive(widget: compTitle, sensitive: fEnable && Editing);
930
931 // Player & viewport menu
932 gtk_widget_set_sensitive(widget: plrJoin, sensitive: fEnable && Editing);
933 gtk_widget_set_sensitive(widget: viewNew, sensitive: fEnable);
934#endif // WITH_DEVELOPER_MODE / _WIN32
935}
936
937bool C4Console::FileOpen()
938{
939 // Get scenario file name
940 char c4sfile[512 + 1] = "";
941 if (!FileSelect(sFilename: c4sfile, iSize: 512,
942 szFilter: "Clonk 4 Scenario\0*.c4s;*.c4f;Scenario.txt\0\0",
943 dwFlags: OFN_HIDEREADONLY | OFN_FILEMUSTEXIST
944 )) return false;
945 // Compose command line
946 char cmdline[2000] = "";
947 SAppend(szSource: "\"", szTarget: cmdline, iMaxL: 1999); SAppend(szSource: c4sfile, szTarget: cmdline, iMaxL: 1999); SAppend(szSource: "\" ", szTarget: cmdline, iMaxL: 1999);
948 // Open game
949 OpenGame(szCmdLine: cmdline);
950 return true;
951}
952
953bool C4Console::FileOpenWPlrs()
954{
955 // Get scenario file name
956 char c4sfile[512 + 1] = "";
957 if (!FileSelect(sFilename: c4sfile, iSize: 512,
958 szFilter: "Clonk 4 Scenario\0*.c4s;*.c4f\0\0",
959 dwFlags: OFN_HIDEREADONLY | OFN_FILEMUSTEXIST
960 )) return false;
961 // Get player file name(s)
962 char c4pfile[4096 + 1] = "";
963 if (!FileSelect(sFilename: c4pfile, iSize: 4096,
964 szFilter: "Clonk 4 Player\0*.c4p\0\0",
965 dwFlags: OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT | OFN_EXPLORER
966 )) return false;
967 // Compose command line
968 char cmdline[6000] = "";
969 SAppend(szSource: "\"", szTarget: cmdline, iMaxL: 5999); SAppend(szSource: c4sfile, szTarget: cmdline, iMaxL: 5999); SAppend(szSource: "\" ", szTarget: cmdline, iMaxL: 5999);
970 if (DirectoryExists(szFileName: c4pfile)) // Multiplayer
971 {
972 const char *cptr = c4pfile + SLen(sptr: c4pfile) + 1;
973 while (*cptr)
974 {
975 SAppend(szSource: "\"", szTarget: cmdline, iMaxL: 5999);
976 SAppend(szSource: c4pfile, szTarget: cmdline, iMaxL: 5999); SAppend(DirSep, szTarget: cmdline, iMaxL: 5999);
977 SAppend(szSource: cptr, szTarget: cmdline, iMaxL: 5999); SAppend(szSource: "\" ", szTarget: cmdline, iMaxL: 5999);
978 cptr += SLen(sptr: cptr) + 1;
979 }
980 }
981 else // Single player
982 {
983 SAppend(szSource: "\"", szTarget: cmdline, iMaxL: 5999); SAppend(szSource: c4pfile, szTarget: cmdline, iMaxL: 5999); SAppend(szSource: "\" ", szTarget: cmdline, iMaxL: 5999);
984 }
985 // Open game
986 OpenGame(szCmdLine: cmdline);
987 return true;
988}
989
990bool C4Console::FileClose()
991{
992 return CloseGame();
993}
994
995#ifdef _WIN32
996bool C4Console::FileSelect(char *sFilename, int iSize, const char *szFilter, DWORD dwFlags, bool fSave)
997#else
998bool C4Console::FileSelect(char *sFilename, int iSize, const char *szFilter, uint32_t dwFlags, bool fSave)
999#endif
1000{
1001#ifdef _WIN32
1002 OPENFILENAMEA ofn{};
1003 ofn.lStructSize = sizeof(ofn);
1004 ofn.hwndOwner = hWindow;
1005 ofn.lpstrFilter = szFilter;
1006 ofn.lpstrFile = sFilename;
1007 ofn.nMaxFile = iSize;
1008 ofn.Flags = dwFlags;
1009
1010 bool fResult;
1011 if (fSave)
1012 fResult = GetSaveFileNameA(&ofn);
1013 else
1014 fResult = GetOpenFileNameA(&ofn);
1015
1016 // Reset working directory to exe path as Windows file dialog might have changed it
1017 SetCurrentDirectoryA(Config.General.ExePath);
1018 return fResult;
1019#elif WITH_DEVELOPER_MODE
1020 GtkWidget *dialog = gtk_file_chooser_dialog_new(title: fSave ? "Save file..." : "Load file...", GTK_WINDOW(window), action: fSave ? GTK_FILE_CHOOSER_ACTION_SAVE : GTK_FILE_CHOOSER_ACTION_OPEN, first_button_text: "format-text-bold", GTK_RESPONSE_CANCEL, fSave ? "document-save" : "document-open", GTK_RESPONSE_ACCEPT, nullptr);
1021
1022 // TODO: Set dialog modal?
1023
1024 if (g_path_is_absolute(file_name: sFilename))
1025 gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), filename: sFilename);
1026 else if (fSave)
1027 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), name: sFilename);
1028
1029 // Install file filter
1030 while (*szFilter)
1031 {
1032 char pattern[16 + 1];
1033
1034 GtkFileFilter *filter = gtk_file_filter_new();
1035 gtk_file_filter_set_name(filter, name: szFilter);
1036 szFilter += SLen(sptr: szFilter) + 1;
1037
1038 while (true)
1039 {
1040 SCopyUntil(szSource: szFilter, sTarget: pattern, cUntil: ';', iMaxL: 16);
1041
1042 int len = SLen(sptr: pattern);
1043 char last_c = szFilter[len];
1044
1045 szFilter += (len + 1);
1046
1047 // Got not all of the filter, try again.
1048 if (last_c != ';' && last_c != '\0')
1049 continue;
1050
1051 gtk_file_filter_add_pattern(filter, pattern);
1052 if (last_c == '\0') break;
1053 }
1054
1055 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
1056 }
1057
1058 gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), select_multiple: (dwFlags & OFN_ALLOWMULTISELECT) != 0);
1059 gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dialog), local_only: true);
1060
1061 int response;
1062 while (true)
1063 {
1064 response = gtk_dialog_run(GTK_DIALOG(dialog));
1065 if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT) break;
1066
1067 bool error = false;
1068
1069 // Check for OFN_FILEMUSTEXIST
1070 if ((dwFlags & OFN_ALLOWMULTISELECT) == 0)
1071 {
1072 char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
1073
1074 if ((dwFlags & OFN_FILEMUSTEXIST) && !g_file_test(filename, test: G_FILE_TEST_IS_REGULAR))
1075 {
1076 Message(szMessage: std::format(fmt: "File \"{}\" does not exist", args&: filename).c_str(), fQuery: false);
1077 error = true;
1078 }
1079
1080 g_free(mem: filename);
1081 }
1082
1083 if (!error) break;
1084 }
1085
1086 if (response != GTK_RESPONSE_ACCEPT)
1087 {
1088 gtk_widget_destroy(widget: dialog);
1089 return false;
1090 }
1091
1092 // Build result string
1093 if ((dwFlags & OFN_ALLOWMULTISELECT) == 0)
1094 {
1095 // Just the file name without multiselect
1096 char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
1097 SCopy(szSource: filename, sTarget: sFilename, iMaxL: iSize);
1098 g_free(mem: filename);
1099 }
1100 else
1101 {
1102 // Otherwise its the folder followed by the file names,
1103 // separated by '\0'-bytes
1104 char *folder = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(dialog));
1105 int len = SLen(sptr: folder);
1106
1107 if (iSize > 0) SCopy(szSource: folder, sTarget: sFilename, iMaxL: (std::min)(a: len + 1, b: iSize));
1108 iSize -= (len + 1); sFilename += (len + 1);
1109 g_free(mem: folder);
1110
1111 GSList *files = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
1112 for (GSList *item = files; item != nullptr; item = item->next)
1113 {
1114 const char *file = static_cast<const char *>(item->data);
1115 char *basefile = g_path_get_basename(file_name: file);
1116
1117 int len = SLen(sptr: basefile);
1118 if (iSize > 0) SCopy(szSource: basefile, sTarget: sFilename, iMaxL: (std::min)(a: len + 1, b: iSize));
1119 iSize -= (len + 1); sFilename += (len + 1);
1120
1121 g_free(mem: basefile);
1122 g_free(mem: item->data);
1123 }
1124
1125 // End of list
1126 *sFilename = '\0';
1127 g_slist_free(list: files);
1128 }
1129
1130 gtk_widget_destroy(widget: dialog);
1131 return true;
1132#endif // WITH_DEVELOPER_MODE / _WIN32
1133 return 0;
1134}
1135
1136bool C4Console::FileRecord()
1137{
1138 // only in running mode
1139 if (!Game.IsRunning || !Game.Control.IsRuntimeRecordPossible()) return false;
1140 // start record!
1141 Game.Control.RequestRuntimeRecord();
1142 // disable menuitem
1143#ifdef _WIN32
1144 EnableMenuItem(GetMenu(hWindow), IDM_FILE_RECORD, MF_BYCOMMAND | MF_GRAYED);
1145#elif WITH_DEVELOPER_MODE
1146 gtk_widget_set_sensitive(widget: fileRecord, sensitive: false);
1147#endif
1148 return true;
1149}
1150
1151void C4Console::ClearPointers(C4Object *pObj)
1152{
1153 EditCursor.ClearPointers(pObj);
1154 PropertyDlg.ClearPointers(pObj);
1155}
1156
1157void C4Console::Default()
1158{
1159 EditCursor.Default();
1160 PropertyDlg.Default();
1161 ToolsDlg.Default();
1162}
1163
1164void C4Console::Clear()
1165{
1166 C4ConsoleBase::Clear();
1167
1168 EditCursor.Clear();
1169 PropertyDlg.Clear();
1170 ToolsDlg.Clear();
1171 ClearViewportMenu();
1172 ClearPlayerMenu();
1173 ClearNetMenu();
1174#ifndef _WIN32
1175 Application.Quit();
1176#endif
1177}
1178
1179void C4Console::Close()
1180{
1181 Application.Quit();
1182}
1183
1184bool C4Console::FileQuit()
1185{
1186 Close();
1187 return true;
1188}
1189
1190#define C4COPYRIGHT_YEAR "2008" // might make this dynamic some time...
1191#define C4COPYRIGHT_COMPANY "RedWolf Design GmbH"
1192
1193void C4Console::HelpAbout()
1194{
1195#ifdef _WIN32
1196 static constexpr auto Message = _CRT_WIDE(C4ENGINECAPTION) L" " _CRT_WIDE(C4VERSION) L"\n\nCopyright (c) " _CRT_WIDE(C4COPYRIGHT_YEAR) L" " _CRT_WIDE(C4COPYRIGHT_COMPANY);
1197 MessageBox(nullptr, Message, _CRT_WIDE(C4ENGINECAPTION), MB_ICONINFORMATION | MB_TASKMODAL);
1198#elif WITH_DEVELOPER_MODE
1199 gtk_show_about_dialog(GTK_WINDOW(window), first_property_name: "name", C4ENGINECAPTION, "version", C4VERSION, "copyright", "Copyright (c) " C4COPYRIGHT_YEAR " " C4COPYRIGHT_COMPANY, nullptr);
1200#endif // WITH_DEVELOPER_MODE / _WIN32
1201}
1202
1203void C4Console::ViewportNew()
1204{
1205 Game.CreateViewport(iPlayer: NO_OWNER);
1206}
1207
1208bool C4Console::UpdateCursorBar(const char *szCursor)
1209{
1210 if (!Active) return false;
1211#ifdef _WIN32
1212 // Cursor
1213 SetDlgItemText(hWindow, IDC_STATICCURSOR, StdStringEncodingConverter::WinAcpToUtf16(szCursor).c_str());
1214 UpdateWindow(GetDlgItem(hWindow, IDC_STATICCURSOR));
1215#elif WITH_DEVELOPER_MODE
1216 gtk_label_set_label(GTK_LABEL(lblCursor), str: ClonkToGtk(text: szCursor).c_str());
1217#endif
1218 return true;
1219}
1220
1221bool C4Console::UpdateViewportMenu()
1222{
1223 if (!Active) return false;
1224 ClearViewportMenu();
1225#ifdef _WIN32
1226 HMENU hMenu = GetSubMenu(GetMenu(hWindow), MenuIndexViewport);
1227#endif
1228 for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next)
1229 {
1230 const std::string text{LoadResStr(id: C4ResStrTableKey::IDS_CNS_NEWPLRVIEWPORT, args: pPlr->GetName())};
1231#ifdef _WIN32
1232 AddMenuItem(hMenu, IDM_VIEWPORT_NEW1 + pPlr->Number, text.c_str());
1233#elif WITH_DEVELOPER_MODE
1234 GtkWidget *menuItem = gtk_menu_item_new_with_label(label: ClonkToGtk(text).c_str());
1235 gtk_menu_shell_append(GTK_MENU_SHELL(menuViewport), child: menuItem);
1236 g_signal_connect(G_OBJECT(menuItem), "activate", G_CALLBACK(OnViewNewPlr), GINT_TO_POINTER(pPlr->Number));
1237 gtk_widget_show(widget: menuItem);
1238#endif // WITH_DEVELOPER_MODE / _WIN32
1239 }
1240 return true;
1241}
1242
1243void C4Console::ClearViewportMenu()
1244{
1245 if (!Active) return;
1246#ifdef _WIN32
1247 HMENU hMenu = GetSubMenu(GetMenu(hWindow), MenuIndexViewport);
1248 while (DeleteMenu(hMenu, 1, MF_BYPOSITION));
1249#elif WITH_DEVELOPER_MODE
1250 GList *children = gtk_container_get_children(GTK_CONTAINER(menuViewport));
1251 for (GList *item = children; item != nullptr; item = item->next)
1252 {
1253 if (item->data != viewNew)
1254 gtk_container_remove(GTK_CONTAINER(menuViewport), GTK_WIDGET(item->data));
1255 }
1256 g_list_free(list: children);
1257#endif // WITH_DEVELOPER_MODE / _WIN32
1258}
1259
1260#ifdef _WIN32
1261bool C4Console::AddMenuItem(HMENU hMenu, DWORD dwID, const char *szString, bool fEnabled)
1262{
1263 if (!Active) return false;
1264
1265 std::wstring text{StdStringEncodingConverter::WinAcpToUtf16(szString)};
1266
1267 MENUITEMINFO minfo{};
1268 minfo.cbSize = sizeof(minfo);
1269 minfo.fMask = MIIM_ID | MIIM_TYPE | MIIM_DATA | MIIM_STATE;
1270 minfo.fType = MFT_STRING;
1271 minfo.wID = dwID;
1272 minfo.dwTypeData = text.data();
1273 minfo.cch = text.size();
1274 if (!fEnabled) minfo.fState |= MFS_GRAYED;
1275 return InsertMenuItem(hMenu, 0, FALSE, &minfo);
1276}
1277
1278bool C4Console::GetPositionData(std::string &id, std::string &subKey, bool &storeSize) const
1279{
1280 id = "Main";
1281 subKey = Config.GetSubkeyPath("Console");
1282 storeSize = false;
1283 return true;
1284}
1285
1286std::wstring C4Console::GetDialogItemText(const HWND dlg, const int item)
1287{
1288 std::wstring result;
1289 const LRESULT textSize{SendDlgItemMessage(dlg, item, WM_GETTEXTLENGTH, 0, 0)};
1290
1291 result.resize_and_overwrite(textSize, [dlg, item, textSize](wchar_t *const ptr, const std::size_t size)
1292 {
1293 return GetDlgItemText(dlg, item, ptr, textSize + 1);
1294 });
1295
1296 return result;
1297}
1298#endif // _WIN32
1299
1300bool C4Console::UpdateModeCtrls(int iMode)
1301{
1302 if (!Active) return false;
1303
1304#ifdef _WIN32
1305 SendDlgItemMessage(hWindow, IDC_BUTTONMODEPLAY, BM_SETSTATE, (iMode == C4CNS_ModePlay), 0);
1306 UpdateWindow(GetDlgItem(hWindow, IDC_BUTTONMODEPLAY));
1307 SendDlgItemMessage(hWindow, IDC_BUTTONMODEEDIT, BM_SETSTATE, (iMode == C4CNS_ModeEdit), 0);
1308 UpdateWindow(GetDlgItem(hWindow, IDC_BUTTONMODEEDIT));
1309 SendDlgItemMessage(hWindow, IDC_BUTTONMODEDRAW, BM_SETSTATE, (iMode == C4CNS_ModeDraw), 0);
1310 UpdateWindow(GetDlgItem(hWindow, IDC_BUTTONMODEDRAW));
1311#elif WITH_DEVELOPER_MODE
1312 // Prevents recursion
1313 g_signal_handler_block(instance: btnModePlay, handler_id: handlerModePlay);
1314 g_signal_handler_block(instance: btnModeEdit, handler_id: handlerModeEdit);
1315 g_signal_handler_block(instance: btnModeDraw, handler_id: handlerModeDraw);
1316
1317 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btnModePlay), is_active: iMode == C4CNS_ModePlay);
1318 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btnModeEdit), is_active: iMode == C4CNS_ModeEdit);
1319 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btnModeDraw), is_active: iMode == C4CNS_ModeDraw);
1320
1321 g_signal_handler_unblock(instance: btnModePlay, handler_id: handlerModePlay);
1322 g_signal_handler_unblock(instance: btnModeEdit, handler_id: handlerModeEdit);
1323 g_signal_handler_unblock(instance: btnModeDraw, handler_id: handlerModeDraw);
1324#endif // WITH_DEVELOPER_MODE / _WIN32
1325 return true;
1326}
1327
1328void C4Console::EditTitle()
1329{
1330 if (Game.Network.isEnabled()) return;
1331#ifdef _WIN32
1332 Game.Title.ShowDialog(hWindow);
1333#endif
1334}
1335
1336void C4Console::EditScript()
1337{
1338 if (Game.Network.isEnabled()) return;
1339#ifdef _WIN32
1340 Game.Script.ShowDialog(hWindow);
1341#endif
1342 Game.ScriptEngine.ReLink(rDefs: &Game.Defs);
1343}
1344
1345void C4Console::EditInfo()
1346{
1347 if (Game.Network.isEnabled()) return;
1348#ifdef _WIN32
1349 Game.Info.ShowDialog(hWindow);
1350#endif
1351}
1352
1353void C4Console::EditObjects()
1354{
1355 ObjectListDlg.Open();
1356}
1357
1358void C4Console::UpdateInputCtrl()
1359{
1360 int cnt;
1361 C4AulScriptFunc *pRef;
1362#ifdef _WIN32
1363 HWND hCombo = GetDlgItem(hWindow, IDC_COMBOINPUT);
1364 // Clear
1365 SendMessage(hCombo, CB_RESETCONTENT, 0, 0);
1366#elif WITH_DEVELOPER_MODE
1367 GtkEntryCompletion *completion = gtk_entry_get_completion(GTK_ENTRY(txtScript));
1368 if (!completion)
1369 {
1370 completion = gtk_entry_completion_new();
1371 GtkListStore *store = gtk_list_store_new(n_columns: 1, G_TYPE_STRING);
1372 gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(store));
1373 g_object_unref(G_OBJECT(store));
1374 gtk_entry_completion_set_text_column(completion, column: 0);
1375 gtk_entry_set_completion(GTK_ENTRY(txtScript), completion);
1376 g_object_unref(G_OBJECT(completion));
1377 }
1378
1379 GtkTreeIter iter;
1380 GtkListStore *store = GTK_LIST_STORE(gtk_entry_completion_get_model(completion));
1381 gtk_list_store_clear(list_store: store);
1382#endif // WITH_DEVELOPER_MODE / _WIN32
1383 // add global and standard functions
1384 for (C4AulFunc *pFn = Game.ScriptEngine.GetFirstFunc(); pFn; pFn = Game.ScriptEngine.GetNextFunc(pFunc: pFn))
1385 if (pFn->GetPublic())
1386 {
1387#ifdef _WIN32
1388 SendMessage(hCombo, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(std::format(L"{}()", StdStringEncodingConverter::WinAcpToUtf16(pFn->Name)).c_str()));
1389#elif WITH_DEVELOPER_MODE
1390 gtk_list_store_append(list_store: store, iter: &iter);
1391 gtk_list_store_set(list_store: store, iter: &iter, 0, pFn->Name, -1);
1392#endif
1393 }
1394 // Add scenario script functions
1395#ifdef _WIN32
1396 if (pRef = Game.Script.GetSFunc(0))
1397 SendMessage(hCombo, CB_INSERTSTRING, 0, reinterpret_cast<LPARAM>(L"----------"));
1398#endif
1399 for (cnt = 0; pRef = Game.Script.GetSFunc(iIndex: cnt); cnt++)
1400 {
1401#ifdef _WIN32
1402 SendMessage(hCombo, CB_INSERTSTRING, 0, reinterpret_cast<LPARAM>(std::format(L"{}()", StdStringEncodingConverter::WinAcpToUtf16(pRef->Name)).c_str()));
1403#elif WITH_DEVELOPER_MODE
1404 gtk_list_store_append(list_store: store, iter: &iter);
1405 gtk_list_store_set(list_store: store, iter: &iter, 0, pRef->Name, -1);
1406#endif
1407 }
1408}
1409
1410bool C4Console::UpdatePlayerMenu()
1411{
1412 if (!Active) return false;
1413 ClearPlayerMenu();
1414#ifdef _WIN32
1415 HMENU hMenu = GetSubMenu(GetMenu(hWindow), MenuIndexPlayer);
1416#endif
1417 for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next)
1418 {
1419 const std::string text{
1420 Game.Network.isEnabled()
1421 ? LoadResStr(id: C4ResStrTableKey::IDS_CNS_PLRQUITNET, args: pPlr->GetName(), args&: pPlr->AtClientName)
1422 : LoadResStr(id: C4ResStrTableKey::IDS_CNS_PLRQUIT, args: pPlr->GetName())
1423 };
1424#ifdef _WIN32
1425 AddMenuItem(hMenu, IDM_PLAYER_QUIT1 + pPlr->Number, text.c_str(), (!Game.Network.isEnabled() || Game.Network.isHost()) && Editing);
1426#elif WITH_DEVELOPER_MODE
1427 // TODO: Implement AddMenuItem...
1428 GtkWidget *menuItem = gtk_menu_item_new_with_label(label: ClonkToGtk(text).c_str());
1429 gtk_menu_shell_append(GTK_MENU_SHELL(menuPlayer), child: menuItem);
1430 g_signal_connect(G_OBJECT(menuItem), "activate", G_CALLBACK(OnPlrQuit), GINT_TO_POINTER(pPlr->Number));
1431 gtk_widget_show(widget: menuItem);
1432
1433 gtk_widget_set_sensitive(widget: menuItem, sensitive: (!Game.Network.isEnabled() || Game.Network.isHost()) && Editing);
1434#endif // WITH_DEVELOPER_MODE / _WIN32
1435 }
1436 return true;
1437}
1438
1439void C4Console::ClearPlayerMenu()
1440{
1441 if (!Active) return;
1442#ifdef _WIN32
1443 HMENU hMenu = GetSubMenu(GetMenu(hWindow), MenuIndexPlayer);
1444 while (DeleteMenu(hMenu, 1, MF_BYPOSITION));
1445#elif WITH_DEVELOPER_MODE
1446 GList *children = gtk_container_get_children(GTK_CONTAINER(menuPlayer));
1447 for (GList *item = children; item != nullptr; item = item->next)
1448 {
1449 if (item->data != plrJoin)
1450 gtk_container_remove(GTK_CONTAINER(menuPlayer), GTK_WIDGET(item->data));
1451 }
1452 g_list_free(list: children);
1453#endif // _WIN32
1454}
1455
1456void C4Console::UpdateMenus()
1457{
1458 if (!Active) return;
1459 EnableControls(fEnable: fGameOpen);
1460 UpdatePlayerMenu();
1461 UpdateViewportMenu();
1462 UpdateNetMenu();
1463}
1464
1465void C4Console::PlayerJoin()
1466{
1467 // Get player file name(s)
1468 char c4pfile[4096 + 1] = "";
1469 if (!FileSelect(sFilename: c4pfile, iSize: 4096,
1470 szFilter: "Clonk 4 Player\0*.c4p\0\0",
1471 dwFlags: OFN_HIDEREADONLY | OFN_ALLOWMULTISELECT | OFN_EXPLORER
1472 )) return;
1473
1474 // Compose player file list
1475 char c4plist[6000] = "";
1476 // Multiple players
1477 if (DirectoryExists(szFileName: c4pfile))
1478 {
1479 const char *cptr = c4pfile + SLen(sptr: c4pfile) + 1;
1480 while (*cptr)
1481 {
1482 SNewSegment(szStr: c4plist);
1483 SAppend(szSource: c4pfile, szTarget: c4plist); SAppend(DirSep, szTarget: c4plist); SAppend(szSource: cptr, szTarget: c4plist);
1484 cptr += SLen(sptr: cptr) + 1;
1485 }
1486 }
1487 // Single player
1488 else
1489 {
1490 SAppend(szSource: c4pfile, szTarget: c4plist);
1491 }
1492
1493 // Join players (via network/ctrl queue)
1494 char szPlayerFilename[_MAX_PATH + 1];
1495 for (int iPar = 0; SCopySegment(fstr: c4plist, segn: iPar, tstr: szPlayerFilename, sepa: ';', _MAX_PATH); iPar++)
1496 if (szPlayerFilename[0])
1497 if (Game.Network.isEnabled())
1498 Game.Network.Players.JoinLocalPlayer(szLocalPlayerFilename: szPlayerFilename, fAdd: true);
1499 else
1500 Game.Players.CtrlJoinLocalNoNetwork(szFilename: szPlayerFilename, iAtClient: Game.Clients.getLocalID(), szAtClientName: Game.Clients.getLocalName());
1501}
1502
1503#ifdef _WIN32
1504void C4Console::UpdateMenuText(HMENU hMenu)
1505{
1506 HMENU hSubMenu;
1507 if (!Active) return;
1508 // File
1509 ModifyMenu(hMenu, MenuIndexFile, MF_BYPOSITION | MF_STRING, 0, StdStringEncodingConverter::WinAcpToUtf16(LoadResStr(C4ResStrTableKey::IDS_MNU_FILE)).c_str());
1510 hSubMenu = GetSubMenu(hMenu, MenuIndexFile);
1511 SetMenuItemText(hSubMenu, IDM_FILE_OPEN, LoadResStr(C4ResStrTableKey::IDS_MNU_OPEN));
1512 SetMenuItemText(hSubMenu, IDM_FILE_OPENWPLRS, LoadResStr(C4ResStrTableKey::IDS_MNU_OPENWPLRS));
1513 SetMenuItemText(hSubMenu, IDM_FILE_RECORD, LoadResStr(C4ResStrTableKey::IDS_MNU_RECORD));
1514 SetMenuItemText(hSubMenu, IDM_FILE_SAVE, LoadResStr(C4ResStrTableKey::IDS_MNU_SAVESCENARIO));
1515 SetMenuItemText(hSubMenu, IDM_FILE_SAVEAS, LoadResStr(C4ResStrTableKey::IDS_MNU_SAVESCENARIOAS));
1516 SetMenuItemText(hSubMenu, IDM_FILE_SAVEGAME, LoadResStr(C4ResStrTableKey::IDS_MNU_SAVEGAME));
1517 SetMenuItemText(hSubMenu, IDM_FILE_SAVEGAMEAS, LoadResStr(C4ResStrTableKey::IDS_MNU_SAVEGAMEAS));
1518 SetMenuItemText(hSubMenu, IDM_FILE_CLOSE, LoadResStr(C4ResStrTableKey::IDS_MNU_CLOSE));
1519 SetMenuItemText(hSubMenu, IDM_FILE_QUIT, LoadResStr(C4ResStrTableKey::IDS_MNU_QUIT));
1520 // Components
1521 ModifyMenu(hMenu, MenuIndexComponents, MF_BYPOSITION | MF_STRING, 0, StdStringEncodingConverter::WinAcpToUtf16(LoadResStr(C4ResStrTableKey::IDS_MNU_COMPONENTS)).c_str());
1522 hSubMenu = GetSubMenu(hMenu, MenuIndexComponents);
1523 SetMenuItemText(hSubMenu, IDM_COMPONENTS_SCRIPT, LoadResStr(C4ResStrTableKey::IDS_MNU_SCRIPT));
1524 SetMenuItemText(hSubMenu, IDM_COMPONENTS_TITLE, LoadResStr(C4ResStrTableKey::IDS_MNU_TITLE));
1525 SetMenuItemText(hSubMenu, IDM_COMPONENTS_INFO, LoadResStr(C4ResStrTableKey::IDS_MNU_INFO));
1526 // Player
1527 ModifyMenu(hMenu, MenuIndexPlayer, MF_BYPOSITION | MF_STRING, 0, StdStringEncodingConverter::WinAcpToUtf16(LoadResStr(C4ResStrTableKey::IDS_MNU_PLAYER)).c_str());
1528 hSubMenu = GetSubMenu(hMenu, MenuIndexPlayer);
1529 SetMenuItemText(hSubMenu, IDM_PLAYER_JOIN, LoadResStr(C4ResStrTableKey::IDS_MNU_JOIN));
1530 // Viewport
1531 ModifyMenu(hMenu, MenuIndexViewport, MF_BYPOSITION | MF_STRING, 0, StdStringEncodingConverter::WinAcpToUtf16(LoadResStr(C4ResStrTableKey::IDS_MNU_VIEWPORT)).c_str());
1532 hSubMenu = GetSubMenu(hMenu, MenuIndexViewport);
1533 SetMenuItemText(hSubMenu, IDM_VIEWPORT_NEW, LoadResStr(C4ResStrTableKey::IDS_MNU_NEW));
1534 // Help
1535 hSubMenu = GetSubMenu(hMenu, MenuIndexHelp);
1536 SetMenuItemText(hSubMenu, IDM_HELP_ABOUT, LoadResStr(C4ResStrTableKey::IDS_MENU_ABOUT));
1537}
1538#endif // _WIN32
1539
1540void C4Console::UpdateNetMenu()
1541{
1542 // Active & network hosting check
1543 if (!Active) return;
1544 if (!Game.Network.isHost() || !Game.Network.isEnabled()) return;
1545 // Clear old
1546 ClearNetMenu();
1547 // Insert menu
1548#ifdef _WIN32
1549 if (!InsertMenu(GetMenu(hWindow), MenuIndexHelp, MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(CreateMenu()), StdStringEncodingConverter::WinAcpToUtf16(LoadResStr(C4ResStrTableKey::IDS_MNU_NET)).c_str())) return;
1550#elif WITH_DEVELOPER_MODE
1551 itemNet = gtk_menu_item_new_with_label(label: LoadResStrGtk(id: C4ResStrTableKey::IDS_MNU_NET).c_str());
1552 GtkWidget *menuNet = gtk_menu_new();
1553 gtk_menu_item_set_submenu(GTK_MENU_ITEM(itemNet), submenu: menuNet);
1554 gtk_menu_shell_insert(GTK_MENU_SHELL(menuBar), child: itemNet, position: MenuIndexHelp);
1555#endif
1556 MenuIndexNet = MenuIndexHelp;
1557 MenuIndexHelp++;
1558
1559#ifdef _WIN32
1560 DrawMenuBar(hWindow);
1561 // Update menu
1562 HMENU hMenu = GetSubMenu(GetMenu(hWindow), MenuIndexNet);
1563#endif
1564
1565 // Host
1566 const auto text = LoadResStr(id: C4ResStrTableKey::IDS_MNU_NETHOST, args: Game.Clients.getLocalName(), args: Game.Clients.getLocalID());
1567#ifdef _WIN32
1568 AddMenuItem(hMenu, IDM_NET_CLIENT1 + Game.Clients.getLocalID(), text.c_str());
1569#elif WITH_DEVELOPER_MODE
1570 GtkWidget *item = gtk_menu_item_new_with_label(label: text.c_str());
1571 gtk_menu_shell_append(GTK_MENU_SHELL(menuNet), child: item);
1572 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(OnNetClient), GINT_TO_POINTER(Game.Clients.getLocalID()));
1573#endif
1574 // Clients
1575 for (C4Network2Client *pClient = Game.Network.Clients.GetNextClient(pClient: nullptr); pClient; pClient = Game.Network.Clients.GetNextClient(pClient))
1576 {
1577 const auto text = LoadResStrChoice(condition: pClient->isActivated(), ifTrue: C4ResStrTableKey::IDS_MNU_NETCLIENT, ifFalse: C4ResStrTableKey::IDS_MNU_NETCLIENTDE,
1578 args: pClient->getName(), args: pClient->getID());
1579#ifdef _WIN32
1580 AddMenuItem(hMenu, IDM_NET_CLIENT1 + pClient->getID(), text.c_str());
1581#elif WITH_DEVELOPER_MODE
1582 item = gtk_menu_item_new_with_label(label: text.c_str());
1583 gtk_menu_shell_append(GTK_MENU_SHELL(menuNet), child: item);
1584 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(OnNetClient), GINT_TO_POINTER(pClient->getID()));
1585#endif
1586 }
1587#if WITH_DEVELOPER_MODE
1588 gtk_widget_show_all(widget: itemNet);
1589#endif
1590 return;
1591}
1592
1593void C4Console::ClearNetMenu()
1594{
1595 if (!Active) return;
1596 if (MenuIndexNet < 0) return;
1597#ifdef _WIN32
1598 DeleteMenu(GetMenu(hWindow), MenuIndexNet, MF_BYPOSITION);
1599#elif WITH_DEVELOPER_MODE
1600 gtk_container_remove(GTK_CONTAINER(menuBar), widget: itemNet);
1601 itemNet = nullptr;
1602#endif
1603 MenuIndexNet = -1;
1604 MenuIndexHelp--;
1605#ifdef _WIN32
1606 DrawMenuBar(hWindow);
1607#endif
1608}
1609
1610void C4Console::SetCaption(const char *szCaption)
1611{
1612 if (!Active) return;
1613#ifdef _WIN32
1614 // Sorry, the window caption needs to be constant so
1615 // the window can be found using FindWindow
1616 SetTitle(C4ENGINECAPTION);
1617#else
1618 SetTitle(szCaption);
1619#endif
1620}
1621
1622void C4Console::Execute()
1623{
1624 EditCursor.Execute();
1625#ifdef _WIN32
1626 PropertyDlg.Execute();
1627#endif
1628 ObjectListDlg.Execute();
1629 UpdateStatusBars();
1630 Game.GraphicsSystem.Execute();
1631}
1632
1633bool C4Console::OpenGame(const char *szCmdLine)
1634{
1635 bool fGameWasOpen = fGameOpen;
1636 // Close any old game
1637 CloseGame();
1638
1639 // Default game dependent members
1640 Default();
1641 SetCaption(GetFilename(path: Game.ScenarioFile.GetName()));
1642 // Init game dependent members
1643 if (!EditCursor.Init()) return false;
1644 // Default game - only if open before, because we do not want to default out the GUI
1645 if (fGameWasOpen) Game.Default();
1646
1647 // Pretend to be called with a new commandline
1648 if (szCmdLine)
1649 Game.ParseCommandLine(szCmdLine);
1650
1651 // PreInit is required because GUI has been deleted
1652 if (!Game.PreInit()) { Game.Clear(); return false; }
1653
1654 // Init game
1655 if (!Game.Init())
1656 {
1657 Game.Clear();
1658 return false;
1659 }
1660
1661 // Console updates
1662 fGameOpen = true;
1663 UpdateInputCtrl();
1664 EnableControls(fEnable: fGameOpen);
1665 UpdatePlayerMenu();
1666 UpdateViewportMenu();
1667
1668 return true;
1669}
1670
1671bool C4Console::CloseGame()
1672{
1673 if (!fGameOpen) return false;
1674 Game.Clear();
1675 Game.GameOver = false; // No leftover values when exiting on closed game
1676 Game.GameOverDlgShown = false;
1677 fGameOpen = false;
1678 EnableControls(fEnable: fGameOpen);
1679 SetCaption(LoadResStr(id: C4ResStrTableKey::IDS_CNS_CONSOLE));
1680 return true;
1681}
1682
1683bool C4Console::TogglePause()
1684{
1685 return Game.TogglePause();
1686}
1687
1688#if WITH_DEVELOPER_MODE
1689
1690C4Console::GCharStringWrapper C4Console::ClonkToGtk(const std::string_view text)
1691{
1692 return {.String: TextEncodingConverter.ClonkToUtf8(input: text)};
1693}
1694
1695
1696// GTK+ callbacks
1697
1698void C4Console::OnScriptEntry(GtkWidget *entry, gpointer data)
1699{
1700 C4Console *console = static_cast<C4Console *>(data);
1701 console->In(szText: gtk_entry_get_text(GTK_ENTRY(console->txtScript)));
1702 gtk_editable_select_region(GTK_EDITABLE(console->txtScript), start_pos: 0, end_pos: -1);
1703}
1704
1705void C4Console::OnPlay(GtkWidget *button, gpointer data)
1706{
1707 static_cast<C4Console *>(data)->DoPlay();
1708
1709 // Must update haltctrls even if DoPlay did noting to restore
1710 // previous settings since GTK may have released this toggle button
1711 static_cast<C4Console *>(data)->UpdateHaltCtrls(fHalt: !!Game.HaltCount);
1712}
1713
1714void C4Console::OnHalt(GtkWidget *button, gpointer data)
1715{
1716 static_cast<C4Console *>(data)->DoHalt();
1717
1718 // Must update haltctrls even if DoPlay did noting to restore
1719 // previous settings since GTK may have released this toggle button
1720 static_cast<C4Console *>(data)->UpdateHaltCtrls(fHalt: !!Game.HaltCount);
1721}
1722
1723void C4Console::OnModePlay(GtkWidget *button, gpointer data)
1724{
1725 static_cast<C4Console *>(data)->EditCursor.SetMode(C4CNS_ModePlay);
1726}
1727
1728void C4Console::OnModeEdit(GtkWidget *button, gpointer data)
1729{
1730 static_cast<C4Console *>(data)->EditCursor.SetMode(C4CNS_ModeEdit);
1731}
1732
1733void C4Console::OnModeDraw(GtkWidget *button, gpointer data)
1734{
1735 static_cast<C4Console *>(data)->EditCursor.SetMode(C4CNS_ModeDraw);
1736}
1737
1738void C4Console::OnFileOpen(GtkWidget *item, gpointer data)
1739{
1740 static_cast<C4Console *>(data)->FileOpen();
1741}
1742
1743void C4Console::OnFileOpenWPlrs(GtkWidget *item, gpointer data)
1744{
1745 static_cast<C4Console *>(data)->FileOpenWPlrs();
1746}
1747
1748void C4Console::OnFileSave(GtkWidget *item, gpointer data)
1749{
1750 static_cast<C4Console *>(data)->FileSave(fSaveGame: false);
1751}
1752
1753void C4Console::OnFileSaveAs(GtkWidget *item, gpointer data)
1754{
1755 static_cast<C4Console *>(data)->FileSaveAs(fSaveGame: false);
1756}
1757
1758void C4Console::OnFileSaveGame(GtkWidget *item, gpointer data)
1759{
1760 static_cast<C4Console *>(data)->FileSave(fSaveGame: true);
1761}
1762
1763void C4Console::OnFileSaveGameAs(GtkWidget *item, gpointer data)
1764{
1765 static_cast<C4Console *>(data)->FileSaveAs(fSaveGame: true);
1766}
1767
1768void C4Console::OnFileRecord(GtkWidget *item, gpointer data)
1769{
1770 static_cast<C4Console *>(data)->FileRecord();
1771}
1772
1773void C4Console::OnFileClose(GtkWidget *item, gpointer data)
1774{
1775 static_cast<C4Console *>(data)->FileClose();
1776}
1777
1778void C4Console::OnFileQuit(GtkWidget *item, gpointer data)
1779{
1780 static_cast<C4Console *>(data)->FileQuit();
1781}
1782
1783void C4Console::OnCompObjects(GtkWidget *item, gpointer data)
1784{
1785 static_cast<C4Console *>(data)->EditObjects();
1786}
1787
1788void C4Console::OnCompScript(GtkWidget *item, gpointer data)
1789{
1790 static_cast<C4Console *>(data)->EditScript();
1791}
1792
1793void C4Console::OnCompTitle(GtkWidget *item, gpointer data)
1794{
1795 static_cast<C4Console *>(data)->EditTitle();
1796}
1797
1798void C4Console::OnCompInfo(GtkWidget *item, gpointer data)
1799{
1800 static_cast<C4Console *>(data)->EditInfo();
1801}
1802
1803void C4Console::OnPlrJoin(GtkWidget *item, gpointer data)
1804{
1805 static_cast<C4Console *>(data)->PlayerJoin();
1806}
1807
1808void C4Console::OnPlrQuit(GtkWidget *item, gpointer data)
1809{
1810 Game.Control.Input.Add(eType: CID_EliminatePlayer, pCtrl: new C4ControlEliminatePlayer(GPOINTER_TO_INT(data)));
1811}
1812
1813void C4Console::OnViewNew(GtkWidget *item, gpointer data)
1814{
1815 static_cast<C4Console *>(data)->ViewportNew();
1816}
1817
1818void C4Console::OnViewNewPlr(GtkWidget *item, gpointer data)
1819{
1820 Game.CreateViewport(GPOINTER_TO_INT(data));
1821}
1822
1823void C4Console::OnHelpAbout(GtkWidget *item, gpointer data)
1824{
1825 static_cast<C4Console *>(data)->HelpAbout();
1826}
1827
1828void C4Console::OnNetClient(GtkWidget *item, gpointer data)
1829{
1830 if (!Game.Control.isCtrlHost()) return;
1831 Game.Clients.CtrlRemove(pClient: Game.Clients.getClientByID(GPOINTER_TO_INT(data)), szReason: LoadResStr(id: C4ResStrTableKey::IDS_MSG_KICKBYMENU));
1832}
1833
1834C4Console::GCharStringWrapper LoadResStrGtkV(C4ResStrTableKey id)
1835{
1836 return C4Console::ClonkToGtk(text: LoadResStrV(id));
1837}
1838
1839#endif // WITH_DEVELOPER_MODE
1840