diff --git a/.gitignore b/.gitignore index e154414..cfa5865 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ node_modules/ cpu* heap* profile* +file_index.json \ No newline at end of file diff --git a/cmd/game_wasm/index.html b/cmd/game_wasm/index.html new file mode 100644 index 0000000..81b8bf3 --- /dev/null +++ b/cmd/game_wasm/index.html @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/cmd/game_wasm/main.go b/cmd/game_wasm/main.go new file mode 100644 index 0000000..f12f0ad --- /dev/null +++ b/cmd/game_wasm/main.go @@ -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) + } +} diff --git a/cmd/game_wasm/wasm_exec.js b/cmd/game_wasm/wasm_exec.js new file mode 100644 index 0000000..e6c8921 --- /dev/null +++ b/cmd/game_wasm/wasm_exec.js @@ -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; + }; + } + } +})(); diff --git a/cmd/game_win/main.go b/cmd/game_win/main.go index d14cf77..6137e6c 100644 --- a/cmd/game_win/main.go +++ b/cmd/game_win/main.go @@ -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 { diff --git a/game/lua.go b/game/lua.go index ce6d950..357ad09 100644 --- a/game/lua.go +++ b/game/lua.go @@ -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 diff --git a/game/mod.go b/game/mod.go index 0afe0fc..0c8c43f 100644 --- a/game/mod.go +++ b/game/mod.go @@ -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 } diff --git a/game/resources.go b/game/resources.go index db86f3e..d99eea4 100644 --- a/game/resources.go +++ b/game/resources.go @@ -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) } diff --git a/game/session.go b/game/session.go index b6c876f..856a8fd 100644 --- a/game/session.go +++ b/game/session.go @@ -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) } } diff --git a/go.mod b/go.mod index d3857f6..f03809d 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 925abd5..9bfe9b4 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/fs/fileinfo.go b/internal/fs/fileinfo.go new file mode 100644 index 0000000..5660800 --- /dev/null +++ b/internal/fs/fileinfo.go @@ -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 +} diff --git a/internal/fs/fs.go b/internal/fs/fs.go new file mode 100644 index 0000000..1bf5cc5 --- /dev/null +++ b/internal/fs/fs.go @@ -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()) + }) +} diff --git a/internal/fs/fs_js.go b/internal/fs/fs_js.go new file mode 100644 index 0000000..73302d9 --- /dev/null +++ b/internal/fs/fs_js.go @@ -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 +} diff --git a/internal/misc/build_index.sh b/internal/misc/build_index.sh new file mode 100755 index 0000000..44112da --- /dev/null +++ b/internal/misc/build_index.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Check if the argument is provided +if [ -z "$1" ]; then + echo "Usage: $0 " + 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" diff --git a/system/audio/audio.go b/system/audio/audio.go index 8d9626d..34174da 100644 --- a/system/audio/audio.go +++ b/system/audio/audio.go @@ -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 { diff --git a/system/audio/audio_js.go b/system/audio/audio_js.go new file mode 100644 index 0000000..fba3a2d --- /dev/null +++ b/system/audio/audio_js.go @@ -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 + }) +} diff --git a/system/audio/empty.go b/system/audio/empty.go index 403c10c..9c71af3 100644 --- a/system/audio/empty.go +++ b/system/audio/empty.go @@ -1,5 +1,5 @@ -//go:build !no_audio -// +build !no_audio +//go:build !no_audio && !js +// +build !no_audio,!js package audio diff --git a/system/gen/faces/faces.go b/system/gen/faces/faces.go index 6c782d8..26f65af 100644 --- a/system/gen/faces/faces.go +++ b/system/gen/faces/faces.go @@ -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 } diff --git a/system/gen/gen.go b/system/gen/gen.go index fb5af58..b12a4d8 100644 --- a/system/gen/gen.go +++ b/system/gen/gen.go @@ -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()) } diff --git a/system/image/cache.go b/system/image/cache.go index 0464485..b52d93a 100644 --- a/system/image/cache.go +++ b/system/image/cache.go @@ -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"))) } diff --git a/system/image/image.go b/system/image/image.go index 9278e82..3cd1022 100644 --- a/system/image/image.go +++ b/system/image/image.go @@ -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 } diff --git a/system/localization/localization.go b/system/localization/localization.go index bdfa88c..7ce1313 100644 --- a/system/localization/localization.go +++ b/system/localization/localization.go @@ -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 } diff --git a/system/settings/browser/browser.go b/system/settings/browser/browser.go new file mode 100644 index 0000000..07854c8 --- /dev/null +++ b/system/settings/browser/browser.go @@ -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) +} diff --git a/ui/menus/eventview/eventview.go b/ui/menus/eventview/eventview.go index 8efb0fe..4031105 100644 --- a/ui/menus/eventview/eventview.go +++ b/ui/menus/eventview/eventview.go @@ -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 } diff --git a/ui/menus/mainmenu/mainmenu.go b/ui/menus/mainmenu/mainmenu.go index 9d3f649..dbc6d47 100644 --- a/ui/menus/mainmenu/mainmenu.go +++ b/ui/menus/mainmenu/mainmenu.go @@ -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) } diff --git a/ui/menus/mods/mods.go b/ui/menus/mods/mods.go index afc0c92..3e602be 100644 --- a/ui/menus/mods/mods.go +++ b/ui/menus/mods/mods.go @@ -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 diff --git a/ui/menus/warning/warning.go b/ui/menus/warning/warning.go new file mode 100644 index 0000000..906baf0 --- /dev/null +++ b/ui/menus/warning/warning.go @@ -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) +} diff --git a/ui/root/root.go b/ui/root/root.go index 9387b3d..956fed8 100644 --- a/ui/root/root.go +++ b/ui/root/root.go @@ -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 {