2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file bootstrap_gui.cpp Barely used user interface for bootstrapping OpenTTD, i.e. downloading the required content. */
11 #include "base_media_base.h"
12 #include "blitter/factory.hpp"
13 #include "error_func.h"
15 #if defined(WITH_FREETYPE) || defined(WITH_UNISCRIBE) || defined(WITH_COCOA)
17 #include "core/geometry_func.hpp"
19 #include "fontcache.h"
21 #include "network/network.h"
22 #include "network/network_content_gui.h"
24 #include "strings_func.h"
25 #include "video/video_driver.hpp"
26 #include "window_func.h"
28 #include "widgets/bootstrap_widget.h"
30 #include "table/strings.h"
32 #include "safeguards.h"
34 /** Widgets for the background window to prevent smearing. */
35 static constexpr NWidgetPart _background_widgets
[] = {
36 NWidget(WWT_PANEL
, COLOUR_DARK_BLUE
, WID_BB_BACKGROUND
), SetResize(1, 1),
41 * Window description for the background window to prevent smearing.
43 static WindowDesc
_background_desc(
44 WDP_MANUAL
, nullptr, 0, 0,
45 WC_BOOTSTRAP
, WC_NONE
,
50 /** The background for the game. */
51 class BootstrapBackground
: public Window
{
53 BootstrapBackground() : Window(_background_desc
)
56 CLRBITS(this->flags
, WF_WHITE_BORDER
);
57 ResizeWindow(this, _screen
.width
, _screen
.height
);
60 void DrawWidget(const Rect
&r
, WidgetID
) const override
62 GfxFillRect(r
.left
, r
.top
, r
.right
, r
.bottom
, 4, FILLRECT_OPAQUE
);
63 GfxFillRect(r
.left
, r
.top
, r
.right
, r
.bottom
, 0, FILLRECT_CHECKER
);
67 /** Nested widgets for the error window. */
68 static constexpr NWidgetPart _nested_bootstrap_errmsg_widgets
[] = {
69 NWidget(NWID_HORIZONTAL
),
70 NWidget(WWT_CAPTION
, COLOUR_GREY
, WID_BEM_CAPTION
), SetDataTip(STR_MISSING_GRAPHICS_ERROR_TITLE
, STR_NULL
),
72 NWidget(WWT_PANEL
, COLOUR_GREY
, WID_BEM_MESSAGE
), EndContainer(),
73 NWidget(NWID_HORIZONTAL
),
74 NWidget(WWT_PUSHTXTBTN
, COLOUR_GREY
, WID_BEM_QUIT
), SetDataTip(STR_MISSING_GRAPHICS_ERROR_QUIT
, STR_NULL
), SetFill(1, 0),
78 /** Window description for the error window. */
79 static WindowDesc
_bootstrap_errmsg_desc(
80 WDP_CENTER
, nullptr, 0, 0,
81 WC_BOOTSTRAP
, WC_NONE
,
82 WDF_MODAL
| WDF_NO_CLOSE
,
83 _nested_bootstrap_errmsg_widgets
86 /** The window for a failed bootstrap. */
87 class BootstrapErrorWindow
: public Window
{
89 BootstrapErrorWindow() : Window(_bootstrap_errmsg_desc
)
94 void Close([[maybe_unused
]] int data
= 0) override
97 this->Window::Close();
100 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
102 if (widget
== WID_BEM_MESSAGE
) {
103 size
= GetStringBoundingBox(STR_MISSING_GRAPHICS_ERROR
);
104 size
.width
+= WidgetDimensions::scaled
.frametext
.Horizontal();
105 size
.height
+= WidgetDimensions::scaled
.frametext
.Vertical();
109 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
111 if (widget
== WID_BEM_MESSAGE
) {
112 DrawStringMultiLine(r
.Shrink(WidgetDimensions::scaled
.frametext
), STR_MISSING_GRAPHICS_ERROR
, TC_FROMSTRING
, SA_CENTER
);
116 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
118 if (widget
== WID_BEM_QUIT
) {
124 /** Nested widgets for the download window. */
125 static constexpr NWidgetPart _nested_bootstrap_download_status_window_widgets
[] = {
126 NWidget(WWT_CAPTION
, COLOUR_GREY
), SetDataTip(STR_CONTENT_DOWNLOAD_TITLE
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
127 NWidget(WWT_PANEL
, COLOUR_GREY
),
128 NWidget(NWID_VERTICAL
), SetPIP(0, WidgetDimensions::unscaled
.vsep_wide
, 0), SetPadding(WidgetDimensions::unscaled
.modalpopup
),
129 NWidget(WWT_EMPTY
, INVALID_COLOUR
, WID_NCDS_PROGRESS_BAR
), SetFill(1, 0),
130 NWidget(WWT_EMPTY
, INVALID_COLOUR
, WID_NCDS_PROGRESS_TEXT
), SetFill(1, 0), SetMinimalSize(350, 0),
135 /** Window description for the download window */
136 static WindowDesc
_bootstrap_download_status_window_desc(
137 WDP_CENTER
, nullptr, 0, 0,
138 WC_NETWORK_STATUS_WINDOW
, WC_NONE
,
139 WDF_MODAL
| WDF_NO_CLOSE
,
140 _nested_bootstrap_download_status_window_widgets
144 /** Window for showing the download status of content */
145 struct BootstrapContentDownloadStatusWindow
: public BaseNetworkContentDownloadStatusWindow
{
147 /** Simple call the constructor of the superclass. */
148 BootstrapContentDownloadStatusWindow() : BaseNetworkContentDownloadStatusWindow(_bootstrap_download_status_window_desc
)
152 void Close([[maybe_unused
]] int data
= 0) override
154 /* If we are not set to exit the game, it means the bootstrap failed. */
156 new BootstrapErrorWindow();
158 this->BaseNetworkContentDownloadStatusWindow::Close();
161 void OnDownloadComplete(ContentID
) override
163 /* We have completed downloading. We can trigger finding the right set now. */
164 BaseGraphics::FindSets();
166 /* And continue going into the menu. */
167 _game_mode
= GM_MENU
;
169 /* _exit_game is used to break out of the outer video driver's MainLoop. */
175 /** The widgets for the query. It has no close box as that sprite does not exist yet. */
176 static constexpr NWidgetPart _bootstrap_query_widgets
[] = {
177 NWidget(NWID_HORIZONTAL
),
178 NWidget(WWT_CAPTION
, COLOUR_GREY
), SetDataTip(STR_MISSING_GRAPHICS_SET_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
180 NWidget(WWT_PANEL
, COLOUR_GREY
, WID_BAFD_QUESTION
), EndContainer(),
181 NWidget(NWID_HORIZONTAL
),
182 NWidget(WWT_PUSHTXTBTN
, COLOUR_GREY
, WID_BAFD_YES
), SetDataTip(STR_MISSING_GRAPHICS_YES_DOWNLOAD
, STR_NULL
),
183 NWidget(WWT_PUSHTXTBTN
, COLOUR_GREY
, WID_BAFD_NO
), SetDataTip(STR_MISSING_GRAPHICS_NO_QUIT
, STR_NULL
),
187 /** The window description for the query. */
188 static WindowDesc
_bootstrap_query_desc(
189 WDP_CENTER
, nullptr, 0, 0,
190 WC_CONFIRM_POPUP_QUERY
, WC_NONE
,
192 _bootstrap_query_widgets
195 /** The window for the query. It can't use the generic query window as that uses sprites that don't exist yet. */
196 class BootstrapAskForDownloadWindow
: public Window
, ContentCallback
{
197 Dimension button_size
; ///< The dimension of the button
200 /** Start listening to the content client events. */
201 BootstrapAskForDownloadWindow() : Window(_bootstrap_query_desc
)
203 this->InitNested(WN_CONFIRM_POPUP_QUERY_BOOTSTRAP
);
204 _network_content_client
.AddCallback(this);
207 /** Stop listening to the content client events. */
208 void Close([[maybe_unused
]] int data
= 0) override
210 _network_content_client
.RemoveCallback(this);
211 this->Window::Close();
214 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
216 /* We cache the button size. This is safe as no reinit can happen here. */
217 if (this->button_size
.width
== 0) {
218 this->button_size
= maxdim(GetStringBoundingBox(STR_MISSING_GRAPHICS_YES_DOWNLOAD
), GetStringBoundingBox(STR_MISSING_GRAPHICS_NO_QUIT
));
219 this->button_size
.width
+= WidgetDimensions::scaled
.frametext
.Horizontal();
220 this->button_size
.height
+= WidgetDimensions::scaled
.frametext
.Vertical();
224 case WID_BAFD_QUESTION
:
225 /* The question is twice as wide as the buttons, and determine the height based on the width. */
226 size
.width
= this->button_size
.width
* 2;
227 size
.height
= GetStringHeight(STR_MISSING_GRAPHICS_SET_MESSAGE
, size
.width
- WidgetDimensions::scaled
.frametext
.Horizontal()) + WidgetDimensions::scaled
.frametext
.Vertical();
232 size
= this->button_size
;
237 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
239 if (widget
!= WID_BAFD_QUESTION
) return;
241 DrawStringMultiLine(r
.Shrink(WidgetDimensions::scaled
.frametext
), STR_MISSING_GRAPHICS_SET_MESSAGE
, TC_FROMSTRING
, SA_CENTER
);
244 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
248 /* We got permission to connect! Yay! */
249 _network_content_client
.Connect();
261 void OnConnect(bool success
) override
264 UserError("Failed to connect to content server. Please acquire a graphics set for OpenTTD. See section 1.4 of README.md.");
265 /* _exit_game is used to break out of the outer video driver's MainLoop. */
271 /* Once connected, request the metadata. */
272 _network_content_client
.RequestContentList(CONTENT_TYPE_BASE_GRAPHICS
);
275 void OnReceiveContentInfo(const ContentInfo
*ci
) override
277 /* And once the meta data is received, start downloading it. */
278 _network_content_client
.Select(ci
->id
);
279 new BootstrapContentDownloadStatusWindow();
284 #endif /* defined(WITH_FREETYPE) */
286 #if defined(__EMSCRIPTEN__)
287 # include <emscripten.h>
288 # include "network/network.h"
289 # include "network/network_content.h"
290 # include "openttd.h"
291 # include "video/video_driver.hpp"
293 class BootstrapEmscripten
: public ContentCallback
{
294 bool downloading
= false;
295 uint total_files
= 0;
296 uint total_bytes
= 0;
297 uint downloaded_bytes
= 0;
300 BootstrapEmscripten()
302 _network_content_client
.AddCallback(this);
303 _network_content_client
.Connect();
306 ~BootstrapEmscripten()
308 _network_content_client
.RemoveCallback(this);
311 void OnConnect(bool success
) override
314 EM_ASM({ if (window
["openttd_bootstrap_failed"]) openttd_bootstrap_failed(); });
318 /* Once connected, request the metadata. */
319 _network_content_client
.RequestContentList(CONTENT_TYPE_BASE_GRAPHICS
);
322 void OnReceiveContentInfo(const ContentInfo
*ci
) override
324 if (this->downloading
) return;
326 /* And once the metadata is received, start downloading it. */
327 _network_content_client
.Select(ci
->id
);
328 _network_content_client
.DownloadSelectedContent(this->total_files
, this->total_bytes
);
329 this->downloading
= true;
331 EM_ASM({ if (window
["openttd_bootstrap"]) openttd_bootstrap($
0, $
1); }, this->downloaded_bytes
, this->total_bytes
);
334 void OnDownloadProgress(const ContentInfo
*, int bytes
) override
336 /* A negative value means we are resetting; for example, when retrying or using a fallback. */
338 this->downloaded_bytes
= 0;
340 this->downloaded_bytes
+= bytes
;
343 EM_ASM({ if (window
["openttd_bootstrap"]) openttd_bootstrap($
0, $
1); }, this->downloaded_bytes
, this->total_bytes
);
346 void OnDownloadComplete(ContentID
) override
348 /* _exit_game is used to break out of the outer video driver's MainLoop. */
354 #endif /* __EMSCRIPTEN__ */
357 * Handle all procedures for bootstrapping OpenTTD without a base graphics set.
358 * This requires all kinds of trickery that is needed to avoid the use of
359 * sprites from the base graphics set which are pretty interwoven.
360 * @return True if a base set exists, otherwise false.
362 bool HandleBootstrap()
364 if (BaseGraphics::GetUsedSet() != nullptr) return true;
366 /* No user interface, bail out with an error. */
367 if (BlitterFactory::GetCurrentBlitter()->GetScreenDepth() == 0) goto failure
;
369 /* If there is no network or no non-sprite font, then there is nothing we can do. Go straight to failure. */
370 #if defined(__EMSCRIPTEN__) || (defined(_WIN32) && defined(WITH_UNISCRIBE)) || (defined(WITH_FREETYPE) && (defined(WITH_FONTCONFIG) || defined(__APPLE__))) || defined(WITH_COCOA)
371 if (!_network_available
) goto failure
;
373 /* First tell the game we're bootstrapping. */
374 _game_mode
= GM_BOOTSTRAP
;
376 #if defined(__EMSCRIPTEN__)
377 new BootstrapEmscripten();
379 /* Initialise the font cache. */
380 InitializeUnicodeGlyphMap();
381 /* Next "force" finding a suitable non-sprite font as the local font is missing. */
382 CheckForMissingGlyphs(false);
384 /* Initialise the palette. The biggest step is 'faking' some recolour sprites.
385 * This way the mauve and gray colours work and we can show the user interface. */
387 static const int offsets
[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x80, 0, 0, 0, 0x04, 0x08 };
388 for (Colours i
= COLOUR_BEGIN
; i
!= COLOUR_END
; i
++) {
389 for (ColourShade j
= SHADE_BEGIN
; j
< SHADE_END
; j
++) {
390 SetColourGradient(i
, j
, offsets
[i
] + j
);
394 /* Finally ask the question. */
395 new BootstrapBackground();
396 new BootstrapAskForDownloadWindow();
397 #endif /* __EMSCRIPTEN__ */
399 /* Process the user events. */
400 VideoDriver::GetInstance()->MainLoop();
402 /* _exit_game is used to get out of the video driver's main loop.
403 * In case GM_BOOTSTRAP is still set we did not exit it via the
404 * "download complete" event, so it was a manual exit. Obey it. */
405 _exit_game
= _game_mode
== GM_BOOTSTRAP
;
406 if (_exit_game
) return false;
408 /* Try to probe the graphics. Should work this time. */
409 if (!BaseGraphics::SetSet({})) goto failure
;
411 /* Finally we can continue heading for the menu. */
412 _game_mode
= GM_MENU
;
416 /* Failure to get enough working to get a graphics set. */
418 UserError("Failed to find a graphics set. Please acquire a graphics set for OpenTTD. See section 1.4 of README.md.");