Emscripten: Implement downloading community levels

Adding ?play=[id] to the deployed HTML file will download and play that specific ID from the community site
This commit is contained in:
ROllerozxa 2025-12-27 21:39:02 +01:00
commit 81d7731665
5 changed files with 225 additions and 3 deletions

View file

@ -268,8 +268,7 @@ if(EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "--preload-file ../data/")
set(LIBRARY_FLAGS "-sUSE_FREETYPE=1 -sUSE_LIBJPEG=1 -sUSE_LIBPNG=1 -sUSE_ZLIB=1 -sUSE_SDL=2 -pthread")
string(APPEND COMMON_FLAGS " ${LIBRARY_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS " ${LIBRARY_FLAGS} -pthread -sPTHREAD_POOL_SIZE=20 -sINITIAL_MEMORY=2013265920 -sALLOW_MEMORY_GROWTH=1 -sTOTAL_STACK=16Mb")
set(CMAKE_EXECUTABLE_SUFFIX ".html")
set(CMAKE_EXE_LINKER_FLAGS " ${LIBRARY_FLAGS} -pthread -sPTHREAD_POOL_SIZE=20 -sINITIAL_MEMORY=2013265920 -sALLOW_MEMORY_GROWTH=1 -sTOTAL_STACK=16Mb -sFETCH=1")
endif()
set(COMMON_FLAGS_DEBUG "${COMMON_FLAGS} -O0 -ggdb -DDEBUG=1")
@ -311,6 +310,13 @@ if(APPLE)
install(FILES "packaging/principia.icns" DESTINATION "${SHAREDIR}")
install(FILES "packaging/Info.plist" DESTINATION "${BUNDLE_PATH}/Contents")
elseif(EMSCRIPTEN)
set(BINDIR .)
install(FILES "packaging/index.html" DESTINATION .)
install(FILES ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.wasm DESTINATION .)
install(FILES ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.data DESTINATION .)
else()
include(GNUInstallDirs)
set(SHAREDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}")

72
packaging/index.html Normal file
View file

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Principia</title>
<style>
body {
margin: 0;
padding: 0;
background: #000513;
color: white;
}
canvas {
display: block;
margin: auto;
position: absolute;
top: 0; bottom: 0; left: 0; right: 0;
aspect-ratio: 16 / 9;
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
<script>
var Module = {
preRun: [],
postRun: [],
print: function (text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.log(text);
},
printErr: function (text) {
if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
console.error(text);
},
canvas: (function () { return document.getElementById('canvas'); })(),
setStatus: function (text) {
console.log(text);
},
totalDependencies: 0,
monitorRunDependencies: function (left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies - left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
}
};
// Read optional `?play=[id]` query parameter and set Module.arguments accordingly
(function() {
var params = new URLSearchParams(window.location.search);
var play = params.get('play');
if (play && play.length > 0) {
Module.arguments = ["principia://principia-web.se/play/lvl/db/" + encodeURIComponent(play)];
}
})();
Module.setStatus('Downloading...');
window.onerror = function (msg) {
// TODO: do not warn on ok events like simulating an infinite loop or exitStatus
Module.setStatus('Exception thrown, see JavaScript console (' + msg + ')');
Module.setStatus = function (text) {
if (text) Module.printErr('[post-exception status] ' + text);
};
};
</script>
<script async src="principia.js"></script>
</body>
</html>

128
src/emscripten_interop.cc Normal file
View file

@ -0,0 +1,128 @@
#ifdef __EMSCRIPTEN__
#include "emscripten_interop.hh"
#include "const.hh"
#include "main.hh"
#include "network.hh"
#include "pkgman.hh"
#include "tms/backend/print.h"
#include "tms/core/err.h"
#include <cstdlib>
#include <emscripten/fetch.h>
/**
* Emscripten-based level downloader using emscripten_fetch (async callbacks).
* Slightly sloppy and copied from _download_level() in network.cc
*/
int _download_level_emscripten(void *p)
{
_play_downloading_error = 0;
if (_play_header_data.error_message) {
free(_play_header_data.error_message);
_play_header_data.error_message = 0;
}
if (_play_header_data.notify_message) {
free(_play_header_data.notify_message);
_play_header_data.notify_message = 0;
}
_play_header_data.error_action = 0;
int arg = (intptr_t)p;
int type = LEVEL_DB;
bool derive = true;
if (arg == 0) {
type = LEVEL_DB;
derive = false;
} else if (arg == 1) {
type = LEVEL_LOCAL;
derive = true;
} else if (arg == 2) {
type = LEVEL_LOCAL;
derive = false;
}
tms_infof("before: %d ++++++++++++++++++++++ ", _play_id);
uint32_t new_id = type == LEVEL_LOCAL ? pkgman::get_next_level_id() : _play_id;
uint32_t old_id = _play_id;
char save_path[1024];
sprintf(save_path, "%s/%d.plvl",
pkgman::get_level_path(type),
new_id);
tms_debugf("save: %s", save_path);
uint32_t r = 0;
if (type == LEVEL_DB) {
lvledit e;
if (e.open(LEVEL_DB, new_id)) {
r = e.lvl.revision;
tms_debugf("we already have this DB level of revision %u", r);
}
}
const char *host = strlen(_community_host) > 0 ? _community_host : P.community_host;
char url[512];
snprintf(url, sizeof(url) - 1, "https://%s/internal/%s_level?i=%d&h=%u",
host,
_play_download_for_pkg ? "get_package" :
(type == LEVEL_DB ? "get" :
(derive ? "derive" : "edit")),
_play_id, r);
tms_infof("url: %s", url);
_play_id = new_id;
tms_infof("_play_id = %d -----------------------", _play_id);
// callbacks
static auto success_cb = [](emscripten_fetch_t *fetch) {
const char *path = (const char*)fetch->userData;
FILE *f = fopen(path, "wb");
if (f) {
fwrite(fetch->data, 1, fetch->numBytes, f);
fclose(f);
tms_infof("Saved level to %s", path);
} else {
tms_errorf("Could not open %s for writing", path);
_play_downloading_error = DOWNLOAD_WRITE_ERROR;
}
emscripten_fetch_close(fetch);
free((void*)path);
_play_downloading = false;
};
static auto error_cb = [](emscripten_fetch_t *fetch) {
const char *path = (const char*)fetch->userData;
tms_errorf("Failed to download level (status %d) -> %s", fetch->status, path ? path : "(null)");
emscripten_fetch_close(fetch);
free((void*)path);
if (fetch->status == 404) {
_play_downloading_error = DOWNLOAD_GENERIC_ERROR;
} else {
_play_downloading_error = DOWNLOAD_CHECK_INTERNET_CONNECTION;
}
_play_downloading = false;
};
// allocate and pass save path as userData
char *user = strdup(save_path);
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.onsuccess = +[](emscripten_fetch_t *fetch){ success_cb(fetch); };
attr.onerror = +[](emscripten_fetch_t *fetch){ error_cb(fetch); };
attr.userData = user;
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
_play_downloading = true;
emscripten_fetch(&attr, url);
return T_OK;
}
#endif

View file

@ -0,0 +1,3 @@
#pragma once
int _download_level_emscripten(void *p);

View file

@ -51,6 +51,10 @@
#include "network.hh"
#ifdef __EMSCRIPTEN__
#include "emscripten_interop.hh"
#endif
principia P={0};
static struct tms_fb *gi_fb;
static struct tms_fb *ao_fb;
@ -1397,10 +1401,17 @@ level_loader(int step)
_play_downloading = true;
create_thread(_download_level, "_download_level", 0);
}
#endif
// Special handling for Emscripten...
#ifdef __EMSCRIPTEN__
if (_play_type == LEVEL_DB) {
_play_downloading = true;
create_thread(_download_level_emscripten, "_download_level_emscripten", 0);
}
#endif
break;
case 1:
#ifdef BUILD_CURL
#if defined(BUILD_CURL) || defined(__EMSCRIPTEN__)
if (num < 10) {
P.s_loading_screen->set_text("Downloading level.");
} else if (num < 20) {
@ -1411,6 +1422,7 @@ level_loader(int step)
if (_play_downloading) return LOAD_RETRY;
#ifdef BUILD_CURL
if (_play_downloading_error) {
handle_downloading_error(_play_downloading_error);
@ -1419,6 +1431,7 @@ level_loader(int step)
if (_play_header_data.notify_message)
ui::message(_play_header_data.notify_message, 1);
}
#endif
#endif
G->screen_back = 0;