fteqw/engine/web/fteshell.html

358 lines
13 KiB
HTML

<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="manifest" href="fte_pwa.json" />
<meta name=viewport content="width=device-width, initial-scale=1">
<title>FTE Engine</title>
<style>
html,body { background-color:#000000; color:#808080; height:100%;width:100%;margin:0;padding:0;}
.emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
div.emscripten { text-align: center; padding:0; margin: 0;}
/* the canvas *must not* have any border or padding, or mouse coords will be wrong */
canvas.emscripten { border: 0px none; width:100%; height:100%; padding:0; margin: 0;}
</style>
</head>
<body ondrop="gotdrop(event);" ondragover="event.preventDefault()">
<div class="emscripten" id="status">Please allow/unblock our javascript to play.</div>
<div id="dropzone" ondrop="gotdrop(event);" ondragover="event.preventDefault()" hidden=1>Drop Zone</div>
<button type="button" onclick="adduserfile()" id="addfile" hidden=1>Add File(s)</button>
<button type="button" onclick="begin()" id="begin" hidden=1>Click To Begin!</button>
<div class="emscripten">
<progress value="0" max="100" id="progress" hidden=1></progress>
</div>
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" hidden=1></canvas>
<script type='text/javascript'>
//set up a service worker, so that we can actually be installed if so, instead of living in the more general browser cache. Yay for running an online game offline...
if ("serviceWorker" in navigator)
navigator.serviceWorker.register("fte_pwa_sw.js", { scope: "./" }).catch((error) => {console.error(`Service worker registration failed: ${error}`);});
function str2ab(str)
{ //helper function. Not utf-8, so stick to ascii chars
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i=0, strLen=str.length; i < strLen; i++)
bufView[i] = str.charCodeAt(i);
return buf;
}
// connect to canvas
var Module = {
files:
{ //these can be arraybuffers(you'll need a helper to define those) or promises(fte will block till they complete), or strings (which will be interpretted as urls and downloaded before any C code is run)
//note that the code below will skip the file-drop prompt if there's any files specified here (or there's a #foo.fmf file specified)
//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);
},
printErr: function(text)
{ //stderr...
console.log(text);
},
canvas: document.getElementById('canvas'), //for webgl to attach to
setStatus: function(text)
{ //gets spammed some prints during startup. blame emscripten.
if (Module.setStatus.interval)
clearInterval(Module.setStatus.interval);
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
if (m) {
text = m[1];
progressElement.value = parseInt(m[2])*100;
progressElement.max = parseInt(m[4])*100;
progressElement.hidden = false;
} else {
progressElement.value = null;
progressElement.max = null;
progressElement.hidden = true;
}
statusElement.innerHTML = text;
statusElement.hidden = text.length==0;
},
// preRun: [],
totalDependencies: 0,
monitorRunDependencies: function(left)
{ //progress is progress...
this.totalDependencies = Math.max(this.totalDependencies, left);
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
},
// onRuntimeInitialized: function(){},
postRun:
[ //each of these are called after main was run. we should have our mainloop set up now
function()
{
if (Module["sched"] === undefined)
{ //our main function failed to set up the main loop. ie: main didn't get called. panic.
alert("Unable to initialise. You may need to restart your browser. If you get this often and inconsistently, consider using a 64bit browser instead.");
Module.setStatus("Initialisation Failure");
}
}
],
};
function begin()
{
if (Module.began)
return;
Module.began = true;
document.getElementById('dropzone').hidden = true;
document.getElementById('addfile').hidden = true;
document.getElementById('begin').hidden = true;
Module.setStatus('Downloading...');
// make a script. do it the hard way for the error.
var s = document.createElement('script');
// set it up
s.setAttribute('src',"ftewebgl.js");
s.setAttribute('type',"text/javascript");
s.setAttribute('charset',"utf-8");
s.addEventListener('error', function() {alert("Oh noes! we got an error!"); Module.setStatus("Unable to download engine javascript");}, false);
// add to DOM
document.head.appendChild(s);
}
//stuff to facilitate our drag+drop filesystem support
function fixupfilepath(fname, path)
{ //we just have a filename, try to guess where to put it.
if (path != "")
return path+fname; //already has a path. use it. this allows people to drag+drop gamedirs.
var ext = fname.substr(fname.lastIndexOf('.') + 1);
if (ext == 'fmf' || ext == 'kpf') //these are the only files that really make sense in the root.
return fname;
if (ext == 'bsp' || ext == 'map' || ext == 'lit' || ext == 'lux')
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)
return;
Module.setStatus('');
document.getElementById('dropzone').hidden = false;
document.getElementById('begin').hidden = false;
let nt = "<H1>FTE Engine (Browser Port)</H1>";
nt = nt + "Drag gamedirs or individual package files here to make them available!<pre>";
let keys = Object.keys(Module.files);
nt += "Session Files ("+keys.length+"):<br/>";
for(let i = 0; i < keys.length; i++)
{
let rem = " <a href=\"javascript:remfile('"+keys[i]+"');\">[forget]</a>" +
" <a href=\"javascript:renamefile('"+keys[i]+"');\">[rename]</a>";
if (Module.files[keys[i]] instanceof ArrayBuffer)
{
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+")"+rem+"<br/>";
}
else
nt += " " + keys[i] + rem + "<br/>";
}
//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 += "<br/>Cached Files ("+ckeys.length+"):<br/>";
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 = " <a href=\"javascript:remcfile('"+fn+"');\">[forget]</a>";
nt += " " + fn + rem + "<br/>";
}
}
//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 += "<br/>Local Files ("+ls.length+"):<br/>";
for (let i = 0; i < ls.length; i++)
{
const fn = ls.key(i);
const rem = " <a href=\"javascript:remlfile('"+fn+"');\">[forget]</a>" + (window.showSaveFilePicker!==undefined?" <a href=\"javascript:savelfile('"+fn+"');\">[export]</a>":"");
nt += " " + fn + rem + "<br/>";
}
}
}
catch(e){}
nt += "</pre>";
nt += "<p/>Cookie Disclaimer:<small> This page does not use cookies, however it does use local storage to save configs+games (consistent with natively installed games).<br/>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.</small>"
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);
Module.files[n]=f.arrayBuffer(); //actually a promise...
Module.files[n].then(buf=>{Module.files[n]=buf;showfiles();}); //try and resolve it now.
});
}
else if (item.isDirectory)
{
// Get folder contents
var dirReader = item.createReader();
dirReader.readEntries(function(entries)
{
for (var i=0; i<entries.length; i++)
scanfiles(entries[i], path + item.name + "/");
});
}
}
function gotdrop(ev)
{ //user drag+dropped something.
ev.preventDefault();
for (var i = 0; i < ev.dataTransfer.items.length; i++)
if (ev.dataTransfer.items[i].webkitGetAsEntry)
{
var d = ev.dataTransfer.items[i].webkitGetAsEntry();
if (d)
scanfiles(d, "");
}
else if (ev.dataTransfer.items[i].kind === 'file')
{
var f = ev.dataTransfer.items[i].getAsFile();
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.
}
showfiles();
}
if (window.showOpenFilePicker)
addfile.hidden = false;
if (window.location.hash != "" || Module["autostart"])
begin(); //if the url has a #foo.fmf then just begin instantly,
else
{
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'.
}
}
</script>
</body>
</html>