feat: WASM build

This commit is contained in:
Daniel Schmidt 2023-12-23 13:35:44 +01:00
parent b944ca2733
commit 35bbe9088f
29 changed files with 1329 additions and 71 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ node_modules/
cpu*
heap*
profile*
file_index.json

125
cmd/game_wasm/index.html Normal file
View File

@ -0,0 +1,125 @@
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<script src="./wasm_exec.js"></script>
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Inconsolata', monospace;
background-color: #1a1a1a !important;
}
.terminal-container {
/* this is important */
overflow: hidden;
}
.xterm .xterm-viewport {
/* see : https://github.com/xtermjs/xterm.js/issues/3564#issuecomment-1004417440 */
width: initial !important;
}
</style>
</head>
<body>
<div class="terminal-container" style="height: 100%; width: 100%;">
<div id="terminal" style="height: 100%"></div>
</div>
<script>
globalThis.settings = {
get(key, emptyValue) {
console.log("get", key, emptyValue)
try {
return JSON.parse(window.localStorage.getItem(key))
} catch (e) {
return emptyValue !== undefined ? emptyValue : null
}
},
getString(key) {
return window.settings.get(key, "")
},
getInt(key) {
return window.settings.get(key, 0)
},
getBool(key) {
return window.settings.get(key, false)
},
getFloat(key) {
return window.settings.get(key, 0.0)
},
getStrings(key) {
return window.settings.get(key, [])
},
set(key, value) {
window.localStorage.setItem(key, JSON.stringify(value))
},
setDefault(key, value) {
if (window.localStorage.getItem(key) !== null) {
return
}
window.localStorage.setItem(key, value)
}
}
</script>
<script>
function initTerminal() {
const term = new Terminal({
fontSize: 18,
fontFamily: 'Inconsolata',
theme: {
background: '#1a1a1a'
}
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
// Register terminal resize
fitAddon.fit();
window.addEventListener('resize', () => (fitAddon.fit()));
// Initial resize
bubbletea_resize(term.cols, term.rows)
// Read from bubbletea and write to xterm
setInterval(() => {
const read = bubbletea_read();
if (read && read.length > 0) {
term.write(read);
}
}, 1000 / 30);
// Resize on terminal resize
term.onResize((size) => (bubbletea_resize(term.cols, term.rows)));
// Write xterm output to bubbletea
term.onData((data) => (bubbletea_write(data)));
}
function init() {
const go = new Go();
WebAssembly.instantiateStreaming(fetch("./eoe.wasm"), go.importObject).then((result) => {
// Run wasm
go.run(result.instance).then(() => {
console.log("wasm finished");
});
// Init terminal. This should be done after bubbletea is initialized. For now, I use a timeout.
setTimeout(() => {
document.fonts.load('16px "Inconsolata"').then(() => {initTerminal();});
}, 1000);
})
}
init();
</script>
</body>
</html>

160
cmd/game_wasm/main.go Normal file
View File

@ -0,0 +1,160 @@
//go:build js
// +build js
package main
import (
"bytes"
"fmt"
"github.com/BigJk/end_of_eden/system/gen"
"github.com/BigJk/end_of_eden/system/gen/faces"
"github.com/BigJk/end_of_eden/system/localization"
"github.com/BigJk/end_of_eden/system/settings"
"github.com/BigJk/end_of_eden/system/settings/browser"
"github.com/BigJk/end_of_eden/ui/menus/mainmenu"
uiset "github.com/BigJk/end_of_eden/ui/menus/settings"
"github.com/BigJk/end_of_eden/ui/menus/warning"
"github.com/BigJk/end_of_eden/ui/root"
"github.com/BigJk/end_of_eden/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
zone "github.com/lrstanley/bubblezone"
"github.com/muesli/termenv"
"log"
_ "net/http/pprof"
"os"
"strings"
"syscall/js"
"time"
)
type MinReadBuffer struct {
buf *bytes.Buffer
}
// For some reason bubbletea doesn't like a Reader that will return 0 bytes instead of blocking,
// so we use this hacky workaround for now. As javascript is single threaded this should be fine
// with regard to concurrency.
func (b *MinReadBuffer) Read(p []byte) (n int, err error) {
for b.buf.Len() == 0 {
time.Sleep(100 * time.Millisecond)
}
return b.buf.Read(p)
}
func (b *MinReadBuffer) Write(p []byte) (n int, err error) {
return b.buf.Write(p)
}
// Creates the bubbletea program and registers the necessary functions in javascript
func createTeaForJS(model tea.Model, option ...tea.ProgramOption) *tea.Program {
// Create buffers for input and output
fromJs := &MinReadBuffer{buf: bytes.NewBuffer(nil)}
fromGo := bytes.NewBuffer(nil)
prog := tea.NewProgram(model, append([]tea.ProgramOption{tea.WithInput(fromJs), tea.WithOutput(fromGo)}, option...)...)
// Register write function in WASM
js.Global().Set("bubbletea_write", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
fromJs.Write([]byte(args[0].String()))
return nil
}))
// Register read function in WASM
js.Global().Set("bubbletea_read", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
b := make([]byte, fromGo.Len())
_, _ = fromGo.Read(b)
fromGo.Reset()
return string(b)
}))
// Register resize function in WASM
js.Global().Set("bubbletea_resize", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
width := args[0].Int()
height := args[1].Int()
prog.Send(tea.WindowSizeMsg{Width: width, Height: height})
return nil
}))
return prog
}
var prog *tea.Program
var loadStyle = lipgloss.NewStyle().Bold(true).Italic(true).Foreground(style.BaseGray)
func main() {
lipgloss.SetColorProfile(termenv.TrueColor)
fmt.Println(lipgloss.NewStyle().Bold(true).Foreground(style.BaseRed).Render("End Of Eden"))
// Init settings
fmt.Println(loadStyle.Render("Initializing Settings. Please wait..."))
{
set := browser.Browser{}
set.SetDefault("audio", true)
set.SetDefault("volume", 1)
set.SetDefault("language", "en")
settings.SetSettings(set)
if err := settings.LoadSettings(); err != nil {
panic(err)
}
}
fmt.Println(loadStyle.Render("Done!"))
// Init generators
fmt.Println(loadStyle.Render("Initializing Proc-Gen. Please wait..."))
{
// Init face generator
if err := faces.InitGlobal("./assets/gen/faces"); err != nil {
panic(err)
}
// Init other gens
gen.InitGen()
}
fmt.Println(loadStyle.Render("Done!"))
// Init Localization
fmt.Println(loadStyle.Render("Initializing Localization. Please wait..."))
{
if err := localization.Global.AddFolder("./assets/locals"); err != nil {
panic(err)
}
localization.SetCurrent(settings.GetString("language"))
}
fmt.Println(loadStyle.Render("Done!"))
log.Println("=================================")
log.Println("= Started")
log.Println("=================================")
// Set window title
fmt.Println("\033]2;End of Eden\007")
uiSettings := []uiset.Value{
{Key: "audio", Name: "Audio", Description: "Enable or disable audio", Type: uiset.Bool, Val: settings.GetBool("audio"), Min: nil, Max: nil},
{Key: "volume", Name: "Volume", Description: "Change the volume", Type: uiset.Float, Val: settings.GetFloat("volume"), Min: 0.0, Max: 2.0},
{Key: "language", Name: "Language", Description: fmt.Sprintf("Change the language (supported: %s)", strings.Join(localization.Global.GetLocales(), ", ")), Type: uiset.String, Val: settings.GetString("language")},
}
// Setup game
var baseModel tea.Model
zones := zone.New()
baseModel = root.New(zones, mainmenu.NewModel(zones, settings.GetGlobal(), uiSettings, func(values []uiset.Value) error {
for i := range values {
settings.Set(values[i].Key, values[i].Val)
}
localization.SetCurrent(settings.GetString("language"))
return settings.SaveSettings()
}))
baseModel = baseModel.(root.Model).PushModel(warning.New(nil, style.RedText.Render("Warning!")+"\n\nThe Browser version is still very experimental. Loading times can be long. Mouse support is clunky. For the best experience, please use the Desktop version!\n\n"+style.GrayTextDarker.Render("Press ESC to continue")))
// Run game
prog = createTeaForJS(baseModel, tea.WithAltScreen(), tea.WithMouseAllMotion(), tea.WithANSICompressor())
if _, err := prog.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}

554
cmd/game_wasm/wasm_exec.js Normal file
View File

@ -0,0 +1,554 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
go: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

View File

@ -7,6 +7,7 @@ import (
teadapter "github.com/BigJk/crt/bubbletea"
"github.com/BigJk/crt/shader"
"github.com/BigJk/end_of_eden/cmd/testargs"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/system/gen"
"github.com/BigJk/end_of_eden/system/gen/faces"
@ -183,7 +184,7 @@ func main() {
// Setup grain shader
if settings.GetBool("grain") {
res, _ := os.ReadFile("./assets/shader/grain.go")
res, _ := fs.ReadFile("./assets/shader/grain.go")
grain, err := ebiten.NewShader(res)
if err != nil {

View File

@ -1,12 +1,12 @@
package game
import (
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
luhelp2 "github.com/BigJk/end_of_eden/internal/lua/luhelp"
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/system/gen/faces"
"github.com/BigJk/end_of_eden/system/localization"
"io/fs"
"path/filepath"
"strings"
@ -24,14 +24,19 @@ func SessionAdapter(session *Session) (*lua.LState, *ludoc.Docs) {
mapper := luhelp2.NewMapper(l)
_ = filepath.Walk("./assets/scripts/libs", func(path string, info fs.FileInfo, _ error) error {
if info != nil && info.IsDir() || !strings.HasSuffix(path, ".lua") {
_ = fs.Walk("./assets/scripts/libs", func(path string, isDir bool) error {
if isDir || !strings.HasSuffix(path, ".lua") {
return nil
}
name := strings.Split(filepath.Base(path), ".")[0]
mod, err := l.LoadFile(path)
luaBytes, err := fs.ReadFile(path)
if err != nil {
return err
}
mod, err := l.LoadString(string(luaBytes))
if err != nil {
session.log.Println("Can't LoadFile module:", path)
return nil

View File

@ -2,7 +2,7 @@ package game
import (
"encoding/json"
"os"
"github.com/BigJk/end_of_eden/internal/fs"
"path/filepath"
)
@ -15,7 +15,7 @@ type Mod struct {
}
func ModDescription(folder string) (Mod, error) {
data, err := os.ReadFile(filepath.Join(folder, "/meta.json"))
data, err := fs.ReadFile(filepath.Join(folder, "/meta.json"))
if err != nil {
return Mod{}, err
}

View File

@ -2,13 +2,12 @@ package game
import (
"fmt"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
luhelp2 "github.com/BigJk/end_of_eden/internal/lua/luhelp"
"github.com/samber/lo"
lua "github.com/yuin/gopher-lua"
"io/fs"
"log"
"path/filepath"
"strings"
)
@ -67,18 +66,20 @@ func NewResourcesManager(state *lua.LState, docs *ludoc.Docs, logger *log.Logger
man.defineDocs(docs)
// Load all local scripts
_ = filepath.Walk("./assets/scripts", func(path string, info fs.FileInfo, err error) error {
_ = fs.Walk("./assets/scripts", func(path string, isDir bool) error {
// Don't load libs
if strings.Contains(path, "scripts/libs") {
return nil
}
if err != nil {
return nil
}
if !isDir && strings.HasSuffix(path, ".lua") {
luaBytes, err := fs.ReadFile(path)
if err != nil {
// TODO: error handling
panic(err)
}
if !info.IsDir() && strings.HasSuffix(path, ".lua") {
if err := man.luaState.DoFile(path); err != nil {
if err := man.luaState.DoString(string(luaBytes)); err != nil {
// TODO: error handling
panic(err)
}

View File

@ -6,6 +6,7 @@ import (
"encoding/gob"
"errors"
"fmt"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/internal/lua/ludoc"
"github.com/BigJk/end_of_eden/system/gen"
"github.com/BigJk/end_of_eden/system/gen/faces"
@ -15,10 +16,8 @@ import (
lua "github.com/yuin/gopher-lua"
"golang.org/x/exp/slices"
"io"
"io/fs"
"log"
"math/rand"
"os"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
@ -330,18 +329,20 @@ func (s *Session) loadMods(mods []string) {
log.Println("Loading mod:", mod.Name)
}
_ = filepath.Walk(filepath.Join("./mods", mods[i]), func(path string, info fs.FileInfo, err error) error {
_ = fs.Walk(filepath.Join("./mods", mods[i]), func(path string, isDir bool) error {
// If we find a locals folder we add it to the localization
if info.IsDir() && info.Name() == "locals" {
if isDir && filepath.Base(path) == "locals" {
_ = localization.Global.AddFolder(path)
}
if err != nil {
return nil
}
if isDir && strings.HasSuffix(path, ".lua") {
luaBytes, err := fs.ReadFile(path)
if err != nil {
// TODO: error handling
panic(err)
}
if !info.IsDir() && strings.HasSuffix(path, ".lua") {
if err := s.luaState.DoFile(path); err != nil {
if err := s.luaState.DoString(string(luaBytes)); err != nil {
s.logLuaError("ModLoader", "", err)
}
}
@ -466,7 +467,7 @@ func (s *Session) SetupFight() {
if err != nil {
s.log.Println("Error saving file:", save)
} else {
if err := os.WriteFile("./session.save", save, 0666); err != nil {
if err := fs.WriteFile("./session.save", save); err != nil {
s.log.Println("Error saving file:", save)
}
}

8
go.mod
View File

@ -2,6 +2,12 @@ module github.com/BigJk/end_of_eden
go 1.20
replace github.com/containerd/console => github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5
replace github.com/atotto/clipboard => github.com/BigJk/clipboard v0.0.0-20230514080810-a7e7bd3670a5
replace github.com/charmbracelet/bubbletea => github.com/BigJk/bubbletea v0.0.0-20231222122351-25c0e0b15c34
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161
github.com/BigJk/crt v0.0.13
@ -36,6 +42,7 @@ require (
github.com/yuin/gopher-lua v1.1.0
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9
golang.org/x/sys v0.13.0
gopkg.in/yaml.v3 v3.0.1
oss.terrastruct.com/d2 v0.4.1
)
@ -109,6 +116,5 @@ require (
gonum.org/v1/plot v0.12.0 // indirect
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
oss.terrastruct.com/util-go v0.0.0-20230320053557-dcb5aac7d972 // indirect
)

19
go.sum
View File

@ -47,6 +47,10 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BigJk/bubbletea v0.0.0-20231222122351-25c0e0b15c34 h1:I3ZTYobM/4vLCKnH6KgN1HX1EwnsaD4lbnOoc4YNhFo=
github.com/BigJk/bubbletea v0.0.0-20231222122351-25c0e0b15c34/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/BigJk/clipboard v0.0.0-20230514080810-a7e7bd3670a5 h1:bJrh/4V+8y6u05623Zja5vJD7RLM0Km5CdUrMJnPTDY=
github.com/BigJk/clipboard v0.0.0-20230514080810-a7e7bd3670a5/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/BigJk/crt v0.0.13 h1:qZRw2tuJCuKsk3lC93Vi+Guib//Rs1m/rcGUR/OCHFE=
github.com/BigJk/crt v0.0.13/go.mod h1:0jGrs6QH6A7zDJz91HfLCXG8yKR12sqOUPq8LrOoDbg=
github.com/BigJk/imeji v0.0.2 h1:h4cKLWkVzs+IVs/27DFTYGsGuA1OqQxrYDNtvlojypU=
@ -70,8 +74,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
github.com/anthonynsimon/bild v0.13.0 h1:mN3tMaNds1wBWi1BrJq0ipDBhpkooYfu7ZFSMhXt1C8=
github.com/anthonynsimon/bild v0.13.0/go.mod h1:tpzzp0aYkAsMi1zmfhimaDyX1xjn2OUc1AJZK/TF0AE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
@ -83,9 +85,6 @@ github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQ
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
@ -108,9 +107,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5 h1:Ig+OPkE3XQrrl+SKsOqAjlkrBN/zrr+Qpw7rCuDjRCE=
github.com/containerd/console v1.0.4-0.20230706203907-8f6c4e4faef5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -298,6 +296,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@ -582,7 +581,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -603,12 +601,14 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -619,6 +619,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=

16
internal/fs/fileinfo.go Normal file
View File

@ -0,0 +1,16 @@
package fs
import "path/filepath"
type FileInfo struct {
Path string `json:"path"`
IsFile bool `json:"IsFile"`
}
func (fi FileInfo) Name() string {
return filepath.Base(fi.Path)
}
func (fi FileInfo) IsDir() bool {
return !fi.IsFile
}

45
internal/fs/fs.go Normal file
View File

@ -0,0 +1,45 @@
//go:build !js
// +build !js
package fs
import (
"github.com/samber/lo"
"io"
"os"
"path/filepath"
)
func ReadDir(path string) ([]FileInfo, error) {
dir, err := os.ReadDir(path)
if err != nil {
return nil, err
}
return lo.Map(dir, func(f os.DirEntry, i int) FileInfo {
return FileInfo{
Path: filepath.Join(path, f.Name()),
IsFile: !f.IsDir(),
}
}), nil
}
func OpenFile(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
return os.OpenFile(name, flag, perm)
}
func ReadFile(path string) ([]byte, error) {
return os.ReadFile(path)
}
func WriteFile(path string, data []byte) error {
return os.WriteFile(path, data, 0644)
}
func Walk(root string, walkFn func(path string, isDir bool) error) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
return walkFn(path, info.IsDir())
})
}

115
internal/fs/fs_js.go Normal file
View File

@ -0,0 +1,115 @@
//go:build js
// +build js
package fs
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/samber/lo"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"syscall/js"
)
type noOpWriteCloser struct{}
func (noOpWriteCloser) Write(p []byte) (int, error) {
return len(p), nil
}
func (noOpWriteCloser) Close() error {
return nil
}
var fileIndex = make(map[string]FileInfo)
func init() {
data, err := ReadFile("/assets/file_index.json")
if err != nil {
panic(err)
}
var fis []FileInfo
if err := json.Unmarshal(data, &fis); err != nil {
panic(err)
}
for _, fi := range fis {
fi.Path = filepath.Clean(fi.Path)
fileIndex[fi.Path] = fi
}
js.Global().Set("fsDump", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
for _, fi := range fis {
fmt.Println(fi.Path)
}
return nil
}))
}
func ReadDir(path string) ([]FileInfo, error) {
cleanPath := filepath.Clean(path)
var fis []FileInfo
for indexPath := range fileIndex {
if strings.HasPrefix(indexPath, cleanPath) {
fis = append(fis, fileIndex[path])
}
}
return fis, nil
}
func ReadFile(path string) ([]byte, error) {
// Check for temp file
jsRes := js.Global().Call("fsRead", path)
if !jsRes.IsNull() && !jsRes.IsUndefined() {
return base64.StdEncoding.DecodeString(jsRes.String())
}
// Check for asset
res, err := http.Get(path)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("could not load file %s: %s", path, res.Status)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
return body, nil
}
func OpenFile(name string, flag int, perm os.FileMode) (io.WriteCloser, error) {
// TODO: Implement
return noOpWriteCloser{}, nil
}
func WriteFile(path string, data []byte) error {
// TODO: error handling
_ = js.Global().Call("fsWrite", path, base64.StdEncoding.EncodeToString(data))
return nil
}
func Walk(root string, walkFn func(path string, isDir bool) error) error {
keys := lo.Keys(fileIndex)
sort.Strings(keys)
cleanPath := filepath.Clean(root)
for _, path := range keys {
if !strings.HasPrefix(path, cleanPath) {
continue
}
if err := walkFn(path, fileIndex[path].IsDir()); err != nil {
return err
}
}
return nil
}

43
internal/misc/build_index.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
# Check if the argument is provided
if [ -z "$1" ]; then
echo "Usage: $0 <folder_path>"
exit 1
fi
folder_path="$1"
# Initialize an empty array
file_index=()
# Function to walk through the directory recursively
walk() {
local directory="$1"
for file in "$directory"/*; do
if [ -d "$file" ]; then
# If it's a directory, add to the array and recursively call walk function
file_index+=(" {\"path\": \"$file\", \"isFile\": false}")
walk "$file"
elif [ -f "$file" ]; then
# If it's a file, add to the array
file_index+=(" {\"path\": \"$file\", \"isFile\": true}")
fi
done
}
# Start walking through the directory
walk "$folder_path"
# Create JSON file with the content of the array in the same folder
{
printf '[\n'
for ((i = 0; i < ${#file_index[@]}; i++)); do
printf '%s' "${file_index[i]}"
if [ $i -ne $(( ${#file_index[@]} - 1 )) ]; then
printf ','
fi
printf '\n'
done
printf ']\n'
} > "$folder_path/file_index.json"

View File

@ -1,12 +1,13 @@
//go:build !no_audio
// +build !no_audio
//go:build !no_audio && !js
// +build !no_audio,!js
// Package audio handles all audio playback. It uses the beep library to play audio files.
package audio
import (
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/settings"
"io/fs"
"log"
"os"
"path/filepath"
@ -41,11 +42,7 @@ func InitAudio() {
go func() {
wg := &sync.WaitGroup{}
_ = filepath.Walk("./assets/audio", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return nil
}
_ = fs.Walk("./assets/audio", func(path string, isDir bool) error {
wg.Add(1)
go func() {
defer wg.Done()
@ -53,7 +50,7 @@ func InitAudio() {
var streamer beep.StreamSeekCloser
var format beep.Format
if !info.IsDir() {
if !isDir {
if strings.HasSuffix(path, ".mp3") {
f, err := os.Open(path)
if err != nil {

40
system/audio/audio_js.go Normal file
View File

@ -0,0 +1,40 @@
//go:build js
// +build js
// Package audio handles all audio playback. It uses the beep library to play audio files.
package audio
import (
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/settings"
"path/filepath"
"strings"
"syscall/js"
)
// InitAudio initializes the audio system. Loads all audio files from the assets/audio folder.
func InitAudio() {}
// Play plays a sound effect. If the sound effect is not loaded, nothing will happen.
func Play(key string, volumeModifier ...float64) {
fs.Walk("./assets/audio", func(path string, isDir bool) error {
if !isDir && strings.HasPrefix(filepath.Base(path), key) {
js.Global().Call("playSound", path)
}
return nil
})
}
// PlayMusic plays a music track. If the music track is not loaded, nothing will happen.
func PlayMusic(key string) {
if settings.GetFloat("volume") == 0 {
return
}
fs.Walk("./assets/audio", func(path string, isDir bool) error {
if !isDir && strings.HasPrefix(filepath.Base(path), key) {
js.Global().Call("loopMusic", path)
}
return nil
})
}

View File

@ -1,5 +1,5 @@
//go:build !no_audio
// +build !no_audio
//go:build !no_audio && !js
// +build !no_audio,!js
package audio

View File

@ -2,9 +2,9 @@ package faces
import (
"fmt"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/samber/lo"
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
@ -63,7 +63,7 @@ func New(dataFolder string) (*FaceGenerator, error) {
data: map[int][][]string{},
}
for i := 0; i < 7; i++ {
bytes, err := os.ReadFile(filepath.Join(dataFolder, fmt.Sprintf("/Face%d.txt", i)))
bytes, err := fs.ReadFile(filepath.Join(dataFolder, fmt.Sprintf("/Face%d.txt", i)))
if err != nil {
return nil, err
}

View File

@ -1,9 +1,9 @@
package gen
import (
"github.com/BigJk/end_of_eden/internal/fs"
"log"
"math/rand"
"os"
"strings"
)
@ -13,14 +13,14 @@ var data = map[string][]string{}
// The data is stored in a map with the type as key and a slice of strings, which are the lines
// of the file, as value.
func InitGen() {
files, err := os.ReadDir("./assets/gen")
files, err := fs.ReadDir("./assets/gen")
if err != nil {
panic(err)
}
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".txt") {
bytes, err := os.ReadFile("./assets/gen/" + file.Name())
bytes, err := fs.ReadFile("./assets/gen/" + file.Name())
if err != nil {
log.Println("Error reading file:", err.Error())
}

View File

@ -5,8 +5,8 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/samber/lo"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -20,7 +20,7 @@ func init() {
// search paths, an error is returned.
func hashFile(path string) (string, error) {
for i := range searchPaths {
data, err := ioutil.ReadFile(filepath.Join(searchPaths[i], path))
data, err := fs.ReadFile(filepath.Join(searchPaths[i], path))
if err != nil {
continue
}
@ -43,7 +43,7 @@ func hash(name string, options Options) (string, error) {
// getCache returns the cached data for the given hash.
func getCache(hash string) (interface{}, error) {
path := filepath.Join("./cache", hash)
data, err := ioutil.ReadFile(path)
data, err := fs.ReadFile(path)
if err != nil {
return nil, err
}
@ -75,5 +75,5 @@ func setCache(hash string, data interface{}) error {
}
path := filepath.Join("./cache", hash)
return ioutil.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
return fs.WriteFile(path, []byte(strings.Join(lines, "\n")))
}

View File

@ -3,7 +3,9 @@
package image
import (
"bytes"
"errors"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/imeji"
"github.com/BigJk/imeji/charmaps"
"github.com/charmbracelet/log"
@ -11,8 +13,11 @@ import (
"image"
"image/draw"
"image/gif"
_ "image/jpeg"
_ "image/png"
"os"
"path/filepath"
"runtime"
"strings"
)
@ -44,6 +49,11 @@ func buildOption(options ...Option) (Options, []imeji.Option) {
data.tag += os.Getenv("EOE_IMG_PATTERN")
}
if runtime.GOOS == "js" {
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
data.tag += "truecolor"
}
switch termenv.DefaultOutput().Profile {
case termenv.TrueColor:
imejiOptions = append(imejiOptions, imeji.WithTrueColor())
@ -82,7 +92,17 @@ func Fetch(name string, options ...Option) (string, error) {
}
for i := range searchPaths {
res, err := imeji.FileString(filepath.Join(searchPaths[i], name), imejiOptions...)
file, err := fs.ReadFile(filepath.Join(searchPaths[i], name))
if err != nil {
continue
}
image, _, err := image.Decode(bytes.NewBuffer(file))
if err != nil {
continue
}
res, err := imeji.ImageString(image, imejiOptions...)
if err == nil {
if err := setCache(hash, res); err != nil {
log.Warn("could not cache image: %s", err)
@ -113,13 +133,12 @@ func FetchAnimation(name string, options ...Option) ([]string, error) {
var frames []string
for i := range searchPaths {
f, err := os.Open(filepath.Join(searchPaths[i], name))
fileBytes, err := fs.ReadFile(filepath.Join(searchPaths[i], name))
if err != nil {
continue
}
defer f.Close()
g, err := gif.DecodeAll(f)
g, err := gif.DecodeAll(bytes.NewBuffer(fileBytes))
if err != nil {
continue
}

View File

@ -2,8 +2,8 @@ package localization
import (
"fmt"
"github.com/BigJk/end_of_eden/internal/fs"
"gopkg.in/yaml.v3"
"os"
"path/filepath"
"strings"
)
@ -44,8 +44,8 @@ func (l *Localization) Add(locale string, translations map[string]string) {
// AddFolder adds all locales from the given folder. Will walk through all
// sub-folders and add all .yaml and .yml files.
func (l *Localization) AddFolder(folder string) error {
return filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return fs.Walk(folder, func(path string, isDir bool) error {
if isDir {
return nil
}
@ -61,7 +61,7 @@ func (l *Localization) AddFolder(folder string) error {
func (l *Localization) AddFile(file string) error {
var parsed map[string]map[string]any
data, err := os.ReadFile(file)
data, err := fs.ReadFile(file)
if err != nil {
return err
}

View File

@ -0,0 +1,74 @@
//go:build js
// +build js
package browser
import (
"syscall/js"
)
type Browser struct{}
func (b Browser) LoadSettings() error {
return nil
}
func (b Browser) SaveSettings() error {
return nil
}
func (b Browser) Get(key string) any {
return js.Global().Get("settings").Call("get", key)
}
func (b Browser) GetString(key string) string {
return js.Global().Get("settings").Call("getString", key).String()
}
func (b Browser) GetStrings(key string) []string {
val := js.Global().Get("settings").Call("getStrings", key)
if val.Type() == js.TypeObject {
var result []string
for i := 0; i < val.Length(); i++ {
result = append(result, val.Index(i).String())
}
return result
}
return nil
}
func (b Browser) GetInt(key string) int {
return js.Global().Get("settings").Call("getInt", key).Int()
}
func (b Browser) GetFloat(key string) float64 {
return js.Global().Get("settings").Call("getFloat", key).Float()
}
func (b Browser) GetBool(key string) bool {
val := js.Global().Get("settings").Call("getBool", key)
if val.Type() == js.TypeBoolean {
return val.Bool()
}
return false
}
func (b Browser) Set(key string, value any) {
js.Global().Get("settings").Call("set", key, value)
}
func (b Browser) GetKeys() []string {
val := js.Global().Get("settings").Call("getKeys")
if val.Type() == js.TypeObject {
var result []string
for i := 0; i < val.Length(); i++ {
result = append(result, val.Index(i).String())
}
return result
}
return nil
}
func (b Browser) SetDefault(key string, value any) {
js.Global().Get("settings").Call("setDefault", key, value)
}

View File

@ -3,6 +3,7 @@ package eventview
import (
"fmt"
"github.com/BigJk/end_of_eden/game"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
image2 "github.com/BigJk/end_of_eden/system/image"
"github.com/BigJk/end_of_eden/ui"
@ -17,7 +18,6 @@ import (
"github.com/muesli/reflow/wordwrap"
"github.com/samber/lo"
"math"
"os"
"strconv"
"strings"
)
@ -182,7 +182,7 @@ func (m Model) eventUpdateContent() Model {
var res string
if strings.HasSuffix(file, ".ans") {
ansRes, err := os.ReadFile("./assets/images/" + file)
ansRes, err := fs.ReadFile("./assets/images/" + file)
if err != nil {
continue
}

View File

@ -3,6 +3,7 @@ package mainmenu
import (
"fmt"
"github.com/BigJk/end_of_eden/game"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
image2 "github.com/BigJk/end_of_eden/system/image"
"github.com/BigJk/end_of_eden/system/settings"
@ -91,8 +92,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case ChoiceContinue:
audio.Play("btn_menu")
if saved, err := os.ReadFile("./session.save"); err == nil {
f, err := os.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if saved, err := fs.ReadFile("./session.save"); err == nil {
f, err := fs.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
panic(err)
}
@ -119,7 +120,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
audio.Play("btn_menu")
_ = os.Mkdir("./logs", 0777)
f, err := os.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
f, err := fs.OpenFile("./logs/S "+strings.ReplaceAll(time.Now().Format(time.DateTime), ":", "-")+".txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
panic(err)
}

View File

@ -2,6 +2,7 @@ package mods
import (
"github.com/BigJk/end_of_eden/game"
"github.com/BigJk/end_of_eden/internal/fs"
"github.com/BigJk/end_of_eden/system/audio"
"github.com/BigJk/end_of_eden/system/settings"
"github.com/BigJk/end_of_eden/ui"
@ -13,7 +14,6 @@ import (
zone "github.com/lrstanley/bubblezone"
"github.com/samber/lo"
"log"
"os"
"path/filepath"
"sort"
)
@ -182,7 +182,7 @@ func (m Model) modActive(mod string) bool {
}
func (m Model) fetchMods() Model {
entries, err := os.ReadDir("./mods")
entries, err := fs.ReadDir("./mods")
if err != nil {
log.Println("Error while reading mods directory:", err)
return m

View File

@ -0,0 +1,39 @@
package warning
import (
"github.com/BigJk/end_of_eden/ui"
"github.com/BigJk/end_of_eden/ui/components"
tea "github.com/charmbracelet/bubbletea"
)
type Model struct {
ui.MenuBase
parent tea.Model
text string
}
func New(parent tea.Model, text string) Model {
return Model{parent: parent, text: text}
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.Size = msg
case tea.KeyMsg:
if msg.Type == tea.KeyEscape {
return m.parent, nil
}
}
return m, nil
}
func (m Model) View() string {
return components.Error(m.Size.Width, m.Size.Height, m.text)
}

View File

@ -51,6 +51,8 @@ func (m Model) Init() tea.Cmd {
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.size = msg
@ -73,6 +75,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.stack[curIndex], cmd = m.stack[curIndex].Update(msg)
if cmd != nil {
cmds = append(cmds, cmd)
}
if menu, ok := m.stack[curIndex].(ui.Menu); ok && !menu.HasSize() {
return m, tea.Batch(cmd, func() tea.Msg {
return m.size
@ -80,10 +86,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if m.stack[curIndex] == nil {
// If we remove the top model, we need to send a window size message to the new top model
// to avoid the layout to be broken.
cmds = append(cmds, func() tea.Msg {
return tea.WindowSizeMsg{
Width: m.size.Width,
Height: m.size.Height,
}
})
m.stack = m.stack[:len(m.stack)-1]
}
return m, cmd
return m, tea.Batch(cmds...)
}
func (m Model) View() string {