From 9c8fe27ce4f8779d58f836a68d303230d8dce9ce Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 23 Feb 2024 22:05:30 +0000 Subject: [PATCH] Command line overhaul --- Makefile | 10 +- doc/prosperon.org | 23 +++-- quickjs/quickjs.c | 8 +- readme.md | 2 +- scripts/debug.js | 118 ++++++++++++++------- scripts/editor.js | 1 + scripts/engine.js | 103 +++++++++++++++---- scripts/entity.js | 34 +++--- scripts/sound.js | 2 - scripts/std.js | 228 +++++++++++++++++++++++++++++++---------- source/engine/cbuf.h | 1 + source/engine/jsffi.c | 40 +++++++- source/engine/script.c | 26 +++-- source/engine/script.h | 6 +- source/engine/warp.c | 10 +- source/engine/yugine.c | 43 ++++---- source/engine/yugine.h | 5 +- 17 files changed, 483 insertions(+), 177 deletions(-) diff --git a/Makefile b/Makefile index 716b07e..f691a13 100755 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ CC := $(notdir $(CC)) DBG ?= 1 OPT ?= 0 +LEAK ?= 0 INFO := LD = $(CC) @@ -64,6 +65,8 @@ ifeq ($(LEAK),1) CPPFLAGS += -fsanitize=address endif +CPPFLAGS += -DLEAK=$(LEAK) + ifeq ($(OPT),small) CPPFLAGS += -Oz -flto -fno-ident -fno-asynchronous-unwind-tables LDFLAGS += -flto @@ -82,7 +85,7 @@ else endif endif -CPPFLAGS += -DHAVE_CEIL -DCP_USE_CGTYPES=0 -DCP_USE_DOUBLES=0 -DHAVE_FLOOR -DHAVE_FMOD -DHAVE_LRINT -DHAVE_LRINTF $(includeflag) -MD $(WARNING_FLAGS) -I. -DVER=\"$(VER)\" -DINFO=\"$(INFO)\" #-DENABLE_SINC_MEDIUM_CONVERTER -DENABLE_SINC_FAST_CONVERTER -DCP_COLLISION_TYPE_TYPE=uintptr_t -DCP_BITMASK_TYPE=uintptr_t +CPPFLAGS += -DHAVE_CEIL -DCP_USE_CGTYPES=0 -DCP_USE_DOUBLES=0 -DHAVE_FLOOR -DHAVE_FMOD -DHAVE_LRINT -DHAVE_LRINTF $(includeflag) -MD $(WARNING_FLAGS) -I. -DVER=\"$(VER)\" -DCOM=\"$(COM)\" -DINFO=\"$(INFO)\" #-DENABLE_SINC_MEDIUM_CONVERTER -DENABLE_SINC_FAST_CONVERTER -DCP_COLLISION_TYPE_TYPE=uintptr_t -DCP_BITMASK_TYPE=uintptr_t # ENABLE_SINC_[BEST|FAST|MEDIUM]_CONVERTER # default, fast and medium available in game at runtime; best available in editor @@ -177,7 +180,7 @@ WARNING_FLAGS = -Wno-incompatible-function-pointer-types NAME = primum$(EXT) SEM = 0.0.1 COM != fossil describe -VER = $(SEM)-$(COM) +VER = $(SEM) LDLIBS := $(addprefix -l, $(LDLIBS)) LDPATHS := $(addprefix -L, $(LDPATHS)) @@ -236,6 +239,7 @@ api.md: $(DOCMD) @rm $^ INPUT = editor DebugControls component.sprite component.polygon2d component.edge2d component.circle2d + INPUTMD := $(addsuffix .input.md, $(INPUT)) input.md: $(INPUTMD) @(echo "# Input"; cat $^) > $@ @@ -251,7 +255,7 @@ input.md: $(INPUTMD) $(BIN)/libquickjs.a: $(QUICKJS_O) make -C quickjs clean - make -C quickjs SYSRT=$(SYSRT) TTARGET=$(TTARGET) ARCH=$(ARCH) DBG=$(DBG) OPT=$(OPT) AR=$(AR) OS=$(OS) libquickjs.a libquickjs.lto.a HOST_CC=$(CC) + make -C quickjs SYSRT=$(SYSRT) TTARGET=$(TTARGET) ARCH=$(ARCH) DBG=$(DBG) OPT=$(OPT) AR=$(AR) OS=$(OS) libquickjs.a libquickjs.lto.a HOST_CC=$(CC) LEAK=$(LEAK) @mkdir -p $(BIN) cp -rf quickjs/libquickjs.* $(BIN) diff --git a/doc/prosperon.org b/doc/prosperon.org index 1089930..f9bc0a0 100644 --- a/doc/prosperon.org +++ b/doc/prosperon.org @@ -77,12 +77,18 @@ The fundamental tool for building in Prosperon is the actor system. Actors run i The most masterful actor is the *Empyrean*. The first actor you create will have the Empyrean as its master. Subsequent actors can use any other actor as its master. +| fn | description | +|--------------------+----------------------------------------------------------| +| delay(fn, seconds) | Calls 'fn' after 'seconds' with the context of the actor | + *** Actor Lifetime When an actor dies, all of the actors that have it as their master[fn::What a mouthful!] will die as well. *** Turns Actors get fragments of time called a *turn*. Actors which belong to different systems can have different lengths of turns. + + *** Actor files Actor files end with the extension *.jso*[fn::"Javascript object".]. They list a series of functions to call on a newly formed actor. Actors have a number of useful functions which are called as defined. @@ -100,8 +106,8 @@ Create a new actor, then kill it. var act_die_call = function() { console.log(`Actor ${this.id} has died.`); } -var act1 = Empyrean.new(); -var act2 = actor1.new(); +var act1 = Empyrean.spawn(); +var act2 = actor1.spawn(); act1.stop = act_die_call; act2.stop = act_die_call; Empyrean.kill(); /* Error: The Empyrean cannot be killed */ @@ -119,8 +125,8 @@ this.stop = function() { #+end_src Now spawn two actors using it. #+begin_src -var act1 = Empyrean.new("hello.jso"); -var act2 = act1.new("hello.jso"); +var act1 = Empyrean.spawn("hello.jso"); +var act2 = act1.spawn("hello.jso"); #+end_src #+end_scholium @@ -153,7 +159,7 @@ Entities can have *components*. Components are essentially javascript wrappers o #+begin_scholium For example, to render an image, set up a *sprite* component on an entity and point its path to an image on your harddrive. #+begin_src -var ent = Empyrean.new(); +var ent = Empyrean.spawn(); var spr = ent.add_component(component.sprite); spr.path = "image.png"; #+end_src @@ -249,11 +255,13 @@ Caution! Because the path is resolved during object load, you will need to fresh **** Links Links can be specified using the "#" sign. These are shortcuts you can specify for large projects. Specify them in the array ~Resources.links~. -An example is of the form ~trees:/world/assets/nature/trees~. Links are called with ~#~, so you can now make a "fern" with ~Primum.new("#trees/fern.jso")~. +An example is of the form ~trees:/world/assets/nature/trees~. Links are called with ~#~, so you can now make a "fern" with ~Primum.spawn("#trees/fern.jso")~. *** Ur auto creation Instead of coding all the ur type creation by hand, Prosperon can automatically search your project's folder and create the ur types for you. Any /[name].jso/ file is converted into an ur with the name. Any /[name].json/ file is then applied over it, should it exist. If there is a /.json/ file without a corresponding /.jso/, it can still be turned into an ur, if it is a valid ur format. +Folders and files beginning with a '.' (hidden) or a '_' will be ignored for ur creation. + The folder hierarchy of your file system determines the ur prototype chain. /.jso/ files inside of a folder will be subtyped off the folder ur name. Only one ur of any name can be created. @@ -333,6 +341,9 @@ The GUI system which ships with Prosperon is called *MUM*. MUM is a declarative, All GUI objects derive from MUM. MUM has a list of properties, used for rendering. Mum also has functions which cause drawing to appear on the screen. +** Physics +Prospeorn comes with the [[https://chipmunk-physics.net][Chipmunk]] physics engine built in. It is a fast, stable physics solution. All entities are assumed to be physics based objects, and components can be added to them to enable more physics features. + * Editor Tour Prosperon's visual editor is an assistant for the creation and editing of your game entities and actors. In the editor, all ur types are loaded, and assets are constantly monitored for changes for hot reloading. diff --git a/quickjs/quickjs.c b/quickjs/quickjs.c index 859f80a..dcca277 100644 --- a/quickjs/quickjs.c +++ b/quickjs/quickjs.c @@ -91,14 +91,16 @@ */ //#define DUMP_BYTECODE (1) /* dump the occurence of the automatic GC */ -#define DUMP_GC +//#define DUMP_GC /* dump objects freed by the garbage collector */ //#define DUMP_GC_FREE /* dump objects leaking when freeing the runtime */ +#ifdef LEAK #define DUMP_LEAKS 1 +#endif /* dump memory usage before running the garbage collector */ -#define DUMP_MEM -#define DUMP_OBJECTS /* dump objects in JS_FreeContext */ +//#define DUMP_MEM +//#define DUMP_OBJECTS /* dump objects in JS_FreeContext */ //#define DUMP_ATOMS /* dump atoms in JS_FreeContext */ //#define DUMP_SHAPES /* dump shapes in JS_FreeContext */ //#define DUMP_MODULE_RESOLVE diff --git a/readme.md b/readme.md index 427ec98..2516ac3 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ # Prosperon Engine -A developer minded, 2D-first game engine. +The easily extendable, developer minded, 2D-first game engine. See the documentation [here](https://prosperon.dev/doc). diff --git a/scripts/debug.js b/scripts/debug.js index d855d7b..a0f4ba6 100644 --- a/scripts/debug.js +++ b/scripts/debug.js @@ -1,15 +1,22 @@ /* All draw in screen space */ var Shape = { - circle(pos, radius, color) { cmd(115, pos, radius, color); }, - point(pos,size,color) { color ??= Color.blue; Shape.circle(pos,size,color); }, + line(points, color, thickness) { + thickness ??= 1; + color ??= Color.white; + cmd(83, points, color, thickness); + }, + + poly(points, color) { cmd_points(0,points,color); }, + + circle(pos, radius, color) { cmd(115, pos, radius, color); }, + /* size here is arm length - size of 2 is 4 height total */ - cross(pos, size, color, angle) { - angle ??= 0; + cross(pos, size, color) { color ??= Color.red; var a = [ pos.add([0,size]), @@ -42,8 +49,6 @@ var Shape = { Shape.line(wing1,color); Shape.line(wing2,color); }, - - poly(points, color) { cmd_points(0,points,color); }, rectangle(lowerleft, upperright, color) { var pos = lowerleft.add(upperright).map(x=>x/2); @@ -56,18 +61,17 @@ var Shape = { cmd(53, pos, wh, color); }, - line(points, color, type, thickness) { - thickness ??= 1; - type ??= 0; - color ??= Color.white; - - switch (type) { - case 0: - cmd(83, points, color, thickness); - } - }, }; +Shape.doc = "Draw shapes in screen space."; +Shape.circle.doc = "Draw a circle at pos, with a given radius and color."; +Shape.cross.doc = "Draw a cross centered at pos, with arm length size."; +Shape.arrow.doc = "Draw an arrow from start to end, with wings of length wingspan at angle wingangle."; +Shape.poly.doc = "Draw a concave polygon from a set of points."; +Shape.rectangle.doc = "Draw a rectangle, with its corners at lowerleft and upperright."; +Shape.box.doc = "Draw a box centered at pos, with width and height in the tuple wh."; +Shape.line.doc = "Draw a line from a set of points, and a given thickness."; + var Debug = { fn_break(fn, obj) { if (typeof fn !== 'function') return; @@ -352,11 +356,36 @@ console.clear = function() cmd(146); } +console.assert = function(assertion, msg, objs) +{ + if (!assertion) { + console.error(msg); + console.stack(); + } +} + +var say = function(msg) { + console.say(msg); +} + +say.doc = "Print to std out with an appended newline."; + +var gist = function(o) +{ + if (typeof o === 'object') return json.encode(o,null,1); + if (typeof o === 'string') return o; + return o.toString(); +} +gist.doc = "Return the best string gist of an object."; + var API = {}; API.doc_entry = function(obj, key) { - var d = obj.doc; - var doc = ""; + if (typeof key !== 'string') { + console.warn("Cannot print a key that isn't a string."); + return undefined; + } + var title = key; var o = obj[key]; @@ -364,23 +393,23 @@ API.doc_entry = function(obj, key) o = obj.impl[key]; var t = typeof o; - if (t === 'object' && Array.isArray(o)) t = 'array'; - - if (t === 'function') { + if (Array.isArray(o)) t = "array"; + else if (t === 'function') { title = o.toString().tofirst(')') + ")"; + title = title.fromfirst('('); + title = key + "(" + title; if (o.doc) doc = o.doc; t = ""; - } - if (t === 'undefined') t = ""; + } else if (t === 'undefined') t = ""; - if (t) t = "**" + t + "**"; + if (t) t = "**" + t + "**\n"; - if (!doc) { - if (d && d[key]) doc = d[key]; - else return ""; - } + var doc = ""; + if (o.doc) doc = o.doc; + else if (obj.doc && obj.doc[key]) doc = obj.doc[key]; + else if (Array.isArray(o)) doc = json.encode(o); - return `### \`${title}\` + return `## ${title} ${t} ${doc} `; @@ -388,19 +417,38 @@ ${doc} API.print_doc = function(name) { - var obj = eval(name); - if (!obj.doc) { - Log.warn(`Object has no doc sidecar.`); - return; + var obj = name; + if (typeof name === 'string') { + obj = eval(name); + if (!obj) { + console.warn(`Cannot print the API of '${name}', as it was not found.`); + return undefined; + } + + obj = globalThis[name]; + } + + obj = eval(name); + + if (!Object.isObject(obj)) { + console.warn("Cannot print the API of something that isn't an object."); + return undefined; } - var mdoc = "# " + name + " API #\n"; + if (!obj) { + console.warn(`Object '${name}' does not exist.`); + return; + } + + var mdoc = "# " + name + "\n"; if (obj.doc?.doc) mdoc += obj.doc.doc + "\n"; else if (typeof obj.doc === 'string') mdoc += obj.doc + "\n"; + for (var key in obj) { if (key === 'doc') continue; if (key === 'toString') continue; - mdoc += API.doc_entry(obj, key); + + mdoc += API.doc_entry(obj, key) + "\n"; } return mdoc; diff --git a/scripts/editor.js b/scripts/editor.js index 23977b1..c297594 100644 --- a/scripts/editor.js +++ b/scripts/editor.js @@ -876,6 +876,7 @@ editor.inputs['M-t'] = function() { editor.edit_level.objects.forEach(function(x editor.inputs['M-t'].doc = "Unlock all objects in current level."; editor.inputs['C-n'] = function() { + console.warn(`Spawning a new object on ${editor.edit_level.toString()}`); editor.edit_level.spawn(); }; editor.inputs['C-n'].doc = "Create an empty object."; diff --git a/scripts/engine.js b/scripts/engine.js index ddd3163..6267217 100644 --- a/scripts/engine.js +++ b/scripts/engine.js @@ -5,11 +5,19 @@ function load(file) { files[file] = modtime; } +var cmd_args = function(cc) +{ + console.warn(cc); +} + load("scripts/base.js"); load("scripts/std.js"); //load("scripts/lunr.js"); -var lunrtxt = load("scripts/lunr.js"); -eval(lunrtxt); +//var lunrtxt = load("scripts/lunr.js"); +//eval(lunrtxt); + +/* The global namespace file */ +var prosp = {}; function run(file) { @@ -23,12 +31,17 @@ function run(file) return cmd(117, file); } +run.doc = `Load a given script file.`; +load.doc = `Load a given script file.`; + function run_env(file, env) { var script = IO.slurp(file); return function(){return eval(script);}.call(env); } +run_env.doc = `Load a given script file, evaluating it in the context of the object 'env'.`; + load("scripts/diff.js"); Log.level = 1; @@ -72,6 +85,62 @@ var Device = { gamegear: [160,144,3.2], }; +Device.doc = `Device resolutions given as [x,y,inches diagonal].`; + +var prosperon = {}; +prosperon.version = cmd(255); +prosperon.revision = cmd(256); + +prosperon.semver = {}; +prosperon.semver.valid = function(v, range) +{ + v = v.split('.'); + range = range.split('.'); + if (v.length !== 3) return undefined; + if (range.length !== 3) return undefined; + + if (range[0][0] === '^') { + range[0] = range[0].slice(1); + if (parseInt(v[0]) >= parseInt(range[0])) return true; + + return false; + } + + if (range[0] === '~') { + range[0] = range[0].slice(1); + for (var i = 0; i < 2; i++) + if (parseInt(v[i]) < parseInt(range[i])) return false; + return true; + } + + return prosperon.semver.cmp(v.join('.'), range.join('.')) === 0; +} + +prosperon.semver.cmp = function(v1, v2) +{ + var ver1 = v1.split('.'); + var ver2 = v2.split('.'); + + for (var i = 0; i < 3; i++) { + var n1 = parseInt(ver1[i]); + var n2 = parseInt(ver2[i]); + if (n1 > n2) + return 1; + else if (n1 < n2) + return -1; + } + + return 0; +} + +prosperon.semver.doc = "Functions for semantic versioning numbers. Semantic versioning is given as a triple digit number, as MAJOR.MINOR.PATCH."; +prosperon.semver.cmp.doc = "Compare two semantic version numbers, given like X.X.X."; +prosperon.semver.valid.doc = `Test if semantic version v is valid, given a range. +Range is given by a semantic versioning number, prefixed with nothing, a ~, or a ^. +~ means that MAJOR and MINOR must match exactly, but any PATCH greater or equal is valid. +^ means that MAJOR must match exactly, but any MINOR and PATCH greater or equal is valid.`; + + load("scripts/gui.js"); var timer = { @@ -253,8 +322,6 @@ var Event = { }, }; -Event.observe('quit', undefined, function() { Primum.kill(); }); - var Window = { fullscreen(f) { cmd(145, f); }, set width(w) { cmd(125, w); }, @@ -411,22 +478,14 @@ Spline.bezier_is_handle = function(points, i) { return !Spline.bezier_is_node(po load("scripts/components.js"); var Game = { - init() { - if (!Game.edit) { - load("config.js"); - load("game.js"); - } - else { - load("scripts/editor.js"); - load("editorconfig.js"); - editor.enter_editor(); - } + engine_start(fn) { + cmd(257, fn); + Sound.bus.master = cmd(180); + Sound.master = Sound.bus.master; }, native: Device.pc, - edit: true, - object_count() { return cmd(214); }, @@ -476,6 +535,9 @@ var Game = { }, }; +Game.gc = function() { cmd(259); } +Game.gc.doc = "Force the garbage collector to run."; + Game.doc = {}; Game.doc.object = "Returns the entity belonging to a given id."; Game.doc.quit = "Immediately quit the game."; @@ -511,7 +573,11 @@ function world_start() { Primum.ur = "Primum"; Primum.kill = function() { this.clear(); }; Primum.phys = 2; + gameobject.level = Primum; + gameobject.body = make_gameobject(); + cmd(113,gameobject.body, gameobject); + Object.hide(gameobject, 'timescale'); } load("scripts/physics.js"); @@ -525,8 +591,3 @@ Game.view_camera = function(cam) Window.name = "Prosperon (V0.1)"; Window.width = 1280; Window.height = 720; - -var Asset = {}; -Asset.doc = { - doc: "Functions to manage the loading and unloading of assets, like sounds and images." -}; diff --git a/scripts/entity.js b/scripts/entity.js index 6a3b771..88e3f4b 100644 --- a/scripts/entity.js +++ b/scripts/entity.js @@ -1,4 +1,4 @@ -function obj_unique_name(name, obj) +prosp.obj_unique_name = function(name, obj) { name = name.replaceAll('.', '_'); if (!(name in obj)) return name; @@ -27,9 +27,13 @@ actor.spawn = function(script, config){ this.padawans.push(padawan); return padawan; }; -actor.die = function(actor){ - + +actor.spawn.doc = `Create a new actor, using this actor as the master, initializing it with 'script' and with data (as a JSON or Nota file) from 'config'.`; + +actor.die = function(e){ + e.kill(); }; + actor.timers = []; actor.kill = function(){ this.timers.forEach(t => t.kill()); @@ -38,6 +42,8 @@ actor.kill = function(){ this.__dead__ = true; }; +actor.kill.doc = `Remove this actor and all its padawans from existence.`; + actor.delay = function(fn, seconds) { var t = Object.create(timer); t.remain = seconds; @@ -55,6 +61,8 @@ actor.delay = function(fn, seconds) { return function() { t.kill(); }; }; +actor.delay.doc = `Call 'fn' after 'seconds' with 'this' set to the actor.`; + actor.master = undefined; actor.padawans = []; @@ -682,7 +690,7 @@ var gameobject = { add_component(comp, data) { data ??= undefined; if (typeof comp.make !== 'function') return; - var name = obj_unique_name(comp.toString(), this); + var name = prosp.obj_unique_name(comp.toString(), this); this[name] = comp.make(this); this[name].comp = comp.toString(); this.components[name] = this[name]; @@ -699,10 +707,6 @@ var gameobject = { } Object.mixin(gameobject,gameobject_impl); -gameobject.body = make_gameobject(); -cmd(113,gameobject.body, gameobject); -Object.hide(gameobject, 'timescale'); - gameobject.spawn.doc = `Spawn an entity of type 'ur' on this entity. Returns the spawned entity.`; gameobject.doc = { @@ -865,14 +869,10 @@ prototypes.file2ur = function(file) return file; } -/* Returns an ur, or makes it, for any given type of path - could be a file on a disk like ball/big.js - could be an ur path like ball.big -*/ prototypes.get_ur = function(name) { if (!name) { - Log.warn(`Can't get ur from an undefined.`); + console.error(`Can't get ur from an undefined.`); return; } var urpath = name; @@ -884,13 +884,17 @@ prototypes.get_ur = function(name) if (ur) return ur; else { - Log.warn(`Could not find prototype using name ${name}.`); + console.warn(`Could not find prototype using name ${name}.`); return undefined; } } else return prototypes.ur[urpath]; } +prototypes.get_ur.doc = `Returns an ur, or makes it, for any given type of path + could be a file on a disk like ball/big.js + could be an ur path like ball.big`; + prototypes.get_ur_file = function(path, ext) { var urpath = prototypes.ur2file(path); @@ -908,6 +912,8 @@ prototypes.generate_ur = function(path) ob = ob.map(function(path) { return path.set_ext(""); }); ob = ob.map(function(path) { return path[0] !== '.' ? path : undefined; }); + ob = ob.map(function(path) { return path[0] !== '_' ? path : undefined; }); + ob = ob.filter(x => x !== undefined); ob.forEach(function(name) { prototypes.get_ur(name); }); } diff --git a/scripts/sound.js b/scripts/sound.js index 2719e9e..225dcaa 100644 --- a/scripts/sound.js +++ b/scripts/sound.js @@ -77,8 +77,6 @@ var DSP = { }, }; -Sound.bus.master = cmd(180); -Sound.master = Sound.bus.master; Sound.play.doc = "Play the given file once."; Sound.doc = {}; diff --git a/scripts/std.js b/scripts/std.js index 41ce304..42195b0 100644 --- a/scripts/std.js +++ b/scripts/std.js @@ -15,6 +15,8 @@ function buf2hex(buffer) { // buffer is an ArrayBuffer var OS = {}; OS.cwd = function() { return cmd(144); } OS.exec = function(s) { cmd(143, s); } +OS.cwd.doc = "Get the absolute path of the current working directory."; +OS.exec.doc = "Run a command line instruction, and return when it finishes."; var Resources = {}; Resources.images = ["png", "jpg", "jpeg", "gif"]; @@ -24,6 +26,7 @@ Resources.is_image = function(path) { var ext = path.ext(); return Resources.images.any(x => x === ext); } + Resources.is_sound = function(path) { var ext = path.ext(); return Resources.sounds.any(x => x === ext); @@ -91,13 +94,8 @@ var Log = { cmd(91,msg); }, - say(msg) { - Log.write(msg + '\n'); - }, - - repl(msg) { - cmd(142, msg + '\n'); - }, + say(msg) { Log.write(msg + '\n'); }, + repl(msg) { cmd(142, msg + '\n'); }, stack(skip = 0) { var err = new Error(); @@ -160,6 +158,12 @@ var IO = { paths = paths.filter(function(str) { return str.ext() === ext; }); return paths; }, + compile(script) { + return cmd(260, script); + }, + run_bytecode(byte_file) { + return cmd(261, byte_file); + }, ls() { return cmd(66); }, /* Only works on text files currently */ cp(f1, f2) { @@ -171,6 +175,9 @@ var IO = { rm(f) { return cmd(f); }, + mkdir(dir) { + cmd(258, dir); + }, glob(pat) { var paths = IO.ls(); pat = pat.replaceAll(/([\[\]\(\)\^\$\.\|\+])/g, "\\$1"); @@ -187,7 +194,12 @@ IO.doc = { doc: "Functions for filesystem input/output commands.", exists: "Returns true if a file exists.", slurp: "Returns the contents of given file as a string.", + slurpbytes: "Return the contents of a file as a byte array.", slurpwrite: "Write a given string to a given file.", + cp: "Copy file f1 to f2.", + mv: "Rename file f1 to f2.", + rm: "Remove file f.", + mkdir: "Make dir.", ls: "List contents of the game directory.", glob: "Glob files in game directory.", }; @@ -214,6 +226,7 @@ Parser.replstrs = function(path) var Cmdline = {}; Cmdline.cmds = []; +Cmdline.orders = {}; Cmdline.register_cmd = function(flag, fn, doc) { Cmdline.cmds.push({ flag: flag, @@ -222,10 +235,160 @@ Cmdline.register_cmd = function(flag, fn, doc) { }); }; +Cmdline.register_order = function(order, fn, doc, usage) { + Cmdline.orders[order] = fn; + fn.doc = doc; + usage ??= ""; + fn.usage = `${order} ${usage}`; +} + +Cmdline.register_order("edit", function() { + if (!IO.exists(".prosperon")) { + IO.mkdir(".prosperon"); + var project = {}; + project.version = prosperon.version; + project.revision = prosperon.revision; + IO.slurpwrite(".prosperon/project", json.encode(project)); + } + Game.engine_start(function() { + load("scripts/editor.js"); + load("editorconfig.js"); + editor.enter_editor(); + }); +}, "Edit the project in this folder. Give it the name of an UR to edit that specific object.", "?UR?"); + +Cmdline.register_order("play", function() { + if (!IO.exists(".prosperon")) { + IO.mkdir(".prosperon"); + var project = {}; + project.version = prosperon.version; + project.revision = prosperon.revision; + IO.slurpwrite(".prosperon/project", json.encode(project)); + } + Game.engine_start(function() { + load("config.js"); + load("game.js"); + }); +}, "Play the game present in this folder."); + +Cmdline.register_order("pack", function(str) { + var packname; + if (str.length === 0) + packname = "test.cdb"; + else if (str.length > 1) { + Log.warn("Give me a single filename for the pack."); + return; + } else + packname = str[0]; + + say(`Packing into ${packname}`); + + cmd(124, packname); +}, "Pack the game into the given name.", "NAME"); + +Cmdline.register_order("unpack", function() { + say("Unpacking not implemented."); +}, "Unpack this binary's contents into this folder for editing."); + +Cmdline.register_order("build", function() { + say("Building not implemented."); +}, "Build static assets for this project."); + +Cmdline.register_order("api", function(obj) { + if (!obj[0]) + Cmdline.print_order("api"); + + load("scripts/editor.js"); + var api = API.print_doc(obj[0]); + if (!api) + return; + + say(api); +}, "Print the API for an object as markdown. Give it a file to save the output to.", "OBJECT"); + +Cmdline.register_order("compile", function(argv) { + for (var file of argv) { + var comp = IO.compile(file); + IO.slurpwrite(file + ".byte", comp); + } +}, "Compile one or more provided files into bytecode.", "FILE ..."); + +Cmdline.register_order("input", function(pawn) { + load("scripts/editor.js"); + say(`## Input for ${pawn}`); + eval(`say(Input.print_md_kbm(${pawn}));`); +}, "Print input documentation for a given object as markdown. Give it a file to save the output to", "OBJECT ?FILE?"); + +Cmdline.register_order("run", function(script) { + script = script.join(" "); + if (!script) { + say("Need something to run."); + return; + } + + if (IO.exists(script)) + try { + if (script.endswith(".byte")) + cmd(261, script); + else + run(script); + } catch(e) { } + else { + var ret = eval(script); + if (ret) say(ret); + } +}, "Run a given script. SCRIPT can be the script itself, or a file containing the script", "SCRIPT"); + +Cmdline.print_order = function(fn) +{ + if (typeof fn === 'string') + fn = Cmdline.orders[fn]; + + if (!fn) return; + say(`Usage: prosperon ${fn.usage}`); + say(fn.doc); +} + +Cmdline.register_order("help", function(order) { + + if (!Object.empty(order)) { + var orfn = Cmdline.orders[order]; + + if (!orfn) { + console.warn(`No command named ${order}.`); + return; + } + + Cmdline.print_order(orfn); + return; + } + + Cmdline.print_order("help"); + + for (var cmd of Object.keys(Cmdline.orders).sort()) + say(cmd); + + Cmdline.orders.version(); +}, "Give help with a specific command.", "TOPIC"); + +Cmdline.register_order("version", function() { + say(`Prosperon version ${prosperon.version} [${prosperon.revision}]`); +}, "Display Prosperon info."); + function cmd_args(cmdargs) { var play = false; - var cmds = cmdargs.split(" "); + var cmds = cmdargs.split(/\s+/).slice(1); + + if (cmds.length === 0) + cmds[0] = "play"; + else if (!Cmdline.orders[cmds[0]]) { + console.warn(`Command ${cmds[0]} not found.`); + return; + } + + Cmdline.orders[cmds[0]](cmds.slice(1)); + return; for (var i = 1; i < cmds.length; i++) { if (cmds[i][0] !== '-') { @@ -256,51 +419,10 @@ STD.exit = function(status) { cmd(147,status); } -Cmdline.register_cmd("p", function() { Game.edit = false; }, "Launch engine in play mode."); -Cmdline.register_cmd("v", function() { Log.say(cmd(120)); STD.exit(0);}, "Display engine info."); + Cmdline.register_cmd("l", function(n) { Log.level = n; }, "Set log level."); -Cmdline.register_cmd("h", function(str) { - for (var cmd of Cmdline.cmds) { - Log.say(`-${cmd.flag}: ${cmd.doc}`); - } - STD.exit(0); -}, -"Help."); -Cmdline.register_cmd("b", function(str) { - var packname; - if (str.length === 0) - packname = "test.cdb"; - else if (str.length > 1) { - Log.warn("Give me a single filename for the pack."); - Game.quit(); - } else - packname = str[0]; - - Log.warn(`Packing into ${packname}`); - - cmd(124, packname); - STD.exit(0); -}, "Pack the game into the given name."); - -Cmdline.register_cmd("e", function(pawn) { - load("scripts/editor.js"); - Log.write(`## Input for ${pawn}\n`); - eval(`Log.write(Input.print_md_kbm(${pawn}));`); - STD.exit(0); -}, "Print input documentation for a given object in a markdown table." ); - -Cmdline.register_cmd("t", function() { - Log.warn("Testing not implemented yet."); - STD.exit(0); -}, "Test suite."); - -Cmdline.register_cmd("d", function(obj) { - load("scripts/editor.js"); - Log.say(API.print_doc(obj[0])); - STD.exit(0); -}, "Print documentation for an object."); Cmdline.register_cmd("cjson", function(json) { var f = json[0]; @@ -329,9 +451,3 @@ Cmdline.register_cmd("cjson", function(json) { STD.exit(0); }, "Clean up a jso file."); - -Cmdline.register_cmd("r", function(script) { - try { run(script); } catch(e) { STD.exit(0); } - - STD.exit(0); -}, "Run a script."); diff --git a/source/engine/cbuf.h b/source/engine/cbuf.h index 5da1891..857d117 100644 --- a/source/engine/cbuf.h +++ b/source/engine/cbuf.h @@ -1,6 +1,7 @@ #ifndef CIRCBUF_H #define CIRCBUF_H +#include #include static inline unsigned int powof2(unsigned int x) diff --git a/source/engine/jsffi.c b/source/engine/jsffi.c index afd79d6..0db62e6 100644 --- a/source/engine/jsffi.c +++ b/source/engine/jsffi.c @@ -26,6 +26,9 @@ #include "resources.h" #include +#include +#include + #include "nota.h" #include "render.h" @@ -1081,9 +1084,6 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) ret = JS_NewInt64(js, file_mod_secs(str)); break; - case 120: - ret = str2js(engine_info()); - break; case 121: ret = number2js(get_timescale()); break; @@ -1194,7 +1194,6 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) break; case 147: - YughWarn("EXITING"); exit(js2int(argv[1])); break; case 148: @@ -1435,6 +1434,34 @@ JSValue duk_cmd(JSContext *js, JSValueConst this, int argc, JSValueConst *argv) case 254: ret = warp_damp2js(warp_damp_make()); break; + + case 255: + ret = str2js(VER); + break; + + case 256: + ret = str2js(COM); + break; + case 257: + engine_start(argv[1]); + break; + case 258: + str = js2str(argv[1]); + ret = int2js(mkdir(str, 0777)); + break; + case 259: + script_gc(); + break; + case 260: + str = js2str(argv[1]); + d1 = script_compile(str, &plen); + ret = JS_NewArrayBufferCopy(js, d1, plen); + break; + case 261: + str = js2str(argv[1]); + d1 = slurp_file(str, &plen); + return script_run_bytecode(d1, plen); + break; } if (str) JS_FreeCString(js, str); @@ -1816,7 +1843,6 @@ GETSET_PAIR(warp_gravity, planar_force, vec3) #define MIST_CGETSET_DEF(name, fgetter, fsetter) { name, JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE, JS_DEF_CGETSET, 0, .u = { .getset = { .get = { .getter = fgetter }, .set = { .setter = fsetter } } } } - #define CGETSET_ADD(ID, ENTRY) MIST_CGETSET_DEF(#ENTRY, ID##_get_##ENTRY, ID##_set_##ENTRY) static const JSCFunctionListEntry js_warp_gravity_funcs [] = { @@ -1873,6 +1899,10 @@ JSValue js_emitter_emit(JSContext *js, JSValueConst this, int argc, JSValue *arg return JS_UNDEFINED; } +static const JSCFunctionListEntry js_global_funcs[] = { + +}; + static const JSCFunctionListEntry js_emitter_funcs[] = { CGETSET_ADD(emitter, life), CGETSET_ADD(emitter, life_var), diff --git a/source/engine/script.c b/source/engine/script.c index 3f9a60e..bd01b9c 100644 --- a/source/engine/script.c +++ b/source/engine/script.c @@ -26,7 +26,7 @@ JSRuntime *rt = NULL; #ifndef NDEBUG #define JS_EVAL_FLAGS JS_EVAL_FLAG_STRICT #else -#define JS_EVAL_FLAGS JS_EVAL_FLAG_STRICT// | JS_EVAL_FLAG_STRIP +#define JS_EVAL_FLAGS JS_EVAL_FLAG_STRICT | JS_EVAL_FLAG_STRIP #endif static struct { @@ -76,8 +76,11 @@ void script_stop() ffi_stop(); for (int i = 0; i < shlen(jsstrs); i++) JS_FreeValue(js,jsstrs[i].value); + +#if LEAK JS_FreeContext(js); JS_FreeRuntime(rt); +#endif } void script_gc() @@ -134,16 +137,25 @@ void script_evalf(const char *format, ...) JS_FreeValue(js,obj); } -uint8_t *compile_script(const char *file, size_t *len) { - char *script = slurp_text(file, len); - JSValue obj = JS_Eval(js, script, *len, file, JS_EVAL_FLAG_COMPILE_ONLY | JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAGS); +uint8_t *script_compile(const char *file, size_t *len) { + size_t file_len; + char *script = slurp_text(file, &file_len); + JSValue obj = JS_Eval(js, script, file_len, file, JS_EVAL_FLAG_COMPILE_ONLY | JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAGS); free(script); - size_t out_len; - uint8_t *out = JS_WriteObject(js, &out_len, obj, JS_WRITE_OBJ_BYTECODE); + uint8_t *out = JS_WriteObject(js, len, obj, JS_WRITE_OBJ_BYTECODE); JS_FreeValue(js,obj); return out; } +JSValue script_run_bytecode(uint8_t *code, size_t len) +{ + JSValue b = JS_ReadObject(js, code, len, JS_READ_OBJ_BYTECODE); + JSValue ret = JS_EvalFunction(js, b); + js_print_exception(ret); + JS_FreeValue(js,b); + JS_FreeValue(js,ret); +} + struct callee stacktrace_callee; time_t file_mod_secs(const char *file) { @@ -265,7 +277,7 @@ void script_call_fn_arg(JSValue fn, JSValue arg) void out_memusage(const char *file) { - FILE *f = fopen(file, "w"); + FILE *f = fopen_mkdir(file, "w"); JSMemoryUsage jsmem; JS_ComputeMemoryUsage(rt, &jsmem); JS_DumpMemoryUsage(f, &jsmem, rt); diff --git a/source/engine/script.h b/source/engine/script.h index 21b934e..c04013e 100644 --- a/source/engine/script.h +++ b/source/engine/script.h @@ -2,7 +2,6 @@ #define SCRIPT_H #include "quickjs/quickjs.h" -#include #include extern JSContext *js; @@ -67,12 +66,15 @@ void call_nk_gui(); void unregister_obj(JSValue obj); void send_signal(const char *signal, int argc, JSValue *argv); +void script_gc(); void register_physics(struct callee c); void call_physics(double dt); void register_draw(struct callee c); void call_draw(); -uint8_t *compile_script(const char *file, size_t *len); + +JSValue script_run_bytecode(uint8_t *code, size_t len); +uint8_t *script_compile(const char *file, size_t *len); #endif diff --git a/source/engine/warp.c b/source/engine/warp.c index e4cf27a..57f29da 100644 --- a/source/engine/warp.c +++ b/source/engine/warp.c @@ -21,7 +21,15 @@ warp_damp *warp_damp_make() } void warp_damp_free(warp_damp *d) { free(d); } -void warp_gravity_free(warp_gravity *n) { free(n); } +void warp_gravity_free(warp_gravity *n) { + for (int i = 0; i < arrlen(warps); i++) { + if (warps[i] == n) { + arrdelswap(warps, i); + break; + } + } + free(n); +} HMM_Vec3 warp_damp_force(warp_damp *d, HMM_Vec3 pos, HMM_Vec3 vel) { diff --git a/source/engine/yugine.c b/source/engine/yugine.c index 176681f..4368a2c 100644 --- a/source/engine/yugine.c +++ b/source/engine/yugine.c @@ -81,10 +81,6 @@ static int sim_play = SIM_PLAY; int editor_mode = 0; -#define ENGINEINFO "Yugine version " VER ", " INFO " build.\nCopyright 2022-2024." - -const char *engine_info() { return ENGINEINFO; } - static int argc; static char **args; @@ -94,17 +90,18 @@ void seghandle() exit(1); } -void c_init() { +static JSValue c_init_fn; +void c_init() { input_init(); script_evalf("world_start();"); - render_init(); window_set_icon("icons/moon.gif"); window_resize(sapp_width(), sapp_height()); - script_evalf("Game.init();"); - particle_init(); + + if (!JS_IsUndefined(c_init_fn)) + script_call_sym(c_init_fn); } int frame_fps() { return 1.0/sapp_frame_duration(); } @@ -158,7 +155,7 @@ void c_frame() void c_clean() { gif_rec_end("out.gif"); - out_memusage("jsmem.txt"); + out_memusage(".prosperon/jsmem.txt"); script_stop(); saudio_shutdown(); sg_shutdown(); @@ -250,7 +247,7 @@ static sapp_desc start_desc = { .high_dpi = 0, .sample_count = 1, .fullscreen = 1, - .window_title = "Primum Machinam", + .window_title = "Prosperon", .enable_clipboard = false, .clipboard_size = 0, .enable_dragndrop = true, @@ -303,14 +300,10 @@ sprintf(da.details, "COMPetitive"); dam->update_activity(dam, &da, NULL, NULL); #endif - stm_setup(); /* time */ - start_t = frame_t = stm_now(); - physlast = updatelast = start_t; - sound_init(); resources_init(); - phys2d_init(); - script_startup(); + script_startup(); + int argsize = 0; for (int i = 0; i < argc; i++) { argsize += strlen(argv[i]); @@ -324,15 +317,27 @@ dam->update_activity(dam, &da, NULL, NULL); strcat(cmdstr, argv[i]); if (argc > i+1) strcat(cmdstr, " "); } - script_evalf("cmd_args('%s');", cmdstr); + out_memusage(".prosperon/jsmem.txt"); + script_stop(); + + return 0; +} + +void engine_start(JSValue fn) +{ + c_init_fn = fn; + stm_setup(); /* time */ + start_t = frame_t = stm_now(); + physlast = updatelast = start_t; + sound_init(); + phys2d_init(); + start_desc.width = mainwin.width; start_desc.height = mainwin.height; start_desc.fullscreen = 0; sapp_run(&start_desc); - - return 0; } double apptime() { return stm_sec(stm_diff(stm_now(), start_t)); } diff --git a/source/engine/yugine.h b/source/engine/yugine.h index 637848d..fe501e5 100644 --- a/source/engine/yugine.h +++ b/source/engine/yugine.h @@ -1,6 +1,8 @@ #ifndef YUGINE_H #define YUGINE_H +#include "script.h" + int sim_playing(); int sim_paused(); void sim_start(); @@ -10,9 +12,8 @@ void sim_step(); int phys_stepping(); void set_timescale(float val); void print_stacktrace(); -void yugine_init(); +void engine_start(JSValue fn); /* fn runs after the engine starts */ -const char *engine_info(); void app_name(const char *name); int frame_fps();