pull_request:
jobs:
- build:
-
+ sdl-release:
runs-on: ubuntu-latest
container:
image: debian:latest
# make `git describe` show the correct commit hash
fetch-depth: '0'
- - name: Compile DP
+ - name: Compile
run: |
# prevent git complaining about dubious ownership of the repo
chown -R root:root .
path: |
darkplaces-sdl
+ wasm-release:
+ runs-on: ubuntu-latest
+ container:
+ image: debian:latest
+ steps:
+ - name: Install dependencies
+ run: |
+ apt update
+ apt install --yes build-essential python3-certifi
+
+ - name: Fetch repository
+ uses: actions/checkout@v4.1.1
+ with:
+ # make `git describe` show the correct commit hash
+ fetch-depth: '0'
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3'
+
+ - name: Install emsdk
+ uses: actions/checkout@v4.1.1
+ with:
+ repository: emscripten-core/emsdk
+ path: emsdk
+
+ - name: Compile
+ shell: bash
+ run: |
+ cd emsdk
+
+ # Download and install the latest SDK tools.
+ ./emsdk install latest
+
+ # Make the "latest" SDK "active" for the current user. (writes .emscripten file)
+ ./emsdk activate latest
+
+ # Activate PATH and other environment variables in the current terminal
+ source ./emsdk_env.sh
+
+ cd ..
+
+ # fail if there's any warnings
+ #export CC="cc"
+
+ make emscripten-release
+
+ - name: Upload WASM artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: Wasm
+ path: |
+ darkplaces-wasm.js
darkplaces-agl
darkplaces-glx
darkplaces-sdl
+darkplaces-wasm.html
+darkplaces-wasm.js
darkplaces-dedicated
gmon.out
*.ncb
*.pdb
*.lib
*.exp
+
+# emscripten
+build-obj/
+emsdk/
+docs/output/
+wasm/preload/*
+!wasm/preload/runhere
To get a build suitable for playing you'll need to use MinGW GCC, or download the autobuild from Xonotic (see above).
+### Web-Assembly (Emscripten)
+
+Note that this requires a linux device or WSL2.
+
+1. Install the [Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended)
+1. After activating and sourcing emsdk, compile DarkPlaces for wasm using;
+ ```shell
+ make emscripten-release
+ ```
+1. Copy `darkplaces-wasm.js`, `wasm/index.html`, and `wasm/autoexec.cfg` files to your web server
+1. Copy the Quake `pak0.pak` and any other files into the same web server directory
+
+For the standalone version (single HTML file containing engine and data):
+1. Before compiling, copy game data and .cfg files to the appropriate gamedir in `wasm/preload` (for example, pak0 from Quake would be in `wasm/preload/id1/pak0.pak`)
+1. After activating and sourcing emsdk, compile DarkPlaces for wasm using;
+ ```shell
+ make emscripten-standalone
+ ```
+1. To start DP you must click somewhere in the window!
+1. If you want to upload files into the game filesystem, use `em_upload` in the darkplaces console (upload to /save if you want it to save across restarts)
+1. To save the stuff you uploaded to /save, use `em_save` (note that if you embedded the game, you won't be able to save changes to `/save/games`)
+
+
## Contributing
[DarkPlaces Contributing Guidelines](CONTRIBUTING.md)
Cmd_AddCommand(CF_SHARED, "ls", FS_Ls_f, "list files in searchpath matching an * filename pattern, multiple per line");
Cmd_AddCommand(CF_SHARED, "which", FS_Which_f, "accepts a file name as argument and reports where the file is taken from");
+#ifdef __EMSCRIPTEN__
+ Sys_EM_Register_Commands();
+#endif
+
if (com_startupgamegroup == GAME_NORMAL)
Cmd_AddCommand(CF_SHARED, "game", FS_GameDir_f, "alias of gamedir, for compatibility with some Quake mod READMEs");
}
cvar_t r_render = {CF_CLIENT, "r_render", "1", "enables rendering 3D views (you want this on!)"};
cvar_t r_renderview = {CF_CLIENT, "r_renderview", "1", "enables rendering 3D views (you want this on!)"};
cvar_t r_waterwarp = {CF_CLIENT | CF_ARCHIVE, "r_waterwarp", "1", "warp view while underwater"};
+#ifdef USE_GLES2
+cvar_t gl_polyblend = {CF_CLIENT | CF_ARCHIVE, "gl_polyblend", "0", "tints view while underwater, hurt, etc"};
+#else
cvar_t gl_polyblend = {CF_CLIENT | CF_ARCHIVE, "gl_polyblend", "1", "tints view while underwater, hurt, etc"};
+#endif
cvar_t v_flipped = {CF_CLIENT, "v_flipped", "0", "mirror the screen (poor man's left handed mode)"};
qbool v_flipped_state = false;
case RENDERPATH_GLES2:
CHECKGLERROR
qglGenFramebuffers(1, (GLuint*)&temp);CHECKGLERROR
- R_Mesh_SetRenderTargets(temp, NULL, NULL, NULL, NULL, NULL);
+
+#ifndef USE_GLES2
+ R_Mesh_SetRenderTargets(temp, NULL, NULL, NULL, NULL, NULL); // This breaks GLES2.
// GL_ARB_framebuffer_object (GL3-class hardware) - depth stencil attachment
+#endif
+
#ifdef USE_GLES2
// FIXME: separate stencil attachment on GLES
if (depthtexture && depthtexture->texnum ) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT , depthtexture->gltexturetypeenum , depthtexture->texnum , 0);CHECKGLERROR }
if (depthtexture->glisdepthstencil) { qglFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT , GL_RENDERBUFFER, depthtexture->renderbuffernum );CHECKGLERROR }
}
#endif
+
if (colortexture && colortexture->texnum ) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 , colortexture->gltexturetypeenum , colortexture->texnum , 0);CHECKGLERROR }
if (colortexture2 && colortexture2->texnum) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1 , colortexture2->gltexturetypeenum, colortexture2->texnum, 0);CHECKGLERROR }
if (colortexture3 && colortexture3->texnum) { qglFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2 , colortexture3->gltexturetypeenum, colortexture3->texnum, 0);CHECKGLERROR }
Key_WriteBindings (f);
Cvar_WriteVariables (&cvars_all, f);
-
+#ifdef __EMSCRIPTEN__
+ js_syncFS(false);
+#endif
FS_Close (f);
}
}
Host_Init
====================
*/
-static void Host_Init (void)
+void Host_Init (void)
{
int i;
char vabuf[1024];
host.state = host_init;
+ host.realtime = 0;
+ host.dirtytime = Sys_DirtyTime();
+
if (setjmp(host.abortframe)) // Huh?!
Sys_Error("Engine initialization failed. Check the console (if available) for additional information.\n");
===============
Host_Shutdown
-Cleanly shuts down after the main loop exits.
+Cleanly shuts down, Host_Frame() must not be called again after this.
===============
*/
-static void Host_Shutdown(void)
+void Host_Shutdown(void)
{
if (Sys_CheckParm("-profilegameonly"))
Sys_AllowProfiling(false);
Runs all active servers
==================
*/
-static double Host_Frame(double time)
+double Host_Frame(double time)
{
double cl_wait, sv_wait;
else
return min(cl_wait, sv_wait); // listen server or singleplayer
}
-
-// Cloudwalk: Most overpowered function declaration...
-static inline double Host_UpdateTime (double newtime, double oldtime)
-{
- double time = newtime - oldtime;
-
- if (time < 0)
- {
- // warn if it's significant
- if (time < -0.01)
- Con_Printf(CON_WARN "Host_UpdateTime: time stepped backwards (went from %f to %f, difference %f)\n", oldtime, newtime, time);
- time = 0;
- }
- else if (time >= 1800)
- {
- Con_Printf(CON_WARN "Host_UpdateTime: time stepped forward (went from %f to %f, difference %f)\n", oldtime, newtime, time);
- time = 0;
- }
-
- return time;
-}
-
-void Host_Main(void)
-{
- double time, oldtime, sleeptime;
-
- Host_Init(); // Start!
-
- host.realtime = 0;
- oldtime = Sys_DirtyTime();
-
- // Main event loop
- while(host.state < host_shutdown) // see Sys_HandleCrash() comments
- {
- // Something bad happened, or the server disconnected
- if (setjmp(host.abortframe))
- {
- host.state = host_active; // In case we were loading
- continue;
- }
-
- host.dirtytime = Sys_DirtyTime();
- host.realtime += time = Host_UpdateTime(host.dirtytime, oldtime);
- oldtime = host.dirtytime;
-
- sleeptime = Host_Frame(time);
- ++host.framecount;
- sleeptime -= Sys_DirtyTime() - host.dirtytime; // execution time
- host.sleeptime = Sys_Sleep(sleeptime);
- }
-
- Host_Shutdown();
-}
} host_static_t;
extern host_static_t host;
-
-void Host_Main(void);
void Host_Error(const char *error, ...) DP_FUNC_PRINTF(1) DP_FUNC_NORETURN;
void Host_UpdateVersion(void);
void Host_LockSession(void);
void Host_UnlockSession(void);
void Host_AbortCurrentFrame(void) DP_FUNC_NORETURN;
void Host_SaveConfig(const char *file);
+void Host_Init(void);
+double Host_Frame(double time);
+void Host_Shutdown(void);
#endif
DP_LINK_XMP?=dlopen
endif
+ifeq ($(DP_MAKE_TARGET), wasm)
+ MAKE=emmake make
+# CFLAGS_EXTRA+=--use-port=sdl2 \
+# --use-port=libpng \
+# --use-port=libjpeg \
+# --use-port=zlib \
+# -DNOSUPPORTIPV6 \
+# -DUSE_GLES2
+ CFLAGS_EXTRA+=-s USE_SDL=2 \
+ -s USE_LIBPNG=1 \
+ -s USE_LIBJPEG=1 \
+ -s USE_ZLIB=1 \
+ -DNOSUPPORTIPV6 \
+ -DUSE_GLES2
+
+ SDLCONFIG_CFLAGS=$(SDLCONFIG_UNIXCFLAGS) $(SDLCONFIG_UNIXCFLAGS_X11)
+ SDLCONFIG_LIBS=$(SDLCONFIG_UNIXLIBS) $(SDLCONFIG_UNIXLIBS_X11)
+ SDLCONFIG_STATICLIBS=$(SDLCONFIG_UNIXSTATICLIBS) $(SDLCONFIG_UNIXSTATICLIBS_X11)
+ DP_SSE=0
+
+ DP_LINK_SDL?=shared
+ DP_LINK_ZLIB?=shared
+ DP_LINK_JPEG?=dlopen
+ DP_LINK_ODE?=
+ DP_LINK_CRYPTO?=dlopen
+ DP_LINK_CRYPTO_RIJNDAEL?=dlopen
+ DP_LINK_XMP?=dlopen
+endif
+
# Mac OS X configuration
ifeq ($(DP_MAKE_TARGET), macosx)
OBJ_ICON=
# built to give the executable a proper date string
OBJ_SV= builddate.c sys_null.o vid_null.o thread_null.o $(OBJ_SND_NULL) $(OBJ_COMMON)
OBJ_SDL= builddate.c sys_sdl.o vid_sdl.o thread_sdl.o $(OBJ_MENU) $(OBJ_SND_COMMON) $(OBJ_SND_XMP) snd_sdl.o $(OBJ_VIDEO_CAPTURE) $(OBJ_COMMON)
-
+OBJ_WASM= builddate.c sys_wasm.o vid_sdl.o thread_sdl.o $(OBJ_MENU) $(OBJ_SND_COMMON) $(OBJ_SND_XMP) snd_sdl.o $(OBJ_VIDEO_CAPTURE) $(OBJ_COMMON)
# Compilation
# -D_POSIX_C_SOURCE=200809L doesn't enable all of POSIX 2008, wtf?
EXE_UNIXSDL=darkplaces-sdl
EXE_UNIXSVNEXUIZ=nexuiz-dedicated
EXE_UNIXSDLNEXUIZ=nexuiz-sdl
+EXE_WASMJS=darkplaces-wasm.js
+EXE_WASM=darkplaces-wasm.html
CMD_UNIXRM=rm -rf
CMD_UNIXCP=cp -f
LDFLAGS_LINUXSDL=$(LDFLAGS_UNIXCOMMON) -lrt -ldl -rdynamic $(LDFLAGS_UNIXSDL)
+##### WASM specific variables #####
+
+LDFLAGS_WASMJS=$(LDFLAGS_UNIXCOMMON) -s USE_SDL=2 \
+ -s USE_LIBPNG=1 \
+ -s USE_LIBJPEG=1 \
+ -s USE_ZLIB=1 \
+ -s INITIAL_MEMORY=128mb \
+ -s MAXIMUM_MEMORY=1gb \
+ -s SINGLE_FILE \
+ -s FULL_ES3 \
+ -s MIN_WEBGL_VERSION=2 \
+ -s MAX_WEBGL_VERSION=2 \
+ -s ALLOW_MEMORY_GROWTH=1 \
+ -s ASSERTIONS=2 \
+ -s TOTAL_STACK=32mb \
+ -DUSE_GLES2 \
+ -lidbfs.js \
+ --pre-js ../../../wasm/pre.js \
+ -s EXPORTED_RUNTIME_METHODS=callMain,addRunDependency,removeRunDependency
+
+LDFLAGS_WASM=$(LDFLAGS_UNIXCOMMON) -s USE_SDL=2 \
+ -s USE_LIBPNG=1 \
+ -s USE_LIBJPEG=1 \
+ -s USE_ZLIB=1 \
+ -s INITIAL_MEMORY=128mb \
+ -s MAXIMUM_MEMORY=1gb \
+ -s SINGLE_FILE \
+ -s FULL_ES3 \
+ -s MIN_WEBGL_VERSION=2 \
+ -s MAX_WEBGL_VERSION=2 \
+ -s ALLOW_MEMORY_GROWTH=1 \
+ -s ASSERTIONS=2 \
+ -s TOTAL_STACK=32mb \
+ -DUSE_GLES2 \
+ -lidbfs.js \
+ --pre-js ../../../wasm/standaloneprejs.js \
+ -s EXPORTED_RUNTIME_METHODS=callMain,addRunDependency,removeRunDependency \
+ --shell-file ../../../wasm/standalone-shell.html \
+ --embed-file ../../../wasm/preload@/preload
+
+
##### Mac OS X specific variables #####
# Link
.PHONY : clean clean-profile help \
debug profile release \
sv-debug sv-profile sv-release \
- sdl-debug sdl-profile sdl-release
+ sdl-debug sdl-profile sdl-release emscripten-release
help:
@echo
@echo "* $(MAKE) sdl-release : make SDL client (release version)"
@echo "* $(MAKE) sdl-nexuiz : make SDL client with nexuiz icon (release version)"
@echo
+ @echo "* $(MAKE) emscripten-release : make WASM client (release version)"
+ @echo "* $(MAKE) emscripten-standalone: make standalone WASM client (release version)"
+ @echo
debug :
$(MAKE) $(TARGETS_DEBUG)
DP_MAKE_TARGET=$(DP_MAKE_TARGET) \
EXE='$(EXE_SDL)' CFLAGS_FEATURES='$(CFLAGS_CLIENT)' CFLAGS_SDL='$(SDLCONFIG_CFLAGS)' LDFLAGS_COMMON='$(LDFLAGS_SDL)' LEVEL=1
+emscripten-release :
+ $(MAKE) wasm-release \
+ DP_MAKE_TARGET="wasm" \
+ EXE='$(EXE_WASMJS)' \
+ CFLAGS_FEATURES='$(CFLAGS_CLIENT)' \
+ CFLAGS_SDL='$(SDLCONFIG_CFLAGS)' \
+ LDFLAGS_COMMON='$(LDFLAGS_WASMJS)' \
+ LEVEL=1
+
+emscripten-standalone :
+ $(MAKE) wasm-release \
+ DP_MAKE_TARGET="wasm" \
+ EXE='$(EXE_WASM)' \
+ CFLAGS_FEATURES='$(CFLAGS_CLIENT) -DWASM_USER_ADJUSTABLE' \
+ CFLAGS_SDL='$(SDLCONFIG_CFLAGS)' \
+ LDFLAGS_COMMON='$(LDFLAGS_WASM)' \
+ LEVEL=1
+
sdl-release-profile :
$(MAKE) bin-release-profile \
DP_MAKE_TARGET=$(DP_MAKE_TARGET) \
LDFLAGS='$(LDFLAGS_RELEASE) $(LDFLAGS_COMMON)' LEVEL=2
$(STRIP) $(EXE)
+wasm-release :
+ $(CHECKLEVEL1)
+ @echo
+ @echo '========== $(EXE) (release) =========='
+ $(MAKE) prepare BUILD_DIR=build-obj/release/$(EXE)
+ $(MAKE) -C build-obj/release/$(EXE) $(EXE) \
+ DP_MAKE_TARGET=$(DP_MAKE_TARGET) \
+ CFLAGS='$(CFLAGS_COMMON) $(CFLAGS_FEATURES) $(CFLAGS_EXTRA) $(CFLAGS_RELEASE) $(OPTIM_RELEASE)'\
+ LDFLAGS='$(LDFLAGS_RELEASE) $(LDFLAGS_COMMON)' LEVEL=2
+
prepare :
$(CMD_MKDIR) $(BUILD_DIR)
$(CMD_CP) makefile.inc $(BUILD_DIR)/
$(CHECKLEVEL2)
$(DO_LD)
+$(EXE_WASM): $(OBJ_WASM) $(OBJ_ICON)
+ $(CHECKLEVEL2)
+ $(DO_LD)
+
+$(EXE_WASMJS): $(OBJ_WASM) $(OBJ_ICON)
+ $(CHECKLEVEL2)
+ $(DO_LD)
+
+
$(EXE_SVNEXUIZ): $(OBJ_SV) $(OBJ_ICON_NEXUIZ)
$(CHECKLEVEL2)
$(DO_LD)
clean:
-$(CMD_RM) $(EXE_SV)
-$(CMD_RM) $(EXE_SDL)
+ -$(CMD_RM) $(EXE_WASM)
+ -$(CMD_RM) $(EXE_WASMJS)
-$(CMD_RM) $(EXE_SVNEXUIZ)
-$(CMD_RM) $(EXE_SDLNEXUIZ)
-$(CMD_RM) *.o
"\n",
"invariant gl_Position; // fix for lighting polygons not matching base surface\n",
"# endif\n",
+#ifdef USE_GLES2
+"precision highp float;\n",
+#endif
"#if defined(GLSL130) || defined(GLSL140)\n",
"precision highp float;\n",
"# ifdef VERTEX_SHADER\n",
#include "quakedef.h"
#include "prvm_cmds.h"
+#include "sys.h"
/*
===============================================================================
#endif
FS_Close (f);
+#ifdef __EMSCRIPTEN__
+ js_syncFS(false);
+#endif
Con_Print("done.\n");
}
#endif
# define DP_MOBILETOUCH 1
# define DP_FREETYPE_STATIC 1
+#elif defined(__EMSCRIPTEN__) //this also defines linux, so it must come first
+# define DP_OS_NAME "Browser"
+# define DP_OS_STR "browser"
+# define DP_ARCH_STR "WASM-32"
#elif defined(__linux__)
# define DP_OS_NAME "Linux"
# define DP_OS_STR "linux"
char *Sys_SDL_GetClipboardData (void);
+#ifdef __EMSCRIPTEN__ //WASM-specific functions
+bool js_syncFS (bool x);
+void Sys_EM_Register_Commands(void);
+#endif
+
extern qbool sys_supportsdlgetticks;
unsigned int Sys_SDL_GetTicks (void); // wrapper to call SDL_GetTicks
void Sys_SDL_Delay (unsigned int milliseconds); // wrapper to call SDL_Delay
#endif
}
+// Cloudwalk: Most overpowered function declaration...
+static inline double Sys_UpdateTime (double newtime, double oldtime)
+{
+ double time = newtime - oldtime;
+
+ if (time < 0)
+ {
+ // warn if it's significant
+ if (time < -0.01)
+ Con_Printf(CON_WARN "Host_UpdateTime: time stepped backwards (went from %f to %f, difference %f)\n", oldtime, newtime, time);
+ time = 0;
+ }
+ else if (time >= 1800)
+ {
+ Con_Printf(CON_WARN "Host_UpdateTime: time stepped forward (went from %f to %f, difference %f)\n", oldtime, newtime, time);
+ time = 0;
+ }
+
+ return time;
+}
+
+#ifdef __EMSCRIPTEN__
+ #include <emscripten.h>
+#endif
+/// JS+WebGL doesn't support a main loop, only a function called to run a frame.
+static void Sys_Frame(void)
+{
+ double time, newtime, sleeptime;
+#ifdef __EMSCRIPTEN__
+ static double sleepstarttime = 0;
+ host.sleeptime = Sys_DirtyTime() - sleepstarttime;
+#endif
+
+ if (setjmp(host.abortframe)) // Something bad happened, or the server disconnected
+ host.state = host_active; // In case we were loading
+
+ if (host.state >= host_shutdown) // see Sys_HandleCrash() comments
+ {
+#ifdef __EMSCRIPTEN__
+ emscripten_cancel_main_loop();
+#endif
+#ifdef __ANDROID__
+ Sys_AllowProfiling(false);
+#endif
+ Host_Shutdown();
+ exit(0);
+ }
+
+ newtime = Sys_DirtyTime();
+ host.realtime += time = Sys_UpdateTime(newtime, host.dirtytime);
+ host.dirtytime = newtime;
+
+ sleeptime = Host_Frame(time);
+ ++host.framecount;
+ sleeptime -= Sys_DirtyTime() - host.dirtytime; // execution time
+
+#ifdef __EMSCRIPTEN__
+ // This platform doesn't support a main loop... it will sleep when Sys_Frame() returns.
+ // Not using emscripten_sleep() via Sys_Sleep() because it would cause two sleeps per frame.
+ if (!vid_vsync.integer) // see VID_SetVsync_c()
+ emscripten_set_main_loop_timing(EM_TIMING_SETTIMEOUT, host.restless ? 0 : sleeptime * 1000.0);
+ sleepstarttime = Sys_DirtyTime();
+#else
+ host.sleeptime = Sys_Sleep(sleeptime);
+#endif
+}
+
/** main() but renamed so we can wrap it in sys_sdl.c and sys_null.c
* to avoid needing to include SDL.h in this file (would make the dedicated server require SDL).
* SDL builds need SDL.h in the file where main() is defined because SDL renames and wraps main().
Sys_SetTimerResolution();
#endif
- Host_Main();
-
-#ifdef __ANDROID__
- Sys_AllowProfiling(false);
-#endif
-
-#ifndef WIN32
- fcntl(fileno(stdout), F_SETFL, fcntl(fileno(stdout), F_GETFL, 0) & ~O_NONBLOCK);
- fcntl(fileno(stderr), F_SETFL, fcntl(fileno(stderr), F_GETFL, 0) & ~O_NONBLOCK);
+ Host_Init();
+#ifdef __EMSCRIPTEN__
+ emscripten_set_main_loop(Sys_Frame, 0, true); // doesn't return
+#else
+ while(true)
+ Sys_Frame();
#endif
- fflush(stdout);
- fflush(stderr);
return 0;
}
--- /dev/null
+/*
+ * Include this BEFORE darkplaces.h because it breaks wrapping
+ * _Static_assert. Cloudwalk has no idea how or why so don't ask.
+ */
+#include <SDL.h>
+
+#include "darkplaces.h"
+#include "fs.h"
+#include "vid.h"
+
+#include <emscripten.h>
+#include <emscripten/html5.h>
+#include <string.h>
+
+
+EM_JS(float, js_GetViewportWidth, (void), {
+ return document.documentElement.clientWidth
+});
+EM_JS(float, js_GetViewportHeight, (void), {
+ return document.documentElement.clientHeight
+});
+static EM_BOOL em_on_resize(int etype, const EmscriptenUiEvent *event, void *UData)
+{
+ if(vid_resizable.integer)
+ {
+ Cvar_SetValueQuick(&vid_width, js_GetViewportWidth());
+ Cvar_SetValueQuick(&vid_height, js_GetViewportHeight());
+ Cvar_SetQuick(&vid_fullscreen, "0");
+ }
+ return EM_FALSE;
+}
+
+
+// =======================================================================
+// General routines
+// =======================================================================
+
+#ifdef WASM_USER_ADJUSTABLE
+EM_JS(char *, js_listfiles, (const char *directory), {
+ if(UTF8ToString(directory) == "")
+ {
+ console.log("listing cwd");
+ return stringToNewUTF8(FS.readdir(FS.cwd()).toString());
+ }
+
+ try
+ {
+ return stringToNewUTF8(FS.readdir(UTF8ToString(directory)).toString());
+ }
+ catch (error)
+ {
+ return stringToNewUTF8("directory not found");
+ }
+});
+static void em_listfiles_f(cmd_state_t *cmd)
+{
+ char *output = js_listfiles(Cmd_Argc(cmd) == 2 ? Cmd_Argv(cmd, 1) : "");
+
+ Con_Printf("%s\n", output);
+ free(output);
+}
+
+EM_JS(char *, js_upload, (const char *todirectory), {
+ if (UTF8ToString(todirectory).slice(-1) != "/")
+ {
+ currentname = UTF8ToString(todirectory) + "/";
+ }
+ else
+ {
+ currentname = UTF8ToString(todirectory);
+ }
+
+ file_selector.click();
+ return stringToNewUTF8("Upload started");
+});
+static void em_upload_f(cmd_state_t *cmd)
+{
+ char *output = js_upload(Cmd_Argc(cmd) == 2 ? Cmd_Argv(cmd, 1) : fs_basedir);
+
+ Con_Printf("%s\n", output);
+ free(output);
+}
+
+EM_JS(char *, js_rm, (const char *path), {
+ const mode = FS.lookupPath(UTF8ToString(path)).node.mode;
+
+ if (FS.isFile(mode))
+ {
+ FS.unlink(UTF8ToString(path));
+ return stringToNewUTF8("File removed");
+ }
+
+ return stringToNewUTF8(UTF8ToString(path)+" is not a File.");
+});
+static void em_rm_f(cmd_state_t *cmd)
+{
+ if (Cmd_Argc(cmd) != 2)
+ Con_Printf("No file to remove\n");
+ else
+ {
+ char *output = js_rm(Cmd_Argv(cmd, 1));
+ Con_Printf("%s\n", output);
+ free(output);
+ }
+}
+
+EM_JS(char *, js_rmdir, (const char *path), {
+ const mode = FS.lookupPath(UTF8ToString(path)).node.mode;
+ if (FS.isDir(mode))
+ {
+ try
+ {
+ FS.rmdir(UTF8ToString(path));
+ }
+ catch (error)
+ {
+ return stringToNewUTF8("Unable to remove directory. Is it not empty?");
+ }
+ return stringToNewUTF8("Directory removed");
+ }
+
+ return stringToNewUTF8(UTF8ToString(path)+" is not a directory.");
+});
+static void em_rmdir_f(cmd_state_t *cmd)
+{
+ if (Cmd_Argc(cmd) != 2)
+ Con_Printf("No directory to remove\n");
+ else
+ {
+ char *output = js_rmdir(Cmd_Argv(cmd, 1));
+ Con_Printf("%s\n", output);
+ free(output);
+ }
+}
+
+EM_JS(char *, js_mkd, (const char *path), {
+ try
+ {
+ FS.mkdir(UTF8ToString(path));
+ }
+ catch (error)
+ {
+ return stringToNewUTF8("Unable to create directory. Does it already exist?");
+ }
+ return stringToNewUTF8(UTF8ToString(path)+" directory was created.");
+});
+static void em_mkdir_f(cmd_state_t *cmd)
+{
+ if (Cmd_Argc(cmd) != 2)
+ Con_Printf("No directory to create\n");
+ else
+ {
+ char *output = js_mkd(Cmd_Argv(cmd, 1));
+ Con_Printf("%s\n", output);
+ free(output);
+ }
+}
+
+EM_JS(char *, js_move, (const char *oldpath, const char *newpath), {
+ try
+ {
+ FS.rename(UTF8ToString(oldpath),UTF8ToString(newpath))
+ }
+ catch (error)
+ {
+ return stringToNewUTF8("unable to move.");
+ }
+ return stringToNewUTF8("File Moved");
+});
+static void em_mv_f(cmd_state_t *cmd)
+{
+ if (Cmd_Argc(cmd) != 3)
+ Con_Printf("Nothing to move\n");
+ else
+ {
+ char *output = js_move(Cmd_Argv(cmd,1), Cmd_Argv(cmd,2));
+ Con_Printf("%s\n", output);
+ free(output);
+ }
+}
+
+static void em_wss_f(cmd_state_t *cmd)
+{
+ if (Cmd_Argc(cmd) != 3)
+ Con_Printf("Not Enough Arguments (Expected URL and subprotocol)\n");
+ else
+ {
+ if(strcmp(Cmd_Argv(cmd,2),"binary") == 0 || strcmp(Cmd_Argv(cmd,2),"text") == 0)
+ Con_Printf("Set Websocket URL to %s and subprotocol to %s.\n", Cmd_Argv(cmd,1), Cmd_Argv(cmd,2));
+ else
+ Con_Printf("subprotocol must be either binary or text\n");
+ }
+}
+#endif // WASM_USER_ADJUSTABLE
+
+EM_JS(bool, js_syncFS, (bool populate), {
+ FS.syncfs(populate, function(err) {
+ if(err)
+ {
+ alert("FileSystem Save Error: " + err);
+ return false;
+ }
+
+ alert("Filesystem Saved!");
+ return true;
+ });
+});
+static void em_savefs_f(cmd_state_t *cmd)
+{
+ Con_Printf("Saving Files\n");
+ js_syncFS(false);
+}
+
+void Sys_SDL_Shutdown(void)
+{
+ js_syncFS(false);
+ SDL_Quit();
+}
+
+// Sys_Abort early in startup might screw with automated
+// workflows or something if we show the dialog by default.
+static qbool nocrashdialog = true;
+void Sys_SDL_Dialog(const char *title, const char *string)
+{
+ if(!nocrashdialog)
+ SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, string, NULL);
+}
+
+// In a browser the clipboard can only be read asynchronously
+// doing this efficiently and cleanly requires JSPI
+// enable in makefile.inc with emcc option: -s JSPI
+/* TODO: enable this code when JSPI is enabled by default in browsers
+EM_ASYNC_JS(char *, js_getclipboard, (void),
+{
+ try
+ {
+ const text = await navigator.clipboard.readText();
+ return stringToNewUTF8(text);
+ }
+ catch (err)
+ {
+ return stringToNewUTF8("clipboard error: ", err);
+ }
+}); */
+EM_JS(char *, js_getclipboard, (void), {
+ return stringToNewUTF8("clipboard access requires JSPI which is not currently enabled.");
+});
+char *Sys_SDL_GetClipboardData (void)
+{
+ char *data = NULL;
+ char *cliptext;
+
+ // SDL_GetClipboardText() does nothing in a browser, see above
+ cliptext = js_getclipboard();
+ if (cliptext != NULL) {
+ size_t allocsize;
+ allocsize = min(MAX_INPUTLINE, strlen(cliptext) + 1);
+ data = (char *)Z_Malloc (allocsize);
+ dp_strlcpy (data, cliptext, allocsize);
+ free(cliptext);
+ }
+
+ return data;
+}
+
+void Sys_SDL_Init(void)
+{
+ if (SDL_Init(0) < 0)
+ Sys_Error("SDL_Init failed: %s\n", SDL_GetError());
+
+ // we don't know which systems we'll want to init, yet...
+ // COMMANDLINEOPTION: sdl: -nocrashdialog disables "Engine Error" crash dialog boxes
+ if(!Sys_CheckParm("-nocrashdialog"))
+ nocrashdialog = false;
+
+ emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, EM_FALSE, em_on_resize);
+}
+
+void Sys_EM_Register_Commands(void)
+{
+#ifdef WASM_USER_ADJUSTABLE
+ Cmd_AddCommand(CF_SHARED, "em_ls", em_listfiles_f, "Lists Files in specified directory defaulting to the current working directory (Emscripten Only)");
+ Cmd_AddCommand(CF_SHARED, "em_upload", em_upload_f, "Upload file to specified directory defaulting to basedir (Emscripten Only)");
+ Cmd_AddCommand(CF_SHARED, "em_rm", em_rm_f, "Remove a file from game Filesystem (Emscripten Only)");
+ Cmd_AddCommand(CF_SHARED, "em_rmdir", em_rmdir_f, "Remove a directory from game Filesystem (Emscripten Only)");
+ Cmd_AddCommand(CF_SHARED, "em_mkdir", em_mkdir_f, "Make a directory in game Filesystem (Emscripten Only)");
+ Cmd_AddCommand(CF_SHARED, "em_mv", em_mv_f, "Rename or Move an item in game Filesystem (Emscripten only)");
+ Cmd_AddCommand(CF_SHARED, "em_wss", em_wss_f, "Set Websocket URL and Protocol (Emscripten Only)");
+#endif
+ Cmd_AddCommand(CF_SHARED, "em_save", em_savefs_f, "Save file changes to browser (Emscripten Only)");
+}
+
+qbool sys_supportsdlgetticks = true;
+unsigned int Sys_SDL_GetTicks(void)
+{
+ return SDL_GetTicks();
+}
+
+void Sys_SDL_Delay(unsigned int milliseconds)
+{
+ SDL_Delay(milliseconds);
+}
+
+int main(int argc, char *argv[])
+{
+ return Sys_Main(argc, argv);
+}
return;
*/
+ // __EMSCRIPTEN__ SDL_GL_SetSwapInterval() calls emscripten_set_main_loop_timing()
if (SDL_GL_SetSwapInterval(vsyncwanted) >= 0)
Con_DPrintf("Vsync %s\n", vsyncwanted ? "activated" : "deactivated");
else
{
int windowflags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL;
int i;
-#ifndef USE_GLES2
// SDL usually knows best
const char *drivername = NULL;
-#endif
// video display selection (multi-monitor)
Cvar_SetValueQuick(&vid_info_displaycount, SDL_GetNumVideoDisplays());
--- /dev/null
+cl_particles_quake "1"
+
--- /dev/null
+<!DOCTYPE html>
+<html lang="">
+ <head>
+ <meta charset="UTF-8" />
+ <title>DarkPlaces Quake WASM</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="description" content="" />
+ <link rel="icon" href="favicon.png">
+ <style>
+ html, body {
+ margin: 0;
+ padding: 0;
+ }
+ </style>
+ </head>
+ <body>
+ <canvas id="canvas" oncontextmenu="event.preventDefault()" style="width: auto; height: auto;"></canvas>
+ <script type='text/javascript'>
+ var Module = {
+ canvas: (function() { return document.getElementById('canvas'); })(),
+ files: {
+ "id1/pak0.pak": "pak0.pak",
+ "id1/pak1.pak": "pak1.pak",
+ "id1/autoexec.cfg": "autoexec.cfg"
+ },
+ arguments: ['-game', 'id1', '+crosshair', '1'] // Arguments appended to the default arguments, can be used to change the gamedir
+ };
+ </script>
+ <script type="text/javascript" src="darkplaces-wasm.js"></script>
+ </body>
+</html>
--- /dev/null
+if (!Object.hasOwn(Module, 'arguments')) {
+ Module['arguments'] = ['-basedir', '/game'];
+}
+else {
+ Module['arguments'] = ['-basedir', '/game'].concat(Module['arguments']);
+}
+
+Module['print'] = function(text) {
+ console.log(text);
+}
+
+Module['printErr'] = function(text) {
+ console.error(text);
+}
+
+
+Module['preRun'] = [
+ function()
+ {
+ function stdin(){
+ return '\n'; // Return a newline/line feed character so the user is not prompted for input
+ };
+ FS.init(stdin, null, null); // null for both stdout and stderr
+
+ function createParentDirectory(filePath) {
+ //
+ // Split a filePath into parts, create the directory hierarchy
+ //
+ parts = filePath.split('/');
+ for (let i = parts.length - 1; i > 0; i--) {
+ localDir = '/game/' + parts.slice(0, -i).join('/')
+ try {
+ FS.mkdir(localDir);
+ }
+ catch {
+ // Directory already exists
+ }
+ }
+ }
+
+ function startDownload(localPath, remotePath) {
+ //
+ // Return a promise of a file download
+ //
+ Module['addRunDependency'](localPath); // Tell Emscripten about the dependency
+
+ return fetch(remotePath)
+ .then(response => {
+ return response.arrayBuffer();
+ })
+ .then(arrayBuffer => {
+ const buffer = new Uint8Array(arrayBuffer);
+ stream = FS.open("/game/" + localPath, "w");
+ FS.write(stream, buffer, 0, buffer.byteLength);
+ FS.close(stream);
+ console.log("Downloaded " + localPath);
+ Module['removeRunDependency'](localPath); // Tells Emscripten we've finished the download
+ });
+ }
+
+ function createBaseDir() {
+ //
+ // Creates the Quake basedir and mounts it to IDBFS
+ //
+ FS.mkdir('/game');
+ //mounts IDBFS to where the game would save
+ FS.mount(IDBFS, {}, '/home/web_user/');
+ }
+
+ function downloadGameFiles() {
+ //
+ // Download files specified in the Module.files object
+ //
+ createBaseDir();
+
+ let downloads = [];
+ for (const [localPath, remotePath] of Object.entries(Module.files)) {
+ console.log("Downloading " + remotePath + " to " + localPath);
+
+ createParentDirectory(localPath);
+
+ downloads.push(
+ startDownload(localPath, remotePath)
+ );
+ }
+
+ // Wait for downloads to finish, sync the filesystem, start the game
+ Promise.all(downloads)
+ .then(function(results) {
+ FS.syncfs(true, function (err) {
+ assert(!err);
+ Module.callMain(Module.arguments);
+ });
+ });
+ }
+
+ downloadGameFiles();
+ }
+];
+
+Module['noInitialRun'] = true;
--- /dev/null
+<!DOCTYPE html>
+<html>
+<!-- Thank You Stack Overflow! -->
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8, width=device-width, initial-scale=1">
+ <title>DarkPlaces</title>
+</head>
+
+<body style="margin:0;padding:0">
+
+ <!-- Create the canvas that the C++ code will draw into -->
+ <canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
+
+ <!-- Allow the C++ to access the canvas element -->
+ <script type='text/javascript'>
+ var Module = {
+ canvas: (function() { return document.getElementById('canvas'); })()
+ };
+ Module['preRun'] = []
+ </script>
+
+ <script type='text/javascript'>
+ const file_reader = new FileReader();
+ file_reader.addEventListener("load", readf);
+ function readf(event)
+ {
+ // also heavily derivative of Riot's code on Stack Overflow cause I sure as hell don't understand it.
+ // Riot used the MIT license.
+ const uint8Arr = new Uint8Array(file_reader.result);
+ console.log(currentname+fname);
+ try
+ {
+ stream = FS.open(currentname+fname, 'w');
+ }
+ catch (error)
+ {
+ alert(error.toString() + "... Was that not a directory?");
+ return;
+ }
+
+ FS.write(stream, uint8Arr, 0, uint8Arr.length, 0);
+ FS.close(stream);
+ alert("File Uploaded");
+ }
+
+ var currentname = "";
+ var fname = ""
+
+ function save_files()
+ {
+ fname = this.files[0].name;
+ file_reader.readAsArrayBuffer(this.files[0]);
+ };
+
+ var file_selector = document.createElement('input');
+ file_selector.setAttribute('type', 'file');
+ file_selector.addEventListener("change", save_files, false);
+ </script>
+
+ <!-- Where the script shall be -->
+ {{{ SCRIPT }}}
+
+</body>
+
+</html>
--- /dev/null
+//current command in ascii decimal
+let currentcmd = [0,0,0]
+let currentfile = "";
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+Module['print'] = function(text) { console.log(text); }
+
+Module['preRun'] = function()
+{
+ function stdin() { return 10 };
+ var stdout = null;
+ var stderr = null;
+ FS.init(stdin, stdout, stderr);
+ FS.mount(IDBFS, {}, "/home/web_user/");
+ FS.symlink("/home/web_user", "/save");
+}
+
+Module['noInitialRun'] = true
+
+document.addEventListener('click', (ev) => {
+ console.log("event is captured only once.");
+ args = []
+ if(window.location.href.indexOf("file://") > -1)
+ {
+ try
+ {
+ args = args.concat(prompt("Enter command line arguments").split(" "))
+ }
+ catch (error)
+ {
+ console.log("Error: ", error);
+ console.log("Failed to concat extra arguments (likely passed nothing for the argument)")
+ }
+ }
+ else
+ {
+ parms = new URLSearchParams(window.location.search);
+ try
+ {
+ args = args.concat(parms.get("args").split(" "))
+ }
+ catch (error)
+ {
+ console.log("Error: ", error);
+ console.log("Failed to concat extra arguments (likely passed nothing for the argument)")
+ }
+ }
+
+ FS.syncfs(true, function()
+ {
+ if(FS.analyzePath("/preload/runhere").exists)
+ {
+ FS.symlink("/preload", "/home/web_user/games");
+ args = args.concat(["-basedir", "/home/web_user/games"])
+ }
+ else
+ {
+ args = args.concat(["-basedir", "/home/web_user/"])
+ }
+
+ Module.callMain(args);
+ });
+}, { once: true });