Endless Sky 0.11.3 for the web (fredygames.com), applied by build-endless-sky.sh (patch -p1).
GPL-3.0-or-later, like the game. Everything is under __EMSCRIPTEN__; native builds are unchanged.
Taken from the endless-web port by Thomas Ballinger (github.com/thomasballinger/endless-web,
GPL-3.0, commits 0d8dea4324f6 "Emscripten build support" and 8678fa53da61 "Single-threaded mode
and disable music for Emscripten", based on earlier work by Michal Janiszewski and Thomas Ballinger):
- source/TaskQueue.cpp: no worker threads; tasks run at once on the main thread.
- source/audio/Audio.cpp, source/audio/Music.cpp: sounds load on the main thread; no music
(the MP3/FLAC decoders would need their own thread).
- source/Files.cpp: no zip support (no minizip; plugins are not available in the browser).
- source/GameWindow.cpp: no minimum-resolution warning; errors also go to stdout.
Ours (fredygames.com):
- source/image/ImageBuffer.cpp: read WebP images with libwebp (the build converts the PNG and JPG
images to WebP to keep the download small); no libjpeg or libavif.
- source/GameWindow.cpp: the window starts at the size of the page and never in full screen.
- source/Screen.cpp: when the page's player is smaller than the UI needs at 100%, zoom out
below 100% so that the menus fit.
- source/FrameTimer.cpp: wait for the next frame with emscripten_sleep (returns to the browser).
- source/PreferencesPanel.cpp, source/PlayerInfo.cpp: write the preferences when leaving the
preferences panel and with every save, since browser tabs are usually closed, not quit.
- source/Files.cpp: "open folder" does nothing (the folders live in IndexedDB).
diff --git a/source/Files.cpp b/source/Files.cpp
index 96f5e89..5aaeb4d 100644
--- a/source/Files.cpp
+++ b/source/Files.cpp
@@ -16,7 +16,9 @@ this program. If not, see .
#include "Files.h"
#include "Logger.h"
+#ifndef __EMSCRIPTEN__
#include "ZipFile.h"
+#endif
#include "SDL.h"
@@ -48,6 +50,11 @@ namespace {
// Open the given folder in a separate window.
void OpenFolder(const filesystem::path &path)
{
+#ifdef __EMSCRIPTEN__
+ // In the browser the folders live in the page's IndexedDB; there is nothing to open.
+ static_cast(path);
+ return;
+#endif
// TODO: Remove SDL version check after Ubuntu 20.04 reaches end of life
#if SDL_VERSION_ATLEAST(2, 0, 14)
if(SDL_OpenURL(("file://" + path.string()).c_str()))
@@ -63,6 +70,16 @@ namespace {
#endif
}
+#ifdef __EMSCRIPTEN__
+ // No zip support in the web build (no minizip): plugins are not available there, and the game
+ // data is preloaded into the virtual filesystem as loose files.
+ struct ZipFile {
+ vector ListFiles(const filesystem::path &, bool, bool) { return {}; }
+ bool Exists(const filesystem::path &) { return false; }
+ string ReadFile(const filesystem::path &) { return {}; }
+ };
+ shared_ptr GetZipFile(const filesystem::path &) { return {}; }
+#else
/// The open zip files per thread. Since ZLIB doesn't support multithreaded access on the same zip handle,
/// each file is opened multiple times on demand.
thread_local map> OPEN_ZIP_FILES;
@@ -91,6 +108,7 @@ namespace {
return {};
}
+#endif
}
diff --git a/source/FrameTimer.cpp b/source/FrameTimer.cpp
index dcdf0e6..3c8c809 100644
--- a/source/FrameTimer.cpp
+++ b/source/FrameTimer.cpp
@@ -19,6 +19,8 @@ this program. If not, see .
#define STRICT
#define WIN32_LEAN_AND_MEAN
#include
+#elif defined(__EMSCRIPTEN__)
+#include
#else
#include
#endif
@@ -70,6 +72,10 @@ void FrameTimer::Wait()
// See the thread starting with https://sourceforge.net/p/mingw-w64/mailman/message/37013810/.
#ifdef _WIN32
Sleep(chrono::duration_cast(next - now).count());
+#elif defined(__EMSCRIPTEN__)
+ // Give the rest of the frame back to the browser (with -sASYNCIFY this returns to the event
+ // loop and resumes here), instead of spinning until the next frame is due.
+ emscripten_sleep(chrono::duration_cast(next - now).count());
#else
this_thread::sleep_until(next);
#endif
diff --git a/source/GameWindow.cpp b/source/GameWindow.cpp
index 0e99043..257b76f 100644
--- a/source/GameWindow.cpp
+++ b/source/GameWindow.cpp
@@ -30,12 +30,18 @@ this program. If not, see .
#include
#include
+#ifdef __EMSCRIPTEN__
+#include
+#endif
+
using namespace std;
namespace {
+#ifndef __EMSCRIPTEN__
// The minimal screen resolution requirements.
constexpr int minWidth = 1024;
constexpr int minHeight = 768;
+#endif
SDL_Window *mainWindow = nullptr;
SDL_GLContext context = nullptr;
@@ -168,11 +174,13 @@ bool GameWindow::Init(bool headless)
// Make the window just slightly smaller than the monitor resolution.
int maxWidth = mode.w;
int maxHeight = mode.h;
+#ifndef __EMSCRIPTEN__
if(maxWidth < minWidth || maxHeight < minHeight)
Logger::Log("Monitor resolution is too small! Minimal requirement is "
+ to_string(minWidth) + 'x' + to_string(minHeight)
+ ", while your resolution is " + to_string(maxWidth) + 'x' + to_string(maxHeight) + '.',
Logger::Level::WARNING);
+#endif
int windowWidth = maxWidth - 100;
int windowHeight = maxHeight - 100;
@@ -191,10 +199,18 @@ bool GameWindow::Init(bool headless)
// Settings that must be declared before the window creation.
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
+#ifdef __EMSCRIPTEN__
+ // In the browser the window is the page's canvas, which the page stretches over the whole player
+ // (SDL keeps it at the size of the page when it is resized). Full screen needs a click or a key
+ // press, so the game always starts in the page, whatever the saved setting says.
+ windowWidth = max(1, EM_ASM_INT({ return window.innerWidth; }));
+ windowHeight = max(1, EM_ASM_INT({ return window.innerHeight; }));
+#else
if(Preferences::ScreenModeSetting() == "fullscreen")
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
else if(Preferences::Has("maximized"))
flags |= SDL_WINDOW_MAXIMIZED;
+#endif
// The main window spawns visibly at this point.
#ifdef ES_USE_SDL3
@@ -523,6 +539,7 @@ void GameWindow::ToggleBlockScreenSaver()
void GameWindow::ExitWithError(const string &message, bool doPopUp)
{
// Print the error message in the terminal and the error file.
+ printf("Error: %s\n", message.c_str());
Logger::Log(message, Logger::Level::ERROR);
checkSDLerror();
diff --git a/source/PlayerInfo.cpp b/source/PlayerInfo.cpp
index d6e66d0..ae85702 100644
--- a/source/PlayerInfo.cpp
+++ b/source/PlayerInfo.cpp
@@ -683,6 +683,11 @@ void PlayerInfo::Save() const
// Save global conditions:
DataWriter globalConditions(Files::Config() / "global conditions.txt");
GameData::GlobalConditions().Save(globalConditions);
+#ifdef __EMSCRIPTEN__
+ // In the browser the preferences (including which help messages were already shown) are written
+ // with every save, since the tab is usually closed without quitting the game.
+ Preferences::Save();
+#endif
}
diff --git a/source/PreferencesPanel.cpp b/source/PreferencesPanel.cpp
index e19f248..ddc85ed 100644
--- a/source/PreferencesPanel.cpp
+++ b/source/PreferencesPanel.cpp
@@ -1380,6 +1380,11 @@ void PreferencesPanel::Exit()
}
Command::SaveSettings(Files::Config() / "keys.txt");
+#ifdef __EMSCRIPTEN__
+ // A browser tab is usually closed rather than quit through the menu, so the preferences are
+ // written as soon as the player leaves this panel instead of only when the game quits.
+ Preferences::Save();
+#endif
if(recacheDeadlines)
player.CacheMissionInformation(true);
diff --git a/source/Screen.cpp b/source/Screen.cpp
index 4f1e632..b228d5b 100644
--- a/source/Screen.cpp
+++ b/source/Screen.cpp
@@ -96,6 +96,17 @@ void Screen::SetZoom(int percent, bool noEvent)
int minZoomX = 100 * RAW_WIDTH / MIN_WIDTH;
int minZoomY = 100 * RAW_HEIGHT / MIN_HEIGHT;
int minZoom = min(minZoomX, minZoomY);
+#ifdef __EMSCRIPTEN__
+ // In the browser the window is the page's player, which can be smaller than the UI needs at 100%
+ // (a 4:3 frame of about 770 by 580 pixels): zoom out until the menus fit, instead of cutting them.
+ if(minZoom < 100)
+ {
+ EFFECTIVE_ZOOM = max(minZoom, 50);
+ WIDTH = RAW_WIDTH * 100 / EFFECTIVE_ZOOM;
+ HEIGHT = RAW_HEIGHT * 100 / EFFECTIVE_ZOOM;
+ return;
+ }
+#endif
// Never go below 100% zoom, no matter how small the window is
minZoom = max(minZoom, 100);
// Use increments of 10, like the user setting
diff --git a/source/TaskQueue.cpp b/source/TaskQueue.cpp
index 7f17774..6f10465 100644
--- a/source/TaskQueue.cpp
+++ b/source/TaskQueue.cpp
@@ -23,6 +23,7 @@ this program. If not, see .
using namespace std;
+#ifndef __EMSCRIPTEN__
namespace {
// The main task queue used by the worker threads.
@@ -55,11 +56,13 @@ namespace {
vector threads;
} threads;
}
+#endif // !__EMSCRIPTEN__
void TaskQueue::SetWorkerThreadCount(uint64_t count)
{
+#ifndef __EMSCRIPTEN__
if(count == 0 || threads.threads.size() == count)
return;
@@ -67,6 +70,8 @@ void TaskQueue::SetWorkerThreadCount(uint64_t count)
lock_guard lock(asyncMutex);
shouldQuit = false;
new(&threads) WorkerThreads(count);
+#endif
+ // In Emscripten there is no worker thread pool to resize.
}
@@ -85,6 +90,25 @@ TaskQueue::~TaskQueue()
// any main thread task that still need to be executed!
shared_future TaskQueue::Run(function asyncTask, function syncTask)
{
+#ifdef __EMSCRIPTEN__
+ // In Emscripten, execute tasks synchronously on the calling thread.
+ if(asyncTask)
+ {
+ try {
+ asyncTask();
+ }
+ catch(...)
+ {
+ auto exception = current_exception();
+ syncTask = [exception] { rethrow_exception(exception); };
+ }
+ }
+ if(syncTask)
+ syncTasks.push(std::move(syncTask));
+ promise p;
+ p.set_value();
+ return p.get_future();
+#else
shared_future result;
{
lock_guard lock(asyncMutex);
@@ -99,6 +123,7 @@ shared_future TaskQueue::Run(function asyncTask, function
}
asyncCondition.notify_one();
return result;
+#endif
}
@@ -106,6 +131,14 @@ shared_future TaskQueue::Run(function asyncTask, function
// Process any tasks to be scheduled to be executed on the main thread.
void TaskQueue::ProcessSyncTasks()
{
+#ifdef __EMSCRIPTEN__
+ for(int i = 0; !syncTasks.empty() && i < MAX_SYNC_TASKS; ++i)
+ {
+ auto task = std::move(syncTasks.front());
+ syncTasks.pop();
+ task();
+ }
+#else
unique_lock lock(syncMutex);
for(int i = 0; !syncTasks.empty() && i < MAX_SYNC_TASKS; ++i)
{
@@ -117,6 +150,7 @@ void TaskQueue::ProcessSyncTasks()
task();
lock.lock();
}
+#endif
}
@@ -124,8 +158,11 @@ void TaskQueue::ProcessSyncTasks()
// Waits for all of this queue's task to finish. Ignores any sync tasks to be processed.
void TaskQueue::Wait()
{
+#ifndef __EMSCRIPTEN__
while(!IsDone())
this_thread::yield();
+#endif
+ // In Emscripten, all tasks execute synchronously in Run(), so nothing to wait for.
}
@@ -133,8 +170,12 @@ void TaskQueue::Wait()
// Whether there are any outstanding async tasks left in this queue.
bool TaskQueue::IsDone() const
{
+#ifdef __EMSCRIPTEN__
+ return true;
+#else
lock_guard lock(asyncMutex);
return futures.empty();
+#endif
}
@@ -142,6 +183,7 @@ bool TaskQueue::IsDone() const
// Thread entry point.
void TaskQueue::ThreadLoop() noexcept
{
+#ifndef __EMSCRIPTEN__
while(true)
{
unique_lock lock(asyncMutex);
@@ -194,4 +236,5 @@ void TaskQueue::ThreadLoop() noexcept
asyncCondition.wait(lock, [] { return shouldQuit || !tasks.empty(); });
}
+#endif // !__EMSCRIPTEN__
}
diff --git a/source/audio/Audio.cpp b/source/audio/Audio.cpp
index aa2df8b..c27f9cc 100644
--- a/source/audio/Audio.cpp
+++ b/source/audio/Audio.cpp
@@ -168,7 +168,13 @@ void Audio::LoadSounds(const vector &sources)
}
// Begin loading the files.
if(!loadQueue.empty())
+ {
+#ifdef __EMSCRIPTEN__
+ Load();
+#else
loadThread = thread(&Load);
+#endif
+ }
}
@@ -280,6 +286,10 @@ void Audio::Play(const Sound *sound, const Point &position, SoundCategory catego
// Play the given music. An empty string means to play nothing.
void Audio::PlayMusic(const string &name)
{
+#ifdef __EMSCRIPTEN__
+ // Music is disabled under Emscripten.
+ return;
+#endif
if(!isInitialized)
return;
@@ -431,12 +441,14 @@ void Audio::Quit()
unique_lock lock(audioMutex);
if(!loadQueue.empty())
loadQueue.clear();
+#ifndef __EMSCRIPTEN__
if(loadThread.joinable())
{
lock.unlock();
loadThread.join();
lock.lock();
}
+#endif
// Now, stop and delete any OpenAL sources that are playing.
players.clear();
diff --git a/source/audio/Music.cpp b/source/audio/Music.cpp
index efa8242..1d9bb58 100644
--- a/source/audio/Music.cpp
+++ b/source/audio/Music.cpp
@@ -16,9 +16,11 @@ this program. If not, see .
#include "Music.h"
#include "../Files.h"
+#ifndef __EMSCRIPTEN__
#include "supplier/FlacSupplier.h"
-#include "../text/Format.h"
#include "supplier/Mp3Supplier.h"
+#endif
+#include "../text/Format.h"
#include