1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, Sven2
6 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// Startup screen for non-parameterized engine start (stub)
19
20#include "C4GuiResource.h"
21#include <C4Include.h>
22#include <C4StartupMainDlg.h>
23#include <C4UpdateDlg.h>
24#include <C4Version.h>
25
26#include <C4StartupNetDlg.h>
27#include <C4StartupScenSelDlg.h>
28#include <C4StartupOptionsDlg.h>
29#include <C4StartupAboutDlg.h>
30#include <C4StartupPlrSelDlg.h>
31#include <C4Startup.h>
32#include <C4Game.h>
33#include <C4Log.h>
34#include "C4TextEncoding.h"
35
36#include <format>
37
38C4StartupMainDlg::C4StartupMainDlg() : C4StartupDlg(nullptr) // create w/o title; it is drawn in custom draw proc
39{
40 fFirstShown = true;
41 // screen calculations
42 int iButtonPadding = 2;
43 int iButtonHeight = C4GUI_BigButtonHgt;
44 C4GUI::ComponentAligner caMain(rcBounds, 0, 0, true);
45 C4GUI::ComponentAligner caRightPanel(caMain.GetFromRight(iWdt: rcBounds.Wdt * 2 / 5), rcBounds.Wdt / 26, 40 + rcBounds.Hgt / 8);
46 C4GUI::ComponentAligner caButtons(caRightPanel.GetAll(), 0, iButtonPadding);
47 // main menu buttons
48 C4GUI::CallbackButton<C4StartupMainDlg> *btn;
49 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupMainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_LOCALGAME), caButtons.GetFromTop(iHgt: iButtonHeight), &C4StartupMainDlg::OnStartBtn));
50 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_STARTGAME));
51 btn->SetCustomGraphics(pCustomGfx: &C4Startup::Get()->Graphics.barMainButtons, pCustomGfxDown: &C4Startup::Get()->Graphics.barMainButtonsDown);
52 pStartButton = btn;
53 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupMainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_BTN_NETWORKGAME), caButtons.GetFromTop(iHgt: iButtonHeight), &C4StartupMainDlg::OnNetJoinBtn));
54 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_NETWORKGAME));
55 btn->SetCustomGraphics(pCustomGfx: &C4Startup::Get()->Graphics.barMainButtons, pCustomGfxDown: &C4Startup::Get()->Graphics.barMainButtonsDown);
56 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupMainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_DLG_PLAYERSELECTION), caButtons.GetFromTop(iHgt: iButtonHeight), &C4StartupMainDlg::OnPlayerSelectionBtn));
57 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_PLAYERSELECTION));
58 btn->SetCustomGraphics(pCustomGfx: &C4Startup::Get()->Graphics.barMainButtons, pCustomGfxDown: &C4Startup::Get()->Graphics.barMainButtonsDown);
59 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupMainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_DLG_OPTIONS), caButtons.GetFromTop(iHgt: iButtonHeight), &C4StartupMainDlg::OnOptionsBtn));
60 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_OPTIONS));
61 btn->SetCustomGraphics(pCustomGfx: &C4Startup::Get()->Graphics.barMainButtons, pCustomGfxDown: &C4Startup::Get()->Graphics.barMainButtonsDown);
62 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupMainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_DLG_ABOUT), caButtons.GetFromTop(iHgt: iButtonHeight), &C4StartupMainDlg::OnAboutBtn));
63 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_ABOUT));
64 btn->SetCustomGraphics(pCustomGfx: &C4Startup::Get()->Graphics.barMainButtons, pCustomGfxDown: &C4Startup::Get()->Graphics.barMainButtonsDown);
65 AddElement(pChild: btn = new C4GUI::CallbackButton<C4StartupMainDlg>(LoadResStr(id: C4ResStrTableKey::IDS_DLG_EXIT), caButtons.GetFromTop(iHgt: iButtonHeight), &C4StartupMainDlg::OnExitBtn));
66 btn->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_EXIT));
67 btn->SetCustomGraphics(pCustomGfx: &C4Startup::Get()->Graphics.barMainButtons, pCustomGfxDown: &C4Startup::Get()->Graphics.barMainButtonsDown);
68 // list of selected players
69 AddElement(pChild: pParticipantsLbl = new C4GUI::Label("test", GetClientRect().Wdt * 39 / 40, GetClientRect().Hgt * 9 / 10, ARight, 0xffffffff, &C4GUI::GetRes()->TitleFont, false));
70 pParticipantsLbl->SetToolTip(LoadResStr(id: C4ResStrTableKey::IDS_DLGTIP_SELECTEDPLAYERS));
71
72 CStdFont &trademarkFont = C4GUI::GetRes()->MiniFont;
73 AddElement(pChild: new C4GUI::Label(FANPROJECTTEXT " " TRADEMARKTEXT,
74 GetClientRect().Wdt, GetClientRect().Hgt - trademarkFont.GetLineHeight() / 2, ARight, 0xffffffff, &trademarkFont));
75
76 // player selection shortcut - to be made optional
77 UpdateParticipants();
78 pParticipantsLbl->SetContextHandler(new C4GUI::CBContextHandler<C4StartupMainDlg>(this, &C4StartupMainDlg::OnPlayerSelContext));
79 // key bindings
80 C4CustomKey::CodeList keys;
81 keys.push_back(x: C4KeyCodeEx(K_DOWN)); keys.push_back(x: C4KeyCodeEx(K_RIGHT));
82 if (Config.Controls.GamepadGuiControl)
83 {
84 keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Down))); // right will be done by Dialog already
85 }
86 pKeyDown = new C4KeyBinding(keys, "StartupMainCtrlNext", KEYSCOPE_Gui,
87 new C4GUI::DlgKeyCBEx<C4StartupMainDlg, bool>(*this, false, &C4StartupMainDlg::KeyAdvanceFocus), C4CustomKey::PRIO_CtrlOverride);
88 keys.clear(); keys.push_back(x: C4KeyCodeEx(K_UP)); keys.push_back(x: C4KeyCodeEx(K_LEFT));
89 if (Config.Controls.GamepadGuiControl)
90 {
91 keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Up))); // left will be done by Dialog already
92 }
93 pKeyUp = new C4KeyBinding(keys, "StartupMainCtrlPrev", KEYSCOPE_Gui,
94 new C4GUI::DlgKeyCBEx<C4StartupMainDlg, bool>(*this, true, &C4StartupMainDlg::KeyAdvanceFocus), C4CustomKey::PRIO_CtrlOverride);
95 keys.clear(); keys.push_back(x: C4KeyCodeEx(K_RETURN));
96 pKeyEnter = new C4KeyBinding(keys, "StartupMainOK", KEYSCOPE_Gui,
97 new C4GUI::DlgKeyCB<C4StartupMainDlg>(*this, &C4StartupMainDlg::KeyEnterDown, &C4StartupMainDlg::KeyEnterUp), C4CustomKey::PRIO_CtrlOverride);
98 keys.clear(); keys.push_back(x: C4KeyCodeEx(K_F6));
99 pKeyEditor = new C4KeyBinding(keys, "StartupMainEditor", KEYSCOPE_Gui,
100 new C4GUI::DlgKeyCB<C4StartupMainDlg>(*this, &C4StartupMainDlg::SwitchToEditor), C4CustomKey::PRIO_CtrlOverride);
101}
102
103C4StartupMainDlg::~C4StartupMainDlg()
104{
105 delete pKeyEnter;
106 delete pKeyUp;
107 delete pKeyDown;
108 delete pKeyEditor;
109}
110
111void C4StartupMainDlg::DrawElement(C4FacetEx &cgo)
112{
113 // inherited
114 typedef C4GUI::FullscreenDialog Base;
115 Base::DrawElement(cgo);
116 // draw logo
117 C4FacetEx & = Game.GraphicsResource.fctLogo;
118 float fLogoZoom = 0.4f;
119 fctLogo.DrawX(sfcTarget: cgo.Surface, iX: rcBounds.Wdt * 30 / 31 - int32_t(fLogoZoom * fctLogo.Wdt), iY: rcBounds.Hgt / 21 - 5, iWdt: int32_t(fLogoZoom * fctLogo.Wdt), iHgt: int32_t(fLogoZoom * fctLogo.Hgt));
120 // draw version info
121 lpDDraw->TextOut(szText: LoadResStr(id: C4ResStrTableKey::IDS_DLG_VERSION, C4VERSION).c_str(), rFont&: C4GUI::GetRes()->TextFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: rcBounds.Wdt * 39 / 40, iTy: rcBounds.Hgt / 18 + int32_t(fLogoZoom * fctLogo.Hgt), dwFCol: 0xffffffff, byForm: ARight, fDoMarkup: true);
122}
123
124C4GUI::ContextMenu *C4StartupMainDlg::OnPlayerSelContext(C4GUI::Element *pBtn, int32_t iX, int32_t iY)
125{
126 // preliminary player selection via simple context menu
127 C4GUI::ContextMenu *pCtx = new C4GUI::ContextMenu();
128 pCtx->AddItem(szText: "Add", szToolTip: "Add participant", icoIcon: C4GUI::Ico_None, pMenuHandler: nullptr, pSubmenuHandler: new C4GUI::CBContextHandler<C4StartupMainDlg>(this, &C4StartupMainDlg::OnPlayerSelContextAdd));
129 pCtx->AddItem(szText: "Remove", szToolTip: "Remove participant", icoIcon: C4GUI::Ico_None, pMenuHandler: nullptr, pSubmenuHandler: new C4GUI::CBContextHandler<C4StartupMainDlg>(this, &C4StartupMainDlg::OnPlayerSelContextRemove));
130 return pCtx;
131}
132
133C4GUI::ContextMenu *C4StartupMainDlg::OnPlayerSelContextAdd(C4GUI::Element *pBtn, int32_t iX, int32_t iY)
134{
135 C4GUI::ContextMenu *pCtx = new C4GUI::ContextMenu();
136 const char *szFn;
137 const std::string searchPath{std::format(fmt: "{}{}", args: +Config.General.ExePath, args: +Config.General.PlayerPath)};
138 for (DirectoryIterator i(searchPath.c_str()); szFn = *i; i++)
139 {
140 szFn = Config.AtExeRelativePath(szFilename: szFn);
141 if (*GetFilename(path: szFn) == '.') continue;
142 if (!WildcardMatch(C4CFN_PlayerFiles, szFName2: GetFilename(path: szFn))) continue;
143 if (!SIsModule(szList: Config.General.Participants, szString: szFn, ipIndex: nullptr, fCaseSensitive: false))
144 pCtx->AddItem(szText: TextEncodingConverter.SystemToClonk(input: GetFilenameOnly(strFilename: szFn)).c_str(), szToolTip: "Let this player join in next game", icoIcon: C4GUI::Ico_Player,
145 pMenuHandler: new C4GUI::CBMenuHandlerEx<C4StartupMainDlg, StdStrBuf>(this, &C4StartupMainDlg::OnPlayerSelContextAddPlr, StdStrBuf(szFn)), pSubmenuHandler: nullptr);
146 }
147 return pCtx;
148}
149
150C4GUI::ContextMenu *C4StartupMainDlg::OnPlayerSelContextRemove(C4GUI::Element *pBtn, int32_t iX, int32_t iY)
151{
152 C4GUI::ContextMenu *pCtx = new C4GUI::ContextMenu();
153 char szPlayer[1024 + 1];
154 for (int i = 0; SCopySegment(fstr: Config.General.Participants, segn: i, tstr: szPlayer, sepa: ';', iMaxL: 1024, fSkipWhitespace: true); i++)
155 if (*szPlayer)
156 pCtx->AddItem(szText: GetFilenameOnly(strFilename: szPlayer), szToolTip: "Remove this player from participation list", icoIcon: C4GUI::Ico_Player, pMenuHandler: new C4GUI::CBMenuHandlerEx<C4StartupMainDlg, int>(this, &C4StartupMainDlg::OnPlayerSelContextRemovePlr, i), pSubmenuHandler: nullptr);
157 return pCtx;
158}
159
160void C4StartupMainDlg::OnPlayerSelContextAddPlr(C4GUI::Element *pTarget, const StdStrBuf &rsFilename)
161{
162 SAddModule(szList: Config.General.Participants, szModule: rsFilename.getData());
163 UpdateParticipants();
164}
165
166void C4StartupMainDlg::OnPlayerSelContextRemovePlr(C4GUI::Element *pTarget, const int &iIndex)
167{
168 char szPlayer[1024 + 1];
169 if (SCopySegment(fstr: Config.General.Participants, segn: iIndex, tstr: szPlayer, sepa: ';', iMaxL: 1024, fSkipWhitespace: true))
170 SRemoveModule(szList: Config.General.Participants, szModule: szPlayer, fCaseSensitive: false);
171 UpdateParticipants();
172}
173
174void C4StartupMainDlg::UpdateParticipants()
175{
176 // First validate all participants (files must exist)
177 StdStrBuf strPlayers, strPlayer; strPlayer.SetLength(1024 + 1);
178 strPlayers.Copy(pnData: Config.General.Participants); *Config.General.Participants = 0;
179 for (int i = 0; SCopySegment(fstr: strPlayers.getData(), segn: i, tstr: strPlayer.getMData(), sepa: ';', iMaxL: 1024, fSkipWhitespace: true); i++)
180 {
181 const char *szPlayer = strPlayer.getData();
182 if (!szPlayer || !*szPlayer) continue;
183 if (!FileExists(szFileName: szPlayer)) continue;
184 if (!SEqualNoCase(szStr1: GetExtension(fname: szPlayer), szStr2: "c4p")) continue; // additional sanity check to clear strange exe-path-only entries in player list?
185 SAddModule(szList: Config.General.Participants, szModule: szPlayer);
186 }
187 // Draw selected players - we are currently displaying the players stored in Config.General.Participants.
188 // Existence of the player files is not validated and player filenames are displayed directly
189 // (names are not loaded from the player core).
190 strPlayers = LoadResStr(id: C4ResStrTableKey::IDS_DESC_PLRS);
191 if (!Config.General.Participants[0])
192 strPlayers.Append(pnData: LoadResStr(id: C4ResStrTableKey::IDS_DLG_NOPLAYERSSELECTED));
193 else
194 for (int i = 0; SCopySegment(fstr: Config.General.Participants, segn: i, tstr: strPlayer.getMData(), sepa: ';', iMaxL: 1024, fSkipWhitespace: true); i++)
195 {
196 if (i > 0) strPlayers.Append(pnData: ", ");
197 strPlayers.Append(pnData: TextEncodingConverter.SystemToClonk(input: GetFilenameOnly(strFilename: strPlayer.getData())).c_str());
198 }
199 pParticipantsLbl->SetText(szText: strPlayers.getData());
200}
201
202void C4StartupMainDlg::OnClosed(bool fOK)
203{
204 // if dlg got aborted (by user), quit startup
205 // if it got closed with OK, the user pressed one of the buttons and dialog switching has been done already
206 if (!fOK) C4Startup::Get()->Exit();
207}
208
209void C4StartupMainDlg::OnStartBtn(C4GUI::Control *btn)
210{
211 // advance to scenario selection screen
212 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_ScenSel);
213}
214
215void C4StartupMainDlg::OnPlayerSelectionBtn(C4GUI::Control *btn)
216{
217 // advance to player selection screen
218 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_PlrSel);
219}
220
221void C4StartupMainDlg::OnNetJoinBtn(C4GUI::Control *btn)
222{
223 // advanced net join and host dlg!
224 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_NetJoin);
225}
226
227void C4StartupMainDlg::OnOptionsBtn(C4GUI::Control *btn)
228{
229 // advance to options screen
230 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_Options);
231}
232
233void C4StartupMainDlg::OnAboutBtn(C4GUI::Control *btn)
234{
235 // advance to about screen
236 C4Startup::Get()->SwitchDialog(eToDlg: C4Startup::SDID_About);
237}
238
239void C4StartupMainDlg::OnExitBtn(C4GUI::Control *btn)
240{
241 // callback: exit button pressed
242 C4Startup::Get()->Exit();
243}
244
245bool C4StartupMainDlg::KeyEnterDown()
246{
247 // just execute selected button: Re-Send as space
248 return Game.DoKeyboardInput(K_SPACE, eEventType: KEYEV_Down, fAlt: false, fCtrl: false, fShift: false, fRepeated: false, pForDialog: this);
249}
250
251bool C4StartupMainDlg::KeyEnterUp()
252{
253 // just execute selected button: Re-Send as space
254 return Game.DoKeyboardInput(K_SPACE, eEventType: KEYEV_Up, fAlt: false, fCtrl: false, fShift: false, fRepeated: false, pForDialog: this);
255}
256
257void C4StartupMainDlg::OnShown()
258{
259 // Incoming update
260 if (Application.IncomingUpdate)
261 {
262 C4UpdateDlg::ApplyUpdate(strUpdateFile: Application.IncomingUpdate.getData(), fDeleteUpdate: false, pScreen: GetScreen());
263 Application.IncomingUpdate.Clear();
264 }
265 // Manual update by command line or url
266 if (Application.CheckForUpdates)
267 {
268 C4UpdateDlg::CheckForUpdates(pScreen: GetScreen(), fAutomatic: false);
269 Application.CheckForUpdates = false;
270 }
271 // Automatic update
272 else
273 {
274 if (Config.Network.AutomaticUpdate)
275 C4UpdateDlg::CheckForUpdates(pScreen: GetScreen(), fAutomatic: true);
276 }
277
278 // first start evaluation
279 if (Config.General.FirstStart)
280 {
281 Config.General.FirstStart = false;
282 }
283 // first thing that's needed is a new player, if there's none - independent of first start
284 bool fHasPlayer = false;
285 const char *szFn;
286 const std::string searchPath{std::format(fmt: "{}{}", args: +Config.General.ExePath, args: +Config.General.PlayerPath)};
287 for (DirectoryIterator i(searchPath.c_str()); szFn = *i; i++)
288 {
289 szFn = Config.AtExeRelativePath(szFilename: szFn);
290 if (*GetFilename(path: szFn) == '.') continue; // ignore ".", ".." and private files (".*")
291 if (!WildcardMatch(C4CFN_PlayerFiles, szFName2: GetFilename(path: szFn))) continue;
292 fHasPlayer = true;
293 break;
294 }
295 if (!fHasPlayer)
296 {
297 // no player created yet: Create one
298 C4GUI::Dialog *pDlg;
299 GetScreen()->ShowModalDlg(pDlg: pDlg = new C4StartupPlrPropertiesDlg(nullptr, nullptr), fDestruct: true);
300 }
301 // make sure participants are updated after switching back from player selection
302 UpdateParticipants();
303
304 // First show
305 if (fFirstShown)
306 {
307 // Set the focus to the start button (we might still not have the focus after the update-check sometimes... :/)
308 SetFocus(pCtrl: pStartButton, fByMouse: false);
309 }
310 fFirstShown = false;
311}
312
313bool C4StartupMainDlg::SwitchToEditor()
314{
315#ifdef _WIN32
316 // No editor executable available
317 if (!FileExists(Config.AtExePath(C4CFN_Editor))) return false;
318 // Flag editor launch
319 Application.launchEditor = true;
320 // Quit
321 C4Startup::Get()->Exit();
322#endif
323
324 return true;
325}
326