From effee0e45a3be0b5481734bce6a3516f24a6cd6a Mon Sep 17 00:00:00 2001 From: Shpoike Date: Thu, 20 Jun 2024 18:44:32 +0100 Subject: [PATCH] Make the web port's local storage stuff more visible. Fix web port's touchscreen inputs. --- engine/web/ftejslib.js | 8 +- engine/web/fteshell.html | 174 ++++++++++++++++++++++++++++++++++++--- engine/web/gl_vidweb.c | 22 ++--- engine/web/prejs.js | 25 ++++-- 4 files changed, 195 insertions(+), 34 deletions(-) diff --git a/engine/web/ftejslib.js b/engine/web/ftejslib.js index c40d5e77c..90f51dc8e 100644 --- a/engine/web/ftejslib.js +++ b/engine/web/ftejslib.js @@ -347,7 +347,7 @@ mergeInto(LibraryManager.library, { if (event.type == 'touchstart') {{{makeDynCall('viii','FTEC.evcb.button')}}}(t.identifier+1, 1, -1); - else if (event.type != 'touchmove') + else if (event.type != 'touchmove') //cancel/end/leave... {{{makeDynCall('viii','FTEC.evcb.button')}}}(t.identifier+1, 0, -1); } } @@ -459,7 +459,7 @@ mergeInto(LibraryManager.library, const b = gp.buttons[j]; let p; if (typeof(b) == "object") - p = b.pressed; //.value is a fractional thing. oh well. + p = b.pressed || (b.value > 0.5); //.value is a fractional thing. oh well. else p = b > 0.5; //old chrome bug @@ -596,7 +596,7 @@ mergeInto(LibraryManager.library, 'focus', 'blur']; //try to fix alt-tab events.forEach(function(event) { - Module['canvas'].addEventListener(event, FTEC.handleevent, true); + Module['canvas'].addEventListener(event, FTEC.handleevent, {capture:true, passive:false}); }); var docevents = ['keypress', 'keydown', 'keyup', @@ -894,6 +894,8 @@ mergeInto(LibraryManager.library, } else if (Module["close"]) Module["close"](); + else if (Module["quiturl"]) + document.location.replace(Module["quiturl"]); else { try diff --git a/engine/web/fteshell.html b/engine/web/fteshell.html index 0c5e77558..ac2c5bba6 100644 --- a/engine/web/fteshell.html +++ b/engine/web/fteshell.html @@ -17,7 +17,7 @@
Please allow/unblock our javascript to play.
- +
@@ -46,7 +46,20 @@ var Module = { //string values are deemed to be URLs, so use the str2ab("") helper if you want to embed raw file data instead. // "default.fmf": "default.fmf", // "id1/pak0.pak": "pak0.pak", - }, +// "id1/default.cfg": "webdefaults.cfg", //autoexec.cfg is evil. +/* "id1/touch.cfg": str2ab("showpic_removeall\n" + "showpic touch_moveforward.tga fwd -128 -112 bm 32 32 +forward 5\n" + "showpic touch_moveback.tga back -128 -80 bm 32 32 +back 5\n" + "showpic touch_moveleft.tga left -160 -88 bm 32 32 +moveleft 5\n" + "showpic touch_moveright.tga rght -96 -88 bm 32 32 +moveright 5\n" + "showpic touch_attack.tga fire -160 -160 bm 32 32 +attack 5\n" + "showpic touch_jump.tga jump 128 -80 bm 32 32 +jump 5\n" + "showpic touch_weapons.tga weap 80 -80 bm 32 32 +weaponwheel 5\n" + "showpic touch_menu.tga menu -32 0 tr 32 32 togglemenu 10\n"), +*/ }, +// quiturl: "/", //url to jump to when 'quitting' (otherwise uses history.back). +// arguments:["+alias","f_startup","connect","wss://theservertojoin", "-manifest","default.fmf"], //beware the scheme registration stuff (pwa+js methods). +// manifest: "index.html.fmf", // '-manifest' arg if args are not explicit. also inhibits the #foo.fmf thing. print: function(msg) { //stdout... console.log(msg); @@ -55,7 +68,6 @@ var Module = { { //stderr... console.log(text); }, -// arguments:["wss://theservertojoin", "-manifest", "default.fmf"], //beware the scheme registration stuff (pwa+js methods). canvas: document.getElementById('canvas'), //for webgl to attach to setStatus: function(text) { //gets spammed some prints during startup. blame emscripten. @@ -130,6 +142,88 @@ function fixupfilepath(fname, path) return "id1/maps/" + fname; //bsps get their own extra subdir return "id1/" + fname; //probably a pak. maybe a cfg, no idea really. } +function remfile(fname) +{ + delete Module.files[fname]; + showfiles(); //repaint +} +function renamefile(fname) +{ + const nname = prompt("Please enter new name", fname); + if (nname != null) + { + Module.files[nname] = Module.files[fname]; + delete Module.files[fname]; + showfiles(); + } +} +function remcfile(fname) +{ + Module['cache'].delete("/_/"+fname); + Module['cache'].keys().then((keys)=>{Module['cachekeys'] = keys; showfiles();}); //repaint +} +function remlfile(fname) +{ + window.localStorage.removeItem(fname); + showfiles(); //repaint +} +function savelfile(fname) +{ + window.showSaveFilePicker({id: "openfile", startIn: "documents", suggestedName: fname}) + .then(async (h)=>{ + let data = await window.localStorage.getitem(fname); + let f = await h.createWriteable(); + await f.write(data); + await f.close(); + }); +} +function adduserfile() +{ + window.showOpenFilePicker( + { types:[ + { + description: "Packages", + accept:{"text/*":[".pk3", ".pak", ".pk4", ".zip"]} + }, + { + description: "Maps", + accept:{"text/*":[".bsp.gz", ".bsp", ".map"]} + }, + { + description: "Demos", + accept:{"application/*":[".mvd.gz", ".qwd.gz", ".dem.gz", ".mvd", ".qwd", ".dem"]} //dm2? + }, + { + description: "FTE Manifest", + accept:{"text/*":[".fmf"]} + }, + //model formats?... nah, too many/weird. they can always + //audio formats? eww + //image formats? double eww! + { + description: "Configs", + accept:{"text/*":[".cfg", ".rc"]} + }], + excludeAcceptAllOption:false, //let em pick anything. we actually support more than listed here (and bitrot...) + id:"openfile", //remember the dir we were in for the next invocation + multiple:true + }) + .then((r)=> + { + let gamedir = prompt("Please enter gamedir", "id1"); + if (gamedir != "") + gamedir = gamedir+"/"; + for (let i of r) + { + i.getFile().then((f)=> + { + var n = fixupfilepath(f.name, gamedir); + Module.files[n]=f.arrayBuffer(); //actually a promise... + Module.files[n].then(buf=>{Module.files[n]=buf;showfiles();}); //try and resolve it now. + }); + } + }).catch((e)=>{console.log("showOpenFilePicker() aborted", e);}); +} function showfiles() { //print the pending file list in some pretty way if (Module.began) @@ -137,31 +231,78 @@ function showfiles() Module.setStatus(''); document.getElementById('dropzone').hidden = false; document.getElementById('begin').hidden = false; - var nt = "Drag gamedirs or individual package files here to make them available!
Active Files:
";
-	var keys = Object.keys(Module.files);
-	for(var i = 0; i < keys.length; i++)
+	let nt = "

FTE Engine (Browser Port)

"; + nt = nt + "Drag gamedirs or individual package files here to make them available!
";
+	let keys = Object.keys(Module.files);
+	nt += "Session Files ("+keys.length+"):
"; + for(let i = 0; i < keys.length; i++) { + let rem = " [forget]" + + " [rename]"; if (Module.files[keys[i]] instanceof ArrayBuffer) { - var sz = Module.files[keys[i]].byteLength; + let sz = Module.files[keys[i]].byteLength; if (sz > 512*1024) sz = (sz / (1024*1024)) + "mb"; else if (sz > 512) sz = (sz / 1024) + "kb"; else sz = (sz) + " bytes"; - nt += " " + keys[i] + " ("+sz+")
"; + nt += " " + keys[i] + " ("+sz+")"+rem+"
"; } else - nt += " " + keys[i] + "
"; + nt += " " + keys[i] + rem + "
"; } - nt += "
("+keys.length+" files)"; + + //cache is for large data files. for any pk3s the user might add in-engine. large stuff that's easy to fix if it gets wiped. + const cache = Module['cache']; + const ckeys = Module['cachekeys']; + if (ckeys !== undefined && ckeys.length) + { + nt += "
Cached Files ("+ckeys.length+"):
"; + for(let r of ckeys) + { + const idx = r.url.indexOf("/_/") + if (idx < 0) + continue; //wtf? that entry should not have been in this cache object. + const fn = r.url.substr(idx+3); + let rem = " [forget]"; + nt += " " + fn + rem + "
"; + } + } + + //local storage is used for slightly more persistent things, like user configs and saved games. we have quite limited storage, and this is basically text only. + try + { + const ls = window.localStorage; + if (ls && ls.length) + { + nt += "
Local Files ("+ls.length+"):
"; + for (let i = 0; i < ls.length; i++) + { + const fn = ls.key(i); + const rem = " [forget]" + (window.showSaveFilePicker!==undefined?" [export]":""); + nt += " " + fn + rem + "
"; + } + } + } + catch(e){} + + nt += "
"; + + nt += "

Cookie Disclaimer: This page does not use cookies, however it does use local storage to save configs+games (consistent with natively installed games).
frag-net (our matchmaking service) does not utilise any tracking beyond the session in question, but it does allow connecting to third-party servers which may incorporate ranking systems or accounts or other tracking according to that server's privacy/tracking policies.
" document.getElementById('dropzone').innerHTML = nt; } function scanfiles(item,path) { //for directory drops if (item.isFile) { + if (path=="") + { + path = prompt("Please enter gamedir", "id1"); + if (path != "") + path = path+"/"; + } item.file(function(f) { let n = fixupfilepath(f.name, path); @@ -193,7 +334,7 @@ function gotdrop(ev) else if (ev.dataTransfer.items[i].kind === 'file') { var f = ev.dataTransfer.items[i].getAsFile(); - var n = fixupfilepath(f.name); + var n = fixupfilepath(f.name, ""); Module.files[n]=f.arrayBuffer(); //actually a promise... Module.files[n].then(buf=>{Module.files[n]=buf;showfiles();}); //try and resolve it now. } @@ -201,10 +342,17 @@ function gotdrop(ev) } if (window.showOpenFilePicker) addfile.hidden = false; -if (window.location.hash != "" || Object.keys(Module.files).length) +if (window.location.hash != "" || Module["autostart"]) begin(); //if the url has a #foo.fmf then just begin instantly, else - showfiles(); //otherwise show our lame file dropper and wait for the user to click 'go'. +{ + try { + caches.open('user').then((c)=>{Module['cache']=c;return c.keys();}).then((keys)=>{Module['cachekeys'] = keys; showfiles();}); + } catch(e){ + } finally { + showfiles(); //otherwise show our lame file dropper and wait for the user to click 'go'. + } +} diff --git a/engine/web/gl_vidweb.c b/engine/web/gl_vidweb.c index 679a527d4..31f61c294 100644 --- a/engine/web/gl_vidweb.c +++ b/engine/web/gl_vidweb.c @@ -21,7 +21,7 @@ static struct float repeattime; } gamepaddevices[] = {{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET},{DEVID_UNSET}}; static int keyboardid[] = {0}; -static int mouseid[] = {0}; +static int mouseid[] = {0,1,2,3,4,5,6,7}; static cvar_t *xr_enable; //refrains from starting up when 0 and closes it too, and forces it off too static cvar_t *xr_metresize; @@ -526,7 +526,7 @@ static int DOM_KeyEvent(unsigned int devid, int down, int scan, int uni) } static int RemapTouchId(int id, qboolean final) { - static int touchids[8]; + static int touchids[countof(mouseid)]; int i; if (!id) return id; @@ -535,14 +535,16 @@ static int RemapTouchId(int id, qboolean final) { if (final) touchids[i] = 0; - return i; + return mouseid[i]; } for (i = 1; i < countof(touchids); i++) if (touchids[i] == 0) { if (!final) touchids[i] = id; - return i; + if (mouseid[i] == DEVID_UNSET) + mouseid[i] = i; + return mouseid[i]; } return id; } @@ -554,14 +556,14 @@ static void DOM_ButtonEvent(unsigned int devid, int down, int button) //fixme: the event is a float. we ignore that. while(button < 0) { - IN_KeyEvent(mouseid[devid], true, K_MWHEELUP, 0); - IN_KeyEvent(mouseid[devid], false, K_MWHEELUP, 0); + IN_KeyEvent(devid, true, K_MWHEELUP, 0); + IN_KeyEvent(devid, false, K_MWHEELUP, 0); button += 1; } while(button > 0) { - IN_KeyEvent(mouseid[devid], true, K_MWHEELDOWN, 0); - IN_KeyEvent(mouseid[devid], false, K_MWHEELUP, 0); + IN_KeyEvent(devid, true, K_MWHEELDOWN, 0); + IN_KeyEvent(devid, false, K_MWHEELUP, 0); button -= 1; } } @@ -577,13 +579,13 @@ static void DOM_ButtonEvent(unsigned int devid, int down, int button) button = K_TOUCH; else button += K_MOUSE1; - IN_KeyEvent(mouseid[devid], down, button, 0); + IN_KeyEvent(devid, down, button, 0); } } static void DOM_MouseMove(unsigned int devid, int abs, float x, float y, float z, float size) { devid = RemapTouchId(devid, false); - IN_MouseMove(mouseid[devid], abs, x, y, z, size); + IN_MouseMove(devid, abs, x, y, z, size); } static void DOM_LoadFile(char *loc, char *mime, int handle) diff --git a/engine/web/prejs.js b/engine/web/prejs.js index 5091052fd..306ab54b5 100644 --- a/engine/web/prejs.js +++ b/engine/web/prejs.js @@ -51,6 +51,8 @@ if (typeof Module['files'] !== "undefined" && Object.keys(Module['files']).lengt { Module['loadcachedfiles'](); + Module['curfile'] = undefined; + let files = Module['files']; let names = Object.keys(files); for (let i = 0; i < names.length; i++) @@ -66,6 +68,8 @@ if (typeof Module['files'] !== "undefined" && Object.keys(Module['files']).lengt xhr.open("GET", ab); xhr.onload = function () { + if (curfile == n) + curfile = undefined; if (this.status >= 200 && this.status < 300) { let b = FTEH.h[_emscriptenfte_buf_createfromarraybuf(this.response)]; @@ -78,11 +82,15 @@ if (typeof Module['files'] !== "undefined" && Object.keys(Module['files']).lengt }; xhr.onprogress = function(e) { - if (Module['setStatus']) + if (typeof Module['curfile'] == "undefined") + Module['curfile'] = n; //take it. + if (Module['setStatus'] && curfile==n) Module['setStatus'](n + ' (' + e.loaded + '/' + e.total + ')'); }; xhr.onerror = function () { + if (Module['curfile'] == n) + Module['curfile'] = undefined; removeRunDependency(n); }; xhr.send(); @@ -114,22 +122,23 @@ if (typeof Module['files'] !== "undefined" && Object.keys(Module['files']).lengt } } } -else if (typeof man == "undefined") +else if (!Module['manifest']) { - var man = window.location.protocol + "//" + window.location.host + window.location.pathname; + let man = window.location.protocol + "//" + window.location.host + window.location.pathname; if (man.substr(-1) != '/') man += ".fmf"; else man += "index.fmf"; + Module['manifest'] = man; + + if (window.location.hash != "") + Module['manifest'] = window.location.hash.substring(1); } if (!Module['arguments']) //the html can be explicit about its args if it sets this to an empty array or w/e { Module['arguments'] = []; - if (window.location.hash != "") - man = window.location.hash.substring(1); - // use query string in URL as command line const qstrings = decodeURIComponent(window.location.search.substring(1)); if (qstrings != "") @@ -149,8 +158,8 @@ if (!Module['arguments']) //the html can be explicit about its args if it sets t } } - if (typeof man != "undefined") - Module['arguments'] = Module['arguments'].concat(['-manifest', man]); + if (Module['manifest'] != "") + Module['arguments'] = Module['arguments'].concat(['-manifest', Module['manifest']]); //registerProtocolHandler needs to be able to pass it through to us... so only allow it if we're parsing args from the url. Module['mayregisterscemes'] = true;