git repos / blockattack-game

commit 775432bc

Poul Sander · 2026-09-19 10:25
775432bc68dc7003c9ab6aa2fa2010362a968215 patch · browse files
parent a227b9e6dc90178563a53ea86c9e7ecadd4b676b

Add emscripten build option

Changed files

M .gitignore before
M CMakeLists.txt before
A EMSCRIPTEN.md
M README.md before
M source/code/editor/editor_loader.cpp before
M source/code/gamecontroller.cpp before
M source/code/main.cpp before
M source/code/os.cpp before
M source/code/os.hpp before
M source/code/sago/SagoMisc.cpp before
M source/code/sago/SagoMiscSdl2.cpp before
A source/misc/web/shell.html
diff --git a/.gitignore b/.gitignore index 1bd0073..4e126bf 100644 --- a/.gitignore +++ b/.gitignore
@@ -19,6 +19,7 @@ CMake*
!CMakeLists.txt
CPack*
Makefile
+build.ninja
source/manual/
-source/manual/block_attack_manual.tex
man/blockattack.6.gz
@@ -29,3 +30,4 @@ build
_CPack_Packages
#Different CPack results..
blockattack-*
+build-web
diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d23602..7c5366b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -45,28 +45,81 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
set(MACOSX TRUE)
endif()
-# Setup SDL2 and extensions
-find_package(SDL2 REQUIRED)
-
-# Try modern CMake approach first, fallback to pkg-config for cross-compilation (e.g., MXE)
-find_package(SDL2_image QUIET)
-find_package(SDL2_mixer QUIET)
-find_package(SDL2_ttf QUIET)
-
-set(SDL2_USE_PKGCONFIG OFF)
-if(NOT SDL2_image_FOUND OR NOT SDL2_mixer_FOUND OR NOT SDL2_ttf_FOUND)
- set(SDL2_USE_PKGCONFIG ON)
- find_package(PkgConfig REQUIRED)
- # When using pkg-config, always search for all SDL2 extensions
- pkg_search_module(SDL2_image REQUIRED SDL2_image)
- pkg_search_module(SDL2_mixer REQUIRED SDL2_mixer)
- pkg_search_module(SDL2_ttf REQUIRED SDL2_ttf)
-endif()
+# Platform dependencies (SDL2 family, PhysFS, gettext, Boost) are carried by
+# this INTERFACE target, however each platform provides them.
+add_library(blockattack_deps INTERFACE)
+
+if(EMSCRIPTEN)
+ # Data and locale are preloaded at "/" and cwd is "/", so the STANDALONE
+ # layout (SHAREDIR=".", LOCALEDIR="./locale") is exactly right.
+ set(STANDALONE ON)
+
+ # SDL2 family from Emscripten ports. Same flags at compile time (headers)
+ # and link time (prebuilt libraries).
+ set(BLOCKATTACK_EM_PORTS -sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sSDL2_IMAGE_FORMATS=png,jpg
+ -sUSE_SDL_TTF=2 -sUSE_SDL_MIXER=2 -sSDL2_MIXER_FORMATS=ogg)
+ target_compile_options(blockattack_deps INTERFACE ${BLOCKATTACK_EM_PORTS})
+ target_link_options(blockattack_deps INTERFACE ${BLOCKATTACK_EM_PORTS})
+
+ # PhysFS has no port; build it from source (zip archiver only, packdata.sh
+ # produces zips). Offline: -DFETCHCONTENT_SOURCE_DIR_PHYSFS=<physfs release-3.2.0 checkout>
+ include(FetchContent)
+ FetchContent_Declare(physfs
+ GIT_REPOSITORY https://github.com/icculus/physfs.git
+ GIT_TAG release-3.2.0)
+ set(PHYSFS_BUILD_SHARED OFF CACHE BOOL "" FORCE)
+ set(PHYSFS_BUILD_TEST OFF CACHE BOOL "" FORCE)
+ set(PHYSFS_BUILD_DOCS OFF CACHE BOOL "" FORCE)
+ foreach(archiver 7Z GRP WAD HOG MVL QPAK SLB ISO9660 VDF)
+ set(PHYSFS_ARCHIVE_${archiver} OFF CACHE BOOL "" FORCE)
+ endforeach()
+ set(CMAKE_POLICY_VERSION_MINIMUM 3.5) # physfs 3.2.0 still declares CMake 3.0
+ FetchContent_MakeAvailable(physfs)
+ unset(CMAKE_POLICY_VERSION_MINIMUM)
+ target_link_libraries(blockattack_deps INTERFACE PhysFS::PhysFS-static)
+
+ # libintl is part of Emscripten's libc. Boost.program_options is not used
+ # in the browser (see main.cpp). Exceptions are the game's error path and
+ # emcc disables catching them unless asked; Asyncify needs the JS-based flavour.
+ add_compile_options(-fexceptions)
+ add_link_options(-fexceptions)
+else()
+ # Setup SDL2 and extensions
+ find_package(SDL2 REQUIRED)
+
+ # Try modern CMake approach first, fallback to pkg-config for cross-compilation (e.g., MXE)
+ find_package(SDL2_image QUIET)
+ find_package(SDL2_mixer QUIET)
+ find_package(SDL2_ttf QUIET)
+
+ set(SDL2_USE_PKGCONFIG OFF)
+ if(NOT SDL2_image_FOUND OR NOT SDL2_mixer_FOUND OR NOT SDL2_ttf_FOUND)
+ set(SDL2_USE_PKGCONFIG ON)
+ find_package(PkgConfig REQUIRED)
+ # When using pkg-config, always search for all SDL2 extensions
+ pkg_search_module(SDL2_image REQUIRED SDL2_image)
+ pkg_search_module(SDL2_mixer REQUIRED SDL2_mixer)
+ pkg_search_module(SDL2_ttf REQUIRED SDL2_ttf)
+ endif()
+ find_package(Intl REQUIRED)
-find_package(Intl REQUIRED)
+ find_package(Boost COMPONENTS program_options REQUIRED NO_MODULE)
-find_package(Boost COMPONENTS program_options REQUIRED NO_MODULE)
+ # Link SDL2 extensions - use imported targets if available, otherwise use pkg-config variables
+ if(SDL2_USE_PKGCONFIG)
+ target_include_directories(blockattack_deps INTERFACE ${SDL2_image_INCLUDE_DIRS} ${SDL2_mixer_INCLUDE_DIRS} ${SDL2_ttf_INCLUDE_DIRS})
+ target_link_directories(blockattack_deps INTERFACE ${SDL2_image_LIBRARY_DIRS} ${SDL2_mixer_LIBRARY_DIRS} ${SDL2_ttf_LIBRARY_DIRS})
+ target_link_libraries(blockattack_deps INTERFACE ${SDL2_image_LINK_LIBRARIES} ${SDL2_mixer_LINK_LIBRARIES} ${SDL2_ttf_LINK_LIBRARIES})
+ else()
+ target_link_libraries(blockattack_deps INTERFACE SDL2_image::SDL2_image SDL2_mixer::SDL2_mixer SDL2_ttf::SDL2_ttf)
+ endif()
+ target_include_directories(blockattack_deps INTERFACE ${Intl_INCLUDE_DIRS})
+ target_link_libraries(blockattack_deps INTERFACE SDL2::SDL2 physfs ${Boost_LIBRARIES})
+ if (MACOSX)
+ target_link_libraries(blockattack_deps INTERFACE intl)
+ endif()
+endif()
#Find all sources but one
file(GLOB SOURCES CONFIGURE_DEPENDS "source/code/*.cpp" "source/code/*.inc" "source/code/*.h*" "source/code/*/*.cpp" "source/code/*/*.h*" "source/code/Libs/*.c" "source/code/Libs/*.h*" "source/code/Libs/*/*.cpp" "source/code/Libs/*/*/*.cpp")
@@ -124,36 +177,47 @@ endif()
target_include_directories(blockattack SYSTEM PRIVATE "source/code/Libs/imgui")
target_include_directories(blockattack PRIVATE "source/code/Libs/include")
-if(SDL2_USE_PKGCONFIG)
- target_include_directories(blockattack PRIVATE ${SDL2_image_INCLUDE_DIRS})
- target_include_directories(blockattack PRIVATE ${SDL2_mixer_INCLUDE_DIRS})
- target_include_directories(blockattack PRIVATE ${SDL2_ttf_INCLUDE_DIRS})
- # Add library directories for pkg-config-based libraries
- target_link_directories(blockattack PRIVATE ${SDL2_image_LIBRARY_DIRS})
- target_link_directories(blockattack PRIVATE ${SDL2_mixer_LIBRARY_DIRS})
- target_link_directories(blockattack PRIVATE ${SDL2_ttf_LIBRARY_DIRS})
-endif()
-target_include_directories(blockattack PRIVATE ${Intl_INCLUDE_DIRS})
target_link_libraries(blockattack PRIVATE
- SDL2::SDL2
+ blockattack_deps
fmt
platform_folders
- physfs
- ${Boost_LIBRARIES}
)
+set_target_properties(blockattack PROPERTIES RUNTIME_OUTPUT_DIRECTORY Game)
-# Link SDL2 extensions - use imported targets if available, otherwise use pkg-config variables
-if(SDL2_USE_PKGCONFIG)
- target_link_libraries(blockattack PRIVATE ${SDL2_image_LINK_LIBRARIES} ${SDL2_mixer_LINK_LIBRARIES} ${SDL2_ttf_LINK_LIBRARIES})
-else()
- target_link_libraries(blockattack PRIVATE SDL2_image::SDL2_image SDL2_mixer::SDL2_mixer SDL2_ttf::SDL2_ttf)
-endif()
-
-if (MACOSX)
- target_link_libraries(blockattack PRIVATE intl)
+if(EMSCRIPTEN)
+ set(BLOCKATTACK_WEB_SHELL "${blockattack_SOURCE_DIR}/source/misc/web/shell.html")
+ set(BLOCKATTACK_DATA_FILE "${blockattack_SOURCE_DIR}/Game/blockattack.data")
+ set(BLOCKATTACK_MOD_FILE "${blockattack_SOURCE_DIR}/Game/mods/1.3.0.bricks.data")
+ set(BLOCKATTACK_LOCALE_DIR "${blockattack_SOURCE_DIR}/source/misc/translation/locale")
+ foreach(f IN ITEMS "${BLOCKATTACK_DATA_FILE}" "${BLOCKATTACK_MOD_FILE}" "${BLOCKATTACK_LOCALE_DIR}")
+ if(NOT EXISTS "${f}")
+ message(FATAL_ERROR "${f} is missing. Run ./packdata.sh before configuring the web build.")
+ endif()
+ endforeach()
+ # One page: index.html (from the shell, which sizes the canvas to the
+ # browser window) loads index.js, index.wasm and index.data. ASYNCIFY lets
+ # the blocking SDL_Delay game loops yield to the browser each frame instead
+ # of being rewritten around a callback; its stack holds the locals of every
+ # unwound frame, so it is sized well above the default 4 KiB. IDBFS backs
+ # the save folder (see os.cpp).
+ set_target_properties(blockattack PROPERTIES
+ OUTPUT_NAME index
+ SUFFIX ".html"
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/web")
+ target_link_options(blockattack PRIVATE
+ -sASYNCIFY
+ -sASYNCIFY_STACK_SIZE=1048576
+ -sALLOW_MEMORY_GROWTH=1
+ -lidbfs.js
+ $<$<CONFIG:Debug>:-sASSERTIONS=1>
+ "SHELL:--shell-file ${BLOCKATTACK_WEB_SHELL}"
+ "SHELL:--preload-file ${BLOCKATTACK_DATA_FILE}@/blockattack.data"
+ "SHELL:--preload-file ${BLOCKATTACK_MOD_FILE}@/mods/1.3.0.bricks.data"
+ "SHELL:--preload-file ${BLOCKATTACK_LOCALE_DIR}@/locale")
+ set_property(TARGET blockattack APPEND PROPERTY LINK_DEPENDS
+ "${BLOCKATTACK_WEB_SHELL}" "${BLOCKATTACK_DATA_FILE}" "${BLOCKATTACK_MOD_FILE}")
endif()
-set_target_properties(blockattack PROPERTIES RUNTIME_OUTPUT_DIRECTORY Game)
#Installing
include(CPack)
diff --git a/EMSCRIPTEN.md b/EMSCRIPTEN.md new file mode 100644 index 0000000..51c78c3 --- /dev/null +++ b/EMSCRIPTEN.md
@@ -0,0 +1,15 @@
+## Building for the web (Emscripten)
+The browser build uses the Emscripten SDK (tested with emcc 6.0). It does not change the native build: the SDL2 libraries come from Emscripten's ports and PhysFS is fetched and built from source. The first configure therefore needs network access. To build offline pass `-DFETCHCONTENT_SOURCE_DIR_PHYSFS=<path to a physfs release-3.2.0 checkout>`.
+```bash
+source <emsdk>/emsdk_env.sh
+./packdata.sh
+emcmake cmake -S . -B build-web -DCMAKE_BUILD_TYPE=Release
+cmake --build build-web
+```
+The result is `build-web/web/index.html` with `index.js`, `index.wasm` and `index.data`. Browsers refuse to load wasm from `file://`, so serve the folder:
+```bash
+python3 -m http.server -d build-web/web 8080
+```
+and open <http://localhost:8080/>.
+
+Settings and highscores are stored in the browser's IndexedDB for that origin. Command line options, the editors and screenshots are not available in the browser.
diff --git a/README.md b/README.md index b432019..3dc4d4a 100644 --- a/README.md +++ b/README.md
@@ -17,7 +17,7 @@ Recent Linux version. Target is all current Ubuntu Desktop LTS versions (up to 5
Target is all officially supported versions of Windows for x86-64 that allow manually installed software. Currently Windows 10+.
## Dependencies
-* A version of g++ with C++17 support. Tested on g++-11
+* A version of g++ with C++20 support. Tested on g++-11
* libSDL2
* libSDL2_image
* libSDL2_mixer
diff --git a/source/code/editor/editor_loader.cpp b/source/code/editor/editor_loader.cpp index 643c38f..6acbce6 100644 --- a/source/code/editor/editor_loader.cpp +++ b/source/code/editor/editor_loader.cpp
@@ -23,7 +23,6 @@ https://github.com/sago007/saland
#include "editor_loader.hpp"
#include "../sago/SagoMisc.hpp"
-#include <boost/algorithm/string/predicate.hpp>
#include <iostream>
@@ -84,7 +83,7 @@ std::map<std::string, SagoSprite> LoadSprites() {
std::map<std::string, SagoSprite> sprites;
std::vector<std::string> spritefiles = sago::GetFileList("sprites");
for (std::string& item : spritefiles ) {
- if (boost::algorithm::ends_with(item,".sprite")) {
+ if (item.ends_with(".sprite")) {
std::string filename = "sprites/"+item;
std::string content = sago::GetFileContent(filename.c_str());
if (content.empty()) {
diff --git a/source/code/gamecontroller.cpp b/source/code/gamecontroller.cpp index 2b41349..4c79960 100644 --- a/source/code/gamecontroller.cpp +++ b/source/code/gamecontroller.cpp
@@ -97,11 +97,14 @@ void UnInitGameControllers() {
}
void InitGameControllers() {
+#ifndef __EMSCRIPTEN__
+ //The browser has no user config folder
std::string configFile = sago::getConfigHome()+"/blockattack/gamecontrollerdb.txt";
int errorCode = SDL_GameControllerAddMappingsFromFile(configFile.c_str());
if (errorCode == -1 && verbose) {
std::cerr << "Could not load mapping file: " << configFile << "\n";
}
+#endif
if (verbose) {
std::cout << "Number of Game controllers: " << SDL_NumJoysticks() << "\n";
}
diff --git a/source/code/main.cpp b/source/code/main.cpp index ec2183a..fa34676 100644 --- a/source/code/main.cpp +++ b/source/code/main.cpp
@@ -61,7 +61,11 @@ https://blockattack.net
#include "common.h"
#include "gamecontroller.h"
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#else
#include <boost/program_options.hpp>
+#endif
#include <fstream>
#include "levelselect.hpp"
@@ -82,9 +86,14 @@ GlobalData globalData;
static int InitImages(sago::SagoSpriteHolder& holder);
static void FsSearchParthMainAppend(std::vector<std::string>& paths) {
+#ifdef __EMSCRIPTEN__
+ //The browser build preloads the data file here (CMakeLists.txt)
+ paths.push_back("/blockattack.data");
+#else
paths.push_back((std::string)SHAREDIR+"/blockattack.data");
paths.push_back((std::string)PHYSFS_getBaseDir()+"/blockattack.data");
paths.push_back((std::string)PHYSFS_getBaseDir()+"/data");
+#endif
}
static void FsSearchPathModAppend(std::vector<std::string>& paths, const std::vector<std::string>& modlist) {
@@ -361,6 +370,10 @@ std::string pathToScreenShots() {
//writeScreenShot saves the screen as a bmp file, it uses the time to get a unique filename
void writeScreenShot() {
+#ifdef __EMSCRIPTEN__
+ //The browser has no pictures folder to save to
+ return;
+#endif
if (globalData.verboseLevel) {
std::cout << "Saving screenshot" << "\n";
}
@@ -904,6 +917,11 @@ struct globalConfig {
bool softwareRenderer = false;
};
+#ifdef __EMSCRIPTEN__
+//The browser build has no command line
+static void ParseArguments(int, char*[], globalConfig&) {
+}
+#else
static void ParseArguments(int argc, char* argv[], globalConfig& conf) {
int consoleWidth = boost::program_options::options_description::m_default_line_length;
const char* columnsEnv = getenv("COLUMNS"); // Allows using "COLUMNS=300 help2man" for generating the man page without bad line breaks.
@@ -1046,6 +1064,7 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) {
}
}
+#endif
//Physfs 2.0.z does not have PHYSFS_unmount
#if (PHYSFS_VER_MAJOR <= 2) && (PHYSFS_VER_MINOR < 1)
@@ -1081,6 +1100,7 @@ int main(int argc, char* argv[]) {
bind_textdomain_codeset(PACKAGE, "utf-8");
textdomain (PACKAGE);
ParseArguments(argc, argv, config);
+ OsMountPersistentStorage();
OsCreateSaveFolder();
writeStateFile(argv[0], PHYSFS_getBaseDir());
PhysFsSetSearchPath(config.search_paths, config.savepath);
@@ -1102,10 +1122,13 @@ int main(int argc, char* argv[]) {
//Os create folders must be after the parameters because they can change the home folder
PhysFsCreateFolders();
bool gameShutdownProperly = true;
+#ifndef __EMSCRIPTEN__
+ //A closed browser tab never runs the shutdown code, so the crash detection is skipped there
if (sago::FileExists("gameRunning")) {
gameShutdownProperly = false;
}
sago::WriteFileContent("gameRunning", "Started");
+#endif
globalData.SoundEnabled = true;
globalData.MusicEnabled = true;
twoPlayers = false; //true if two players splitscreen
@@ -1418,9 +1441,19 @@ int main(int argc, char* argv[]) {
catch (std::exception& e) {
sago::SagoFatalError(e.what());
}
+#ifndef __EMSCRIPTEN__
PHYSFS_delete("gameRunning");
+#endif
//Close file system Apstraction layer!
PHYSFS_deinit();
+#ifdef __EMSCRIPTEN__
+ //The page stays open with the last frame on the canvas, so tell the player what happened
+ EM_ASM({
+ if (Module.setStatus) {
+ Module.setStatus('The game has been closed. Reload the page to play again.');
+ }
+ });
+#endif
return 0;
}
diff --git a/source/code/os.cpp b/source/code/os.cpp index 77e0ece..4ab6759 100644 --- a/source/code/os.cpp +++ b/source/code/os.cpp
@@ -23,6 +23,8 @@ https://blockattack.net
#include "os.hpp"
#include <iostream>
+#include <filesystem>
+#include <system_error>
#include <physfs.h>
#include "platform_folders.h"
#include "version.h"
@@ -34,7 +36,14 @@ https://blockattack.net
#endif
#include <dirent.h>
+#if defined(__EMSCRIPTEN__)
+// Emscripten's getuid() stub reports root, which makes PlatformFolders look up
+// a passwd entry that does not exist. The browser build uses a fixed path that
+// only serves as the IDBFS mount point (see OsMountPersistentStorage).
+static const char* const webSaveFolder = "/home/web_user/.local/share";
+#else
static sago::PlatformFolders pf;
+#endif
static std::string overrideSavePath = "";
@@ -48,7 +57,11 @@ std::string getPathToSaveFiles() {
if (overrideSavePath.length() > 0) {
return overrideSavePath;
}
+#if defined(__EMSCRIPTEN__)
+ return std::string(webSaveFolder)+"/"+GAMENAME;
+#else
return pf.getSaveGamesFolder1()+"/"+GAMENAME;
+#endif
}
std::string getPathToStateFiles() {
@@ -91,7 +104,7 @@ std::string defaultPlayerName() {
ret = sago::internal::win32_utf16_to_utf8(win_buffer);
}
#endif
-#if defined(__unix__)
+#if defined(__unix__) && !defined(__EMSCRIPTEN__)
int uid = getuid();
struct passwd* pw = getpwuid(uid);
if (pw && pw->pw_gecos) {
@@ -117,7 +130,6 @@ bool OsPathIsRelative(const std::string& path) {
}
void OsCreateFolder(const std::string& path) {
- //Once all supported systems works with C++17 then we can use "std::filesystem::create_directories" instead
#if defined(_WIN32)
//Now for Windows Vista+
std::vector<std::string> element_vector;
@@ -131,11 +143,40 @@ void OsCreateFolder(const std::string& path) {
}
CreateDirectoryW(win32_utf8_to_utf16(path.c_str()).c_str(), NULL);
#else
- std::string cmd = "mkdir -p '"+path+"/'";
- int retcode = system(cmd.c_str());
- if (retcode != 0) {
- std::cerr << "Failed to create: " << path+"/" << "\n";
+ std::error_code ec;
+ std::filesystem::create_directories(path, ec);
+ if (ec) {
+ std::cerr << "Failed to create: " << path+"/" << " (" << ec.message() << ")\n";
+ }
+#endif
+}
+
+#if defined(__EMSCRIPTEN__)
+#include <emscripten.h>
+
+// Mounts IndexedDB-backed storage at the save folder and pulls the stored
+// files in. Must run before PhysFS gets its write dir so every existing
+// write path (PhysFS and std::ofstream) lands in persistent storage.
+// A failed mount is only logged: the folder still exists in the in-memory
+// file system, so the game runs but nothing survives a reload.
+EM_ASYNC_JS(int, os_web_mount_idbfs, (const char* cpath), {
+ const path = UTF8ToString(cpath);
+ try {
+ FS.mkdirTree(path);
+ FS.mount(FS.filesystems.IDBFS, {}, path);
+ await new Promise((resolve, reject) => FS.syncfs(true, (err) => err ? reject(err) : resolve()));
+ return 0;
}
+ catch (e) {
+ console.error('Persistent storage unavailable, saves will not survive a reload: ' + e);
+ return -1;
+ }
+});
+#endif
+
+void OsMountPersistentStorage() {
+#if defined(__EMSCRIPTEN__)
+ os_web_mount_idbfs(getPathToSaveFiles().c_str());
#endif
}
diff --git a/source/code/os.hpp b/source/code/os.hpp index 05c124f..20c5934 100644 --- a/source/code/os.hpp +++ b/source/code/os.hpp
@@ -41,6 +41,13 @@ void setPathToSaveFiles(const std::string& path);
void OsCreateSaveFolder();
+/**
+ * In the browser build: mounts persistent (IndexedDB) storage at the save
+ * folder and loads the stored files. Must be called before the save folder
+ * is used. Does nothing on other platforms.
+ */
+void OsMountPersistentStorage();
+
bool OsPathIsRelative(const std::string& path);
/**
diff --git a/source/code/sago/SagoMisc.cpp b/source/code/sago/SagoMisc.cpp index 08c92d6..db38434 100644 --- a/source/code/sago/SagoMisc.cpp +++ b/source/code/sago/SagoMisc.cpp
@@ -33,6 +33,36 @@ SOFTWARE.
#define PHYSFS_writeBytes(X,Y,Z) PHYSFS_write(X,Y,1,Z)
#endif
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+
+// Pushes the IDBFS mount to IndexedDB after a write. Fire-and-forget;
+// overlapping requests are coalesced because Emscripten warns when several
+// syncfs calls run at once.
+EM_JS(void, sago_web_syncfs, (), {
+ const state = (Module.sagoSyncState ||= { inFlight: false, pending: false });
+ function run() {
+ state.inFlight = true;
+ FS.syncfs(false, (err) => {
+ state.inFlight = false;
+ if (err) {
+ console.error('IDBFS sync failed: ' + err);
+ }
+ if (state.pending) {
+ state.pending = false;
+ run();
+ }
+ });
+ }
+ if (state.inFlight) {
+ state.pending = true;
+ }
+ else {
+ run();
+ }
+});
+#endif
+
namespace sago {
@@ -109,6 +139,9 @@ void WriteFileContent(const char* filename, const std::string& content) {
}
PHYSFS_writeBytes(myfile, content.c_str(), sizeof(char)*content.length());
PHYSFS_close(myfile);
+#ifdef __EMSCRIPTEN__
+ sago_web_syncfs();
+#endif
}
long int StrToLong(const char* c_string) {
diff --git a/source/code/sago/SagoMiscSdl2.cpp b/source/code/sago/SagoMiscSdl2.cpp index 8a3d294..155c883 100644 --- a/source/code/sago/SagoMiscSdl2.cpp +++ b/source/code/sago/SagoMiscSdl2.cpp
@@ -24,8 +24,24 @@ SOFTWARE.
#include "SagoMiscSdl2.hpp"
#include "SDL.h"
+#ifdef __EMSCRIPTEN__
+#include <emscripten.h>
+#endif
void sago::SagoFatalError(const char* errorMsg) {
+#ifdef __EMSCRIPTEN__
+ //SDL_ShowMessageBox is not implemented in the Emscripten video driver and
+ //alert() would block the page, so the message goes to the console and to
+ //the status overlay of the shell page
+ EM_ASM({
+ var message = 'Fatal error: ' + UTF8ToString($0);
+ console.error(message);
+ if (Module.setStatus) {
+ Module.setStatus(message);
+ }
+ }, errorMsg);
+ abort();
+#else
const SDL_MessageBoxButtonData buttons[] = {
{ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 0, "Ok" },
};
@@ -41,6 +57,7 @@ void sago::SagoFatalError(const char* errorMsg) {
int buttonid;
SDL_ShowMessageBox(&messageboxdata, &buttonid);
abort();
+#endif
}
void sago::SagoFatalErrorF(const char* fmt, ...) {
diff --git a/source/misc/web/shell.html b/source/misc/web/shell.html new file mode 100644 index 0000000..7bdbc33 --- /dev/null +++ b/source/misc/web/shell.html
@@ -0,0 +1,81 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Block Attack - Rise of the Blocks</title>
+ <style>
+ html, body {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: #000;
+ color: #ddd;
+ font-family: sans-serif;
+ }
+ /* The canvas fills the viewport. SDL's Emscripten backend adopts an
+ externally sized canvas as the window size and reports every browser
+ resize as a window resize, which the game letterboxes itself. */
+ #canvas {
+ display: block;
+ width: 100vw;
+ height: 100vh;
+ outline: none;
+ }
+ #status {
+ position: fixed;
+ left: 0;
+ right: 0;
+ top: 45%;
+ text-align: center;
+ font-size: 1.5em;
+ pointer-events: none;
+ }
+ #progress {
+ display: block;
+ margin: 0.5em auto 0;
+ width: 40%;
+ }
+ #hint {
+ font-size: 0.6em;
+ margin-top: 1em;
+ }
+ </style>
+</head>
+<body>
+ <div id="status">Loading...<progress id="progress" value="0" max="100" hidden></progress><div id="hint">Sound starts after the first click on the game.</div></div>
+ <canvas id="canvas" tabindex="-1" oncontextmenu="event.preventDefault()"></canvas>
+ <script>
+ var statusElement = document.getElementById('status');
+ var progressElement = document.getElementById('progress');
+ var Module = {
+ canvas: document.getElementById('canvas'),
+ print: function(text) { console.log(text); },
+ printErr: function(text) { console.error(text); },
+ setStatus: function(text) {
+ // Emscripten reports downloads as "Downloading data... (loaded/total)".
+ var match = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
+ if (match) {
+ progressElement.value = parseInt(match[2]) * 100 / parseInt(match[4]);
+ progressElement.hidden = false;
+ statusElement.firstChild.textContent = match[1].trim();
+ } else {
+ statusElement.firstChild.textContent = text;
+ progressElement.hidden = true;
+ }
+ statusElement.hidden = text.length == 0;
+ },
+ onRuntimeInitialized: function() { statusElement.hidden = true; }
+ };
+ Module.canvas.addEventListener('webglcontextlost', function(event) {
+ alert('WebGL context lost. Reload the page.');
+ event.preventDefault();
+ }, false);
+ window.onerror = function(message) {
+ Module.setStatus('Error: ' + message);
+ };
+ </script>
+ {{{ SCRIPT }}}
+</body>
+</html>