Initial commit

This commit is contained in:
Dakedres
2022-10-28 02:48:42 -06:00
commit cf9323beae
59 changed files with 11866 additions and 0 deletions

97
src/App.js Executable file
View File

@@ -0,0 +1,97 @@
const Instance = require('./Instance'),
Launcher = require('./Launcher'),
{ app } = require('./util/constants')
class App {
constructor() {
this.name = app.id
this.categories = app.categories
this.version = app.version
this.sessions = []
this.assets = {}
this.bundle = $bundle.for('/')
let self
// Run acts as a proxy of sorts so we can retain access to
// the app instance as the context
this.exec = function(cfg) {
self.run(cfg, this)
}
const openAsset = (name, path) =>
this.bundle.open(path, 'URL').then(url => {
self.assets[name] = url
})
// Fake filename so we can target it with CSS to work around
// how app icons work
this.icon = 'xash'
// Should probably just load all of "import/"
// but there's not any ls function in abnt
this.init()
this.config = {
pauseOnLostFocus: true
}
self = this
}
async run(cfg, context) {
const self = this,
{ arg, cli } = context
console.log(context)
if(arg.arguments.length > 0) {
let path = $fs.utils.resolvePath(arg.arguments[0])
if($fs.utils.exist(path) !== false) {
this.launch(path)
} else {
cli.log.error(`Could not open path "${path}"`)
}
} else {
new Launcher(this)
}
}
launch(path) {
let sess = new Instance(this, path) //testing path
this.sessions.push(sess)
}
cleanInstances() {
const { sessions } = this
for(let i in sessions)
if(sessions[i].closed)
delete this.sessions[i]
}
killAll() {
for(let session of this.sessions)
session.kill()
}
init() {
this.bundle.open('global.css', 'URL')
.then($loader.css)
const assets = {
"trame": "./half-trame.png",
"play": "./assets/play.svg",
"icon": "./import/icon.png"
}
for(let name in assets) {
this.bundle.open(assets[name], 'URL')
.then(url => document.querySelector(':root').style.setProperty(`--xash3d-${name}`, `url(${url})`) )
}
}
}
module.exports = App

55
src/Console.js Executable file
View File

@@ -0,0 +1,55 @@
class Console extends DocumentFragment {
constructor(onCloseSignal) {
super()
this.container = document.createElement('code')
this.parent = undefined
this.onCloseSignal = onCloseSignal
super.append(this.container)
}
_createLine(lines, classList) {
for(let content of lines) {
if(content == 'exit(0)')
this.onCloseSignal()
let line = document.createElement('div')
if(classList) line.classList = classList
this.container.appendChild(line)
line.innerText = content
}
this._tickParent()
}
_tickParent() {
if(this.parent) {
let { parent } = this
parent.scrollTop = parent.scrollHeight
}
}
log(...lines) {
return this._createLine(lines)
}
error(...lines) {
return this._createLine(lines, 'ui_log__red')
}
attachTo(window) {
let { body } = window.el
body.append(this)
this.parent = body
this._tickParent()
}
unattach() {
this.parent = undefined
}
}
module.exports = Console

166
src/EmulatedIDB.js Normal file
View File

@@ -0,0 +1,166 @@
const localBasePath = '.config/xash/saves',
localExt = '.jso'
const callIfPresent = (func, args, fallback = false) => {
if(typeof func === 'function') {
return func(...args)
} else if(fallback) {
return fallback(...args)
}
}
class EmulatedIDB {
constructor() {
const self = this
const keyOnlyWrapper = original => key =>
original(this.getRemoteKey(key) )
const valueKeyWrapper = original => (item, key) =>
original(item, this.getRemoteKey(key) )
this.objectStoreOverrides = {
get: keyOnlyWrapper,
delete: keyOnlyWrapper,
put: valueKeyWrapper,
add: valueKeyWrapper,
clear: original => () => {
let out = {}
this.getLocalRemoteKeys
.then(keys => Promise.all(
keys.map(key => localforage.removeItem(key))
))
.catch(err => {
callIfPresent(out.onerror, [ err ], err => {
throw err
})
})
.then(() => {
callIfPresent(out.onsuccess, [ {} ])
})
return out
},
index: () => targetEntryKey => {
function openKeyCursor() {
let out = {}
self.getLocalRemoteKeys().then(async remoteKeys => {
let files = await Promise.all(
remoteKeys
.map(async remoteKey => {
let obj = await localforage.getItem(remoteKey)
return {
key: obj[targetEntryKey],
primaryKey: self.getLocalKey(remoteKey)
}
})
)
let index = 0
const nextItem = () => {
callIfPresent(out.onsuccess, [
{ target: { result: makeCursor() } }
])
}
const makeCursor = () => {
if(index < files.length)
return Object.assign(files[index], {
continue() {
index++
nextItem()
}
})
else
return null
}
nextItem()
})
return out
}
return { openKeyCursor }
}
}
}
createMethodProxy(target, funcs) {
return new Proxy(target, {
get(target, prop) {
console.log('IDB Proxy accessing:', prop)
if(funcs[prop])
return funcs[prop](target[prop].bind(target), target)
else
return target[prop]
},
set(target, prop, value) {
// Ensure sure handlers like onsuccess and such work properly
target[prop] = typeof value == 'function' ? value.bind(target) : value
}
})
}
getRemoteKey(key, includeExt = true) {
return localBasePath + (includeExt ? key + localExt : key)
}
isKeyLocal(key) {
return key.startsWith(localBasePath) && key.endsWith(localExt)
}
getLocalKey(key) {
return key.slice(localBasePath.length, -localExt.length)
}
getLocalRemoteKeys() {
return localforage.keys()
.then(keys =>
keys.filter(key => this.isKeyLocal(key))
)
}
patch(IDBFS) {
const self = this
IDBFS.getDB = function getDB(name, callback) {
const dbEmulator = self.createMethodProxy(top.localforage._dbInfo.db, {
transaction: original => (name, ...args) => {
let out = original(['a'], ...args)
if(Array.isArray(name) && name[0] == IDBFS.DB_STORE_NAME)
out = self.createMethodProxy(out, {
objectStore: original => () =>
self.createMethodProxy(original('a'), self.objectStoreOverrides)
})
// if(Array.isArray(name) )
// out = self.createMethodProxy(out, {
// objectStore: original => () =>
// self.createMethodProxy(original('a'), self.objectStoreOverrides)
// })
return out
}
})
callback(null, dbEmulator)
}
}
sync() {
$explorer.refresh()
}
}
module.exports = EmulatedIDB

148
src/Instance.js Executable file
View File

@@ -0,0 +1,148 @@
const Console = require('./Console')
const EmulatedIDB = require('./EmulatedIDB')
const ModPackage = require('./ModPackage')
const handleIframe = require('./util/handleIframe'),
// gzip = require('./util/gzip'),
promisify = require('./util/promisify')
const openAsync = promisify($file.open),
{ Buffer } = le._apps.abnt
class Instance {
constructor(app, modPath) {
let self = this,
bundleDir = app.bundle.for('/import/')
let width = 640 + 7 - 9,
height = 480 + 28 - 30
const options = {
title: 'Xash3D',
// url: "data:text/plain,",
url: app.bundle.openSync('./main.html', 'URL'),
// icon: app.icon,
// Windows93 adds to these to compensate for title height and such,
// but we want it to match the canvas resolution.
// We want 647 x 508 on the window element
width,
height,
minWidth: width,
minHeight: height,
bodyClass: 'xash3d_main',
menu: [
{
name: 'Game',
items: [
{
name: 'Open console',
action: function() {
self.openConsole()
self.focusConsole()
}
}
]
}
],
onready() {
const { iframe } = self.window.el
iframe.contentWindow.instance = self
handleIframe(iframe, '/import/')
},
onclose() {
self.consoleWindow?.close()
self.closed = true
app.cleanInstances()
}
}
this.consoleWindow
this.window = $window(options)
this.closed = false
this.app = app
this.assets = new Map()
this.import = new Proxy(this.assets, {
get(target, prop) {
console.log(`LOADING "${prop}"`)
if( !target.has(prop) && bundleDir.access(prop) ) {
target.set(prop, bundleDir.openSync(prop, 'URL') )
}
return target.get(prop)
}
})
this.emulatedIDB = new EmulatedIDB()
this.arguments = []
this.package = this.loadPackage(modPath)
.then(mod => {
this.window.changeTitle(mod.manifest.name)
return mod
})
.catch(console.error)
this.console = new Console(() => this.onQuit())
this.onCloseConsole
console.log('INSTANCE:', this)
}
async loadPackage(path) {
let buffer = await openAsync(path, 'ArrayBuffer'),
mod = await ModPackage.unpack(buffer)
return mod
}
openConsole() {
let self = this
if(this.consoleWindow)
// Put focus on the window instead
this.focusConsole()
const options = {
title: 'Xash Console',
bodyClass: 'ui_terminal xash3d_terminal',
onready() {
self.console.attachTo(self.consoleWindow)
self.focusConsole()
},
onclose() {
self.console.unattach()
self.onCloseConsole?.call()
}
}
this.consoleWindow = $window(options)
}
focusConsole() {
this.consoleWindow.el.header.click()
}
// Triggered when the user presses "quit" on the main menu
onQuit() {
if(this.consoleWindow) {
this.onCloseConsole = () => {
// Prevent .kill from trying to close the console
this.consoleWindow = null
this.kill()
}
} else {
this.kill()
}
}
kill() {
this.window?.close()
this.consoleWindow?.close()
delete this.assets // pls my memory
}
}
module.exports = Instance

121
src/Launcher.js Normal file
View File

@@ -0,0 +1,121 @@
const ModPackage = require('./ModPackage')
const constants = require('./util/constants.json'),
promisify = require('./util/promisify')
const create = (name, ...children) => {
let classList = name.split('.')
ele = document.createElement(classList.shift() || 'div')
if(classList)
ele.classList = classList.join(' ')
children.forEach(child =>
typeof(child) == 'string' ? ele.innerText += child : ele.appendChild(child)
)
return ele
}
const openAsync = promisify($file.open)
class Launcher {
constructor(app) {
const self = this
const options = {
title: 'Xash3D Launcher',
// html: app.bundle.openSync('./launcher.html', 'String'),
bodyClass: 'skin_inset xash3d_launcher',
width: 350,
height: 400,
// onready() {
// const { iframe } = launcher.el,
// win = iframe.contentWindow
// win.app = self
// handleIframe(iframe)
// },
// TODO: move to actual mods folder
onready() {
console.log(this)
self.modList = this.el.body.appendChild(create('ul') )
self.loadMods()
},
footer: `
<span style="display: inline-block; color: #555; padding: 3px 1px;">
v${app.version}
</span>
<span style="float: right">
<button onclick="$exe('${constants.paths.modPath}')">Open Mods Folder</button>
</span>
`
}
this.app = app
this.window = $window(options)
}
async loadMods() {
let { modPath } = constants.paths,
files = $io.obj.getPath(window.le._files, modPath, '/'),
out = []
console.log(files)
if(files) {
for(let name in files)
if(files[name] == 0 && $fs.utils.getExt(name) == 'asar') {
let path = modPath + name,
promise = openAsync(path, 'ArrayBuffer')
.then(async buffer => ({
name,
path,
size: buffer.byteLength,
manifest: await ModPackage.unpack(buffer, true)
.then(data => JSON.parse(data.manifestString) )
}))
out.push(promise)
}
}
out = await Promise.all(out)
out
.map(mod => this.renderMod(mod))
.map(ele => this.modList.appendChild(ele) )
}
renderMod(mod) {
// const mod = document.createElement('div'),
// header = document.createElement('header'),
// info = document.createElement('div')
// modName = document.createElement('h5'),
// modInfo = document.createElement('span'),
// launch = document.createElement('div')
// header.appendChild(modName)
// header.appendChild(modInfo)
// mod.appendChild('header')
let self = this,
launch,
element = create('.mod.skin_outset',
create('header',
create('.info',
create('h5', mod.manifest.name),
create('span', `${mod.name} | ${parseInt(mod.size / 1_000_000)}mb`)
),
launch = create('.launch')
)
)
launch.onclick = () => {
self.app.launch(mod.path)
self.window.destroy()
}
return element
}
}
module.exports = Launcher

18
src/ManifestParser.js Executable file
View File

@@ -0,0 +1,18 @@
const Ajv = require('ajv'),
constants = require('./util/constants')
const validate = new ajv().compile(constants.manifestSchema)
class ManifestParser {
constructor(manifestString) {
let data = JSON.parse(manifestString),
valid = validate(data)
if(!valid)
throw new Error('Uh oh, stinky!')
return data
}
}
module.exports = ManifestParser

74
src/ModPackage.js Executable file
View File

@@ -0,0 +1,74 @@
const { gzip } = require('./util/gzip')
// I don't like using a loader in this way but I
// guess this is punishment for putting off
// putting AsarHandler in it's own project
// and finishing FakeBuffer
const { AsarHandler: Asar, Buffer } = le._apps.abnt
function toArrayBuffer(buffer) {
let ab = new ArrayBuffer(buffer.length),
view = new Uint8Array(ab)
for (let i = 0; i < buffer.length; ++i) {
view[i] = buffer[i]
}
return ab
}
const textDecoder = new TextDecoder('utf-8')
class ModPackage {
static async unpack(buffer, direct = false) {
let asar = new Asar(buffer),
manifestString = textDecoder.decode( asar.get('manifest.json') ),
files = [ ...asar.contents ]
const decompress = path => new Promise((resolve, reject) => {
// gzip(asar.get(path), (error, data) => {
// console.log('GZIP: ', path, data)
// if(error)
// reject(error)
// else
// resolve([ path, toArrayBuffer(data) ])
// })
resolve([ path, asar.get(path) ])
})
files.splice(files.indexOf('manifest.json'), 1)
files = files.map(decompress)
files = await Promise.all(files)
return direct ? { files, manifestString } : new this(new Map(files), manifestString)
}
constructor(files, manifestString) {
this.manifest = JSON.parse(manifestString)
this.files = files
this.cache = new Map()
}
get(path) {
if(this.cache.has(path))
return this.cache.get(path)
let data = this.files.get(path),
ext = $fs.utils.getExt(path),
file = new Blob([ data ], { type: le._get.ext.mime[ext] })
this.cache.set(path, file)
return file
}
getURL(path) {
let file = this.get(path),
url = URL.createObjectURL(file)
return url
}
}
module.exports = ModPackage

24
src/emf/Opener.js Normal file
View File

@@ -0,0 +1,24 @@
const { emf } = require('../util/constants.json'),
merge = require('../../util/merge')
class Opener {
constructor() {
this.name = emf.id
this.silent = true
this.categories = emf.categories
this.accept = emf.ext
this.exec = (url, context) => {
console.log(url, context)
}
}
patchSystem() {
// Import to prevent race conditions
const { _get } = window.le
window.le._get = merge(_get, emf.filetypePatch)
}
}
module.exports = Opener

152
src/hidden_Instance.js Executable file
View File

@@ -0,0 +1,152 @@
const Console = require('./Console')
const ModPackage = require('./ModPackage')
const handleIframe = require('./util/handleIframe'),
gzip = require('./util/gzip'),
promisify = require('./util/promisify')
const openAsync = promisify($file.open),
{ Buffer } = le._apps.abnt
class Instance {
constructor(app, modPath) {
let self = this,
bundleDir = app.bundle.for('/import/')
this.consoleWindow
this.window
this.mod
this.closed = false
this.app = app
this.assets = new Map()
this.import = new Proxy(this.assets, {
get(target, prop) {
console.log(`LOADING "${prop}"`)
if( !target.has(prop) && bundleDir.access(prop) ) {
target.set(prop, bundleDir.openSync(prop, 'URL') )
}
return target.get(prop)
}
})
this.arguments = []
this.console = new Console()
console.log('INSTANCE:', this)
// Post init
this.package = this.loadPackage(modPath)
.then(mod => {
this.mod = mod
console.log('woo window,', this.window)
return mod
})
this.package.catch(console.error)
this.openMain()
}
async loadPackage(path) {
const buffer = await openAsync(path, 'ArrayBuffer').then(ab => Buffer.from(ab)),
mod = await ModPackage.unpack(buffer)
return mod
}
openMain() {
// We'll need to shove it in a fragment to force it to load while hidden
const iframe = document.createElement('iframe'),
self = this
const options = {
title: 'Xash3D',
// url: "data:text/plain,",
// icon: app.icon,
// Windows93 adds to these to compensate for title height and such,
// but we want it to match the canvas resolution.
// We want 647 x 508 on the window element
width: 640 + 7 - 9,
height: 480 + 28 - 30,
menu: [
{
name: 'Game',
items: [
{
name: 'Open console',
action: function() {
self.openConsole()
}
}
]
}
],
onready() {
console.log('this:', this)
this.el.body.appendChild(iframe)
},
onclose() {
self.consoleWindow?.close()
self.closed = true
app.cleanInstances()
}
}
iframe.style.display = 'none'
iframe.src = this.app.bundle.openSync('./main.html', 'URL')
iframe.onload = () => {
iframe.contentWindow.instance = self
iframe.contentWindow.onmessage = event => {
console.log(event)
if(event.data == 'loadingDone')
self.window = $window(options)
iframe.style.display = 'initial'
}
handleIframe(iframe, '/import/')
console.log('loadd')
}
document.body.append(iframe)
console.log(iframe)
}
openConsole() {
const self = this
if(this.consoleWindow)
// Put focus on the window instead
this.focusConsole()
const options = {
title: 'Xash Console',
bodyClass: 'ui_terminal xash3d_terminal',
onready() {
self.console.attachTo(self.consoleWindow)
self.focusConsole()
},
onclose() {
self.console.unattach()
}
}
this.consoleWindow = $window(options)
}
focusConsole() {
this.consoleWindow.el.header.click()
}
kill() {
this.window?.close()
this.consoleWindow?.close()
delete this.assets // pls my memory
}
}
module.exports = Instance

7
src/index.js Executable file
View File

@@ -0,0 +1,7 @@
// https://icrazyblaze.github.io/Xash3D-Emscripten/xash.html
// https://github.com/icrazyblaze/Xash3D-Emscripten
const App = require('./App'),
{ app } = require('./util/constants')
le._apps[app.id] = new App()

31
src/util/Loader.js Executable file
View File

@@ -0,0 +1,31 @@
// $loader alternative that can be bind to other scopes
class Loader {
constructor(document = window.document) {
this.document = document
}
createElement(tag, resource, rel) {
const executor = (resolve, reject) => {
const { document } = this,
element = document.createElement(tag)
element[tag === 'script' ? 'src' : 'href'] = resource
element.rel = rel
element.onload = () => resolve(element)
document.head.appendChild(element)
}
return new Promise(executor)
}
script(src) {
return this.createElement('script', src)
}
css(href) {
return this.createElement('link', href, 'stylesheet')
}
}
module.exports = Loader

View File

@@ -0,0 +1,58 @@
const Loader = require('./Loader')
const wrapProcessor = (preprocessor, path, handle) => async data => {
let ext = $fs.utils.getExt(path),
processed = await preprocessor(data, path).catch(console.error),
blob = new Blob([ processed ], { type: le._get.ext.mime[ext] })
handle( URL.createObjectURL(blob) )
}
const handleIframe = async (iframe, path = '/') => {
const convert = (tag, from, to, preprocessor) => {
let elements = iframe.contentDocument.querySelectorAll(tag)
for(let element of elements) {
let original = element.getAttribute(from)
if(!original)
continue
let handle = url => {
element[to] = url
}
$bundle.for(path).open(original, preprocessor ? 'String' : 'URL')
.then(preprocessor ? wrapProcessor(preprocessor, path, handle) : handle)
}
}
convert('script', 'lsrc', 'src')
convert('img', 'lsrc', 'src')
convert('link', 'lhref', 'href', async (stylesheet, path) => {
let matches = stylesheet.matchAll(/url\(("(.+?)"|'(.+?)'|(.+?))\)/g),
fromIndex = 0,
out = []
console.log(matches)
for(let match of matches) {
let bundle = $bundle.for( $fs.utils.getFolderPath(path) )
console.log(bundle.access(match[2]))
out.concat([
stylesheet.slice(fromIndex, match.index),
'url("' + await bundle.open(match[2], 'URL') + '")'
])
fromIndex = match.index + match[0].length
}
console.log(out, fromIndex)
return out.join('')
})
}
module.exports = handleIframe

71
src/util/constants.json Executable file
View File

@@ -0,0 +1,71 @@
{
"app": {
"id": "xash",
"categories": "Game",
"version": "0.0.0b"
},
"emf": {
"id": "emf",
"ext": ".emf",
"categories": "Utility"
},
"paths": {
"modPath": "/a/.config/xash/mods/",
"saves": "/a/.config/xash/saves/"
},
"manifestScheme": {
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/object1619767958.json",
"title": "Root",
"type": "object",
"required": [
"name",
"entry"
],
"properties": {
"name": {
"$id": "#root/name",
"title": "Name",
"type": "string",
"examples": [
"Half-life Deathmatch"
],
"pattern": "^.*$"
},
"description": {
"$id": "#root/description",
"title": "Description",
"type": "string",
"examples": [
"hurrr durr big p"
],
"pattern": "^.*$"
},
"entry": {
"$id": "#root/entry",
"title": "Entry",
"type": "string",
"examples": [
"./hldm.js"
],
"pattern": "^.*$"
},
"compressed": {
"$id": "#root/compressed",
"title": "Compressed",
"type": "boolean",
"examples": [
true
],
"default": true
},
"readme": {
"$id": "#root/readme",
"title": "Readme",
"type": "string",
"pattern": "^.*$"
}
}
}
}

8
src/util/gzip.js Executable file
View File

@@ -0,0 +1,8 @@
// Require will import EVERYTHING no matter what I do,
// so I'm making a fool of myself with this stupid
// util script. Should've listened to robbie and
// gotten used to ES6 syntax but noooo I had to be
// a special snowflake with my stupid CJS garbage.
import { gzip } from "fflate"
export { gzip }

24
src/util/handleIframe.js Executable file
View File

@@ -0,0 +1,24 @@
const Loader = require('./Loader')
const handleIframe = async (iframe, path = '/') => {
const convert = (tag, from, to) => {
let elements = iframe.contentDocument.querySelectorAll(tag)
for(let element of elements) {
let original = element.getAttribute(from)
if(!original)
continue
$bundle.for(path).open(original, 'URL').then(url => {
element[to] = url
})
}
}
convert('script', 'lsrc', 'src')
convert('img', 'lsrc', 'src')
convert('link', 'lhref', 'href')
}
module.exports = handleIframe

17
src/util/promisify.js Executable file
View File

@@ -0,0 +1,17 @@
const promisify = original => {
const async = (...args) => {
const executor = (resolve, reject) => {
try {
original(...args, resolve)
} catch(error) {
reject(error)
}
}
return new Promise(executor)
}
return async
}
module.exports = promisify