Add: support for emscripten (play-OpenTTD-in-the-browser)

Emscripten compiles to WASM, which can be loaded via
HTML / JavaScript. This allows you to play OpenTTD inside a
browser.

Co-authored-by: milek7 <me@milek7.pl>
This commit is contained in:
Patric Stout
2020-12-05 21:57:47 +01:00
committed by Patric Stout
parent 2da07f7615
commit d15dc9f40f
25 changed files with 844 additions and 12 deletions

View File

@@ -26,7 +26,6 @@
#include "safeguards.h"
static const int ASCII_LETTERSTART = 32; ///< First printable ASCII letter.
static const int MAX_FONT_SIZE = 72; ///< Maximum font size.
/** Default heights for the different sizes of fonts. */
static const int _default_font_height[FS_END] = {10, 6, 18, 10};
@@ -202,6 +201,8 @@ bool SpriteFontCache::GetDrawGlyphShadow()
FreeTypeSettings _freetype;
static const int MAX_FONT_SIZE = 72; ///< Maximum font size.
static const byte FACE_COLOUR = 1;
static const byte SHADOW_COLOUR = 2;

View File

@@ -162,7 +162,9 @@ struct DrawPixelInfo {
union Colour {
uint32 data; ///< Conversion of the channel information to a 32 bit number.
struct {
#if TTD_ENDIAN == TTD_BIG_ENDIAN
#if defined(__EMSCRIPTEN__)
uint8 r, g, b, a; ///< colour channels as used in browsers
#elif TTD_ENDIAN == TTD_BIG_ENDIAN
uint8 a, r, g, b; ///< colour channels in BE order
#else
uint8 b, g, r, a; ///< colour channels in LE order
@@ -177,7 +179,9 @@ union Colour {
* @param a The channel for the alpha/transparency.
*/
Colour(uint8 r, uint8 g, uint8 b, uint8 a = 0xFF) :
#if TTD_ENDIAN == TTD_BIG_ENDIAN
#if defined(__EMSCRIPTEN__)
r(r), g(g), b(b), a(a)
#elif TTD_ENDIAN == TTD_BIG_ENDIAN
a(a), r(r), g(g), b(b)
#else
b(b), g(g), r(r), a(a)

View File

@@ -13,6 +13,9 @@
#include "string_func.h"
#include "fileio_func.h"
#include <fstream>
#ifdef __EMSCRIPTEN__
# include <emscripten.h>
#endif
#if (defined(_POSIX_C_SOURCE) && _POSIX_C_SOURCE >= 199309L) || (defined(_XOPEN_SOURCE) && _XOPEN_SOURCE >= 500)
# include <unistd.h>
@@ -115,6 +118,10 @@ bool IniFile::SaveToDisk(const char *filename)
}
#endif
#ifdef __EMSCRIPTEN__
EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
#endif
return true;
}

View File

@@ -299,7 +299,15 @@ static SOCKET ConnectLoopProc(addrinfo *runp)
if (!SetNoDelay(sock)) DEBUG(net, 1, "[%s] setting TCP_NODELAY failed", type);
if (connect(sock, runp->ai_addr, (int)runp->ai_addrlen) != 0) {
int err = connect(sock, runp->ai_addr, (int)runp->ai_addrlen);
#ifdef __EMSCRIPTEN__
/* Emscripten is asynchronous, and as such a connect() is still in
* progress by the time the call returns. */
if (err != 0 && errno != EINPROGRESS)
#else
if (err != 0)
#endif
{
DEBUG(net, 1, "[%s] could not connect %s socket: %s", type, family, strerror(errno));
closesocket(sock);
return INVALID_SOCKET;

View File

@@ -83,6 +83,16 @@ typedef unsigned long in_addr_t;
# include <errno.h>
# include <sys/time.h>
# include <netdb.h>
# if defined(__EMSCRIPTEN__)
/* Emscripten doesn't support AI_ADDRCONFIG and errors out on it. */
# undef AI_ADDRCONFIG
# define AI_ADDRCONFIG 0
/* Emscripten says it supports FD_SETSIZE fds, but it really only supports 64.
* https://github.com/emscripten-core/emscripten/issues/1711 */
# undef FD_SETSIZE
# define FD_SETSIZE 64
# endif
#endif /* UNIX */
/* OS/2 stuff */
@@ -148,12 +158,16 @@ typedef unsigned long in_addr_t;
*/
static inline bool SetNonBlocking(SOCKET d)
{
#ifdef _WIN32
u_long nonblocking = 1;
#ifdef __EMSCRIPTEN__
return true;
#else
# ifdef _WIN32
u_long nonblocking = 1;
# else
int nonblocking = 1;
#endif
# endif
return ioctlsocket(d, FIONBIO, &nonblocking) == 0;
#endif
}
/**
@@ -163,10 +177,14 @@ static inline bool SetNonBlocking(SOCKET d)
*/
static inline bool SetNoDelay(SOCKET d)
{
#ifdef __EMSCRIPTEN__
return true;
#else
/* XXX should this be done at all? */
int b = 1;
/* The (const char*) cast is needed for windows */
return setsockopt(d, IPPROTO_TCP, TCP_NODELAY, (const char*)&b, sizeof(b)) == 0;
#endif
}
/* Make sure these structures have the size we expect them to be */

View File

@@ -1154,3 +1154,14 @@ bool IsNetworkCompatibleVersion(const char *other)
const char *hash2 = ExtractNetworkRevisionHash(other);
return hash1 && hash2 && (strncmp(hash1, hash2, GITHASH_SUFFIX_LEN) == 0);
}
#ifdef __EMSCRIPTEN__
extern "C" {
void CDECL em_openttd_add_server(const char *host, int port)
{
NetworkUDPQueryServer(NetworkAddress(host, port), true);
}
}
#endif

View File

@@ -23,6 +23,10 @@
#include <zlib.h>
#endif
#ifdef __EMSCRIPTEN__
# include <emscripten.h>
#endif
#include "../safeguards.h"
extern bool HasScenario(const ContentInfo *ci, bool md5sum);
@@ -289,6 +293,13 @@ void ClientNetworkContentSocketHandler::DownloadSelectedContent(uint &files, uin
{
bytes = 0;
#ifdef __EMSCRIPTEN__
/* Emscripten is loaded via an HTTPS connection. As such, it is very
* difficult to make HTTP connections. So always use the TCP method of
* downloading content. */
fallback = true;
#endif
ContentIDList content;
for (const ContentInfo *ci : this->infos) {
if (!ci->IsSelected() || ci->state == ContentInfo::ALREADY_HERE) continue;
@@ -535,6 +546,10 @@ void ClientNetworkContentSocketHandler::AfterDownload()
unlink(GetFullFilename(this->curInfo, false));
}
#ifdef __EMSCRIPTEN__
EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
#endif
this->OnDownloadComplete(this->curInfo->id);
} else {
ShowErrorMessage(STR_CONTENT_ERROR_COULD_NOT_EXTRACT, INVALID_STRING_ID, WL_ERROR);

View File

@@ -39,6 +39,9 @@
#include "../safeguards.h"
#ifdef __EMSCRIPTEN__
# include <emscripten.h>
#endif
static void ShowNetworkStartServerWindow();
static void ShowNetworkLobbyWindow(NetworkGameList *ngl);
@@ -475,6 +478,14 @@ public:
this->filter_editbox.cancel_button = QueryString::ACTION_CLEAR;
this->SetFocusedWidget(WID_NG_FILTER);
/* As the master-server doesn't support "websocket" servers yet, we
* let "os/emscripten/pre.js" hardcode a list of servers people can
* join. This means the serverlist is curated for now, but it is the
* best we can offer. */
#ifdef __EMSCRIPTEN__
EM_ASM(if (window["openttd_server_list"]) openttd_server_list());
#endif
this->last_joined = NetworkGameListAddItem(NetworkAddress(_settings_client.network.last_host, _settings_client.network.last_port));
this->server = this->last_joined;
if (this->last_joined != nullptr) NetworkUDPQueryServer(this->last_joined->address);
@@ -615,6 +626,12 @@ public:
this->GetWidget<NWidgetStacked>(WID_NG_NEWGRF_SEL)->SetDisplayedPlane(sel == nullptr || !sel->online || sel->info.grfconfig == nullptr);
this->GetWidget<NWidgetStacked>(WID_NG_NEWGRF_MISSING_SEL)->SetDisplayedPlane(sel == nullptr || !sel->online || sel->info.grfconfig == nullptr || !sel->info.version_compatible || sel->info.compatible);
#ifdef __EMSCRIPTEN__
this->SetWidgetDisabledState(WID_NG_FIND, true);
this->SetWidgetDisabledState(WID_NG_ADD, true);
this->SetWidgetDisabledState(WID_NG_START, true);
#endif
this->DrawWidgets();
}

View File

@@ -73,6 +73,11 @@
#include "safeguards.h"
#ifdef __EMSCRIPTEN__
# include <emscripten.h>
# include <emscripten/html5.h>
#endif
void CallLandscapeTick();
void IncreaseDate();
void DoPaletteAnimations();
@@ -104,6 +109,15 @@ void CDECL usererror(const char *s, ...)
ShowOSErrorBox(buf, false);
if (VideoDriver::GetInstance() != nullptr) VideoDriver::GetInstance()->Stop();
#ifdef __EMSCRIPTEN__
emscripten_exit_pointerlock();
/* In effect, the game ends here. As emscripten_set_main_loop() caused
* the stack to be unwound, the code after MainLoop() in
* openttd_main() is never executed. */
EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
EM_ASM(if (window["openttd_abort"]) openttd_abort());
#endif
exit(1);
}

View File

@@ -45,6 +45,9 @@
#include "../error.h"
#include <atomic>
#include <string>
#ifdef __EMSCRIPTEN__
# include <emscripten.h>
#endif
#include "table/strings.h"
@@ -2495,6 +2498,10 @@ static void SaveFileDone()
InvalidateWindowData(WC_STATUS_BAR, 0, SBI_SAVELOAD_FINISH);
_sl.saveinprogress = false;
#ifdef __EMSCRIPTEN__
EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
#endif
}
/** Set the error message from outside of the actual loading/saving of the game (AfterLoadGame and friends) */

View File

@@ -27,6 +27,10 @@
#include <mutex>
#include <condition_variable>
#include <algorithm>
#ifdef __EMSCRIPTEN__
# include <emscripten.h>
# include <emscripten/html5.h>
#endif
#include "../safeguards.h"
@@ -673,7 +677,19 @@ void VideoDriver_SDL::LoopOnce()
InteractiveRandom(); // randomness
while (PollEvent() == -1) {}
if (_exit_game) return;
if (_exit_game) {
#ifdef __EMSCRIPTEN__
/* Emscripten is event-driven, and as such the main loop is inside
* the browser. So if _exit_game goes true, the main loop ends (the
* cancel call), but we still have to call the cleanup that is
* normally done at the end of the main loop for non-Emscripten.
* After that, Emscripten just halts, and the HTML shows a nice
* "bye, see you next time" message. */
emscripten_cancel_main_loop();
MainLoopCleanup();
#endif
return;
}
mod = SDL_GetModState();
keys = SDL_GetKeyboardState(&numkeys);
@@ -722,9 +738,17 @@ void VideoDriver_SDL::LoopOnce()
_local_palette = _cur_palette;
} else {
/* Release the thread while sleeping */
if (_draw_mutex != nullptr) draw_lock.unlock();
CSleep(1);
if (_draw_mutex != nullptr) draw_lock.lock();
if (_draw_mutex != nullptr) {
draw_lock.unlock();
CSleep(1);
draw_lock.lock();
} else {
/* Emscripten is running an event-based mainloop; there is already some
* downtime between each iteration, so no need to sleep. */
#ifndef __EMSCRIPTEN__
CSleep(1);
#endif
}
NetworkDrawChatMessage();
DrawMouseCursor();
@@ -778,6 +802,10 @@ void VideoDriver_SDL::MainLoop()
DEBUG(driver, 1, "SDL2: using %sthreads", _draw_threaded ? "" : "no ");
#ifdef __EMSCRIPTEN__
/* Run the main loop event-driven, based on RequestAnimationFrame. */
emscripten_set_main_loop_arg(&this->EmscriptenLoop, this, 0, 1);
#else
while (!_exit_game) {
LoopOnce();
}
@@ -803,6 +831,15 @@ void VideoDriver_SDL::MainLoopCleanup()
_draw_mutex = nullptr;
_draw_signal = nullptr;
}
#ifdef __EMSCRIPTEN__
emscripten_exit_pointerlock();
/* In effect, the game ends here. As emscripten_set_main_loop() caused
* the stack to be unwound, the code after MainLoop() in
* openttd_main() is never executed. */
EM_ASM(if (window["openttd_syncfs"]) openttd_syncfs());
EM_ASM(if (window["openttd_exit"]) openttd_exit());
#endif
}
bool VideoDriver_SDL::ChangeResolution(int w, int h)

View File

@@ -46,6 +46,11 @@ private:
void MainLoopCleanup();
bool CreateMainSurface(uint w, uint h, bool resize);
#ifdef __EMSCRIPTEN__
/* Convert a constant pointer back to a non-constant pointer to a member function. */
static void EmscriptenLoop(void *self) { ((VideoDriver_SDL *)self)->LoopOnce(); }
#endif
/**
* This is true to indicate that keyboard input is in text input mode, and SDL_TEXTINPUT events are enabled.
*/