From 1142bfb8963e7f27c908891eebe678bd3d5135ee Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Thu, 25 Jul 2024 17:53:53 -0500 Subject: [PATCH] made engine into a small bootstrap script --- scripts/base.js | 5 - scripts/debug.js | 2 +- scripts/engine.js | 671 ++++--------------------------------------- scripts/entity.js | 7 +- scripts/profile.js | 46 ++- scripts/prosperon.js | 425 +++++++++++++++++++++++++++ scripts/render.js | 204 ++++++++++++- 7 files changed, 714 insertions(+), 646 deletions(-) create mode 100644 scripts/prosperon.js diff --git a/scripts/base.js b/scripts/base.js index 950f6a1..db21c52 100644 --- a/scripts/base.js +++ b/scripts/base.js @@ -824,10 +824,6 @@ Object.defineProperty(String.prototype, 'sub', { } }); -Object.defineProperty(String.prototype, 'rm', { - value: function(index, endidx = index+1) { return this.slice(0,index) + this.slice(endidx); } -}); - Object.defineProperty(String.prototype, 'updir', { value: function() { if (this.lastIndexOf('/') === this.length-1) @@ -1647,7 +1643,6 @@ Math.sign = function(n) { return n >= 0 ? 1 : -1; } return { convert, time, - json, Vector, bbox, yaml diff --git a/scripts/debug.js b/scripts/debug.js index f41babc..128cbe6 100644 --- a/scripts/debug.js +++ b/scripts/debug.js @@ -50,7 +50,7 @@ debug.draw = function() { "EDIT", [0, 0], 1); } -function assert(op, str = `assertion failed [value '${op}']`) +var assert = function(op, str = `assertion failed [value '${op}']`) { if (!op) console.panic(str); diff --git a/scripts/engine.js b/scripts/engine.js index 4563e3f..d841d8a 100644 --- a/scripts/engine.js +++ b/scripts/engine.js @@ -1,5 +1,9 @@ "use math"; +Object.defineProperty(String.prototype, 'rm', { + value: function(index, endidx = index+1) { return this.slice(0,index) + this.slice(endidx); } +}); + Object.defineProperty(String.prototype, "tolast", { value: function (val) { var idx = this.lastIndexOf(val); @@ -25,12 +29,23 @@ Object.defineProperty(String.prototype, "folder", { globalThis.Resources = {}; -Resources.rm_fn = function(fnstr, text) +Resources.rm_fn = function(fn, text) { - while (text.match(fnstr)) { - } -} + var reg = new RegExp(fn.source + "\\s*\\("); + var match; + while (match = text.match(reg)) { + var last = match.index+match[0].length; + var par = 1; + while (par !== 0) { + if (text[last] === '(') par++; + if (text[last] === ')') par--; + last++; + } + text = text.rm(match.index, last); + } + return text; +} Resources.rm_fn.doc = "Remove calls to a given function from a given text script."; Resources.replpath = function (str, path) { @@ -58,6 +73,9 @@ Resources.replstrs = function (path) { var stem = path.dir(); // remove console statements + script = Resources.rm_fn(/console\.(spam|info|warn|error)/, script); + script = Resources.rm_fn(/profile\.(cache|frame|endcache|endframe)/, script); + script = Resources.rm_fn(/assert/, script); //script = script.replace(/console\.(.*?)\(.*?\)/g, ''); //script = script.replace(/assert\(.*?\)/g, ''); @@ -122,8 +140,6 @@ Resources.find_script = function (file) { return find_ext(file, Resources.scripts); }; -var t_units = ["ns", "us", "ms", "s", "m", "h"]; - console.transcript = ""; console.say = function (msg) { msg += "\n"; @@ -200,638 +216,55 @@ console.doc = { globalThis.global = globalThis; -var use_prof = "USE"; +var use_cache = {}; -profile.addreport = function(){}; - -function use(file, env = {}, script) { +globalThis.use = function(file, env = {}, script) { file = Resources.find_script(file); - var st = profile.now(); + profile.cache("USE", file); - if (use.cache[file]) { - var ret = use.cache[file].call(env); - profile.addreport(use_prof, file, st); + if (use_cache[file]) { + var ret = use_cache[file].call(env); return; } script ??= Resources.replstrs(file); + script = `(function() { var self = this; ${script}; })`; var fn = os.eval(file, script); - use.cache[file] = fn; + use_cache[file] = fn; var ret = fn.call(env); - profile.addreport(use_prof, file, st); + profile.endcache(); return ret; } -use.cache = {}; +function stripped_use (file, env = {}, script) { + file = Resources.find_script(file); -global.check_registers = function (obj) { - for (var reg in Register.registries) { - if (typeof obj[reg] === 'function') { - var fn = obj[reg].bind(obj); - var name = obj.ur ? obj.ur.name : obj.toString(); - obj.timers.push(Register.registries[reg].register(fn, name)); - } + if (use_cache[file]) { + var ret = use_cache[file].call(env); + return; } - - for (var k in obj) { - if (!k.startsWith("on_")) continue; - var signal = k.fromfirst("on_"); - Event.observe(signal, obj, obj[k]); - } -}; + script ??= Resources.replstrs(file); -Object.assign(global, use("scripts/base")); -global.obscure("global"); -global.mixin("scripts/profile"); -global.mixin("scripts/render"); -global.mixin("scripts/debug"); + script = `(function() { var self = this; ${script}; })`; + var fn = os.eval(file, script); + var ret = fn.call(env); + profile.endcache(); + return ret; +} -var frame_t = profile.secs(profile.now()); - -var sim = {}; -sim.mode = "play"; -sim.play = function () { - this.mode = "play"; - os.reindex_static(); -}; -sim.playing = function () { - return this.mode === "play"; -}; -sim.pause = function () { - this.mode = "pause"; -}; -sim.paused = function () { - return this.mode === "pause"; -}; -sim.step = function () { - this.mode = "step"; -}; -sim.stepping = function () { - return this.mode === "step"; -}; - -var physlag = 0; - -var gggstart = game.engine_start; -game.engine_start = function (s) { - game.startengine = 1; - gggstart( - function () { - global.mixin("scripts/sound.js"); - world_start(); - window.set_icon(os.make_texture("icons/moon.gif")); - Object.readonly(window.__proto__, "vsync"); - Object.readonly(window.__proto__, "enable_dragndrop"); - Object.readonly(window.__proto__, "enable_clipboard"); - Object.readonly(window.__proto__, "high_dpi"); - Object.readonly(window.__proto__, "sample_count"); - s(); - - shape.quad = { - pos: os.make_buffer([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0], 0), - verts: 4, - uv: os.make_buffer([0, 1, 1, 1, 0, 0, 1, 0], 2), - index: os.make_buffer([0, 1, 2, 2, 1, 3], 1), - count: 6, - }; - - shape.triangle = { - pos: os.make_buffer([0, 0, 0, 0.5, 1, 0, 1, 0, 0], 0), - uv: os.make_buffer([0, 0, 0.5, 1, 1, 0], 2), - verts: 3, - count: 3, - index: os.make_buffer([0, 2, 1], 1), - }; - - shape.centered_quad = { - pos: os.make_buffer([-0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5], 0), - verts: 4, - uv: os.make_buffer([0,1,1,1,0,0,1,0],2), - index: os.make_buffer([0,1,2,2,1,3],1), - count: 6 - }; - - render.init(); - - camera = make_camera(); - camera.transform.pos = [0,0,-100]; - camera.mode = "keep"; - camera.break = "fit"; - camera.size = game.size; - gamestate.camera = camera; - - hudcam = make_camera(); - hudcam.near = 0; - hudcam.size = camera.size; - hudcam.mode = "keep"; - hudcam.break = "fit"; - - appcam = make_camera(); - appcam.near = 0; - appcam.size = window.size; - appcam.transform.pos = [window.size.x,window.size.y,-100]; - screencolor = render.screencolor(); - }, - process, - window.size.x, - window.size.y, - ); -}; - -game.startengine = 0; -var frames = []; - -prosperon.release_mode = function() +function bare_use(file) { - prosperon.debug = false; - mum.debug = false; - debug.kill(); -} -prosperon.debug = true; - -// Returns an array in the form of [left, bottom, right, top] in pixels of the camera to render to -// Camera viewport is [left,bottom,right,top] in relative values -function camviewport() -{ - var aspect = (this.viewport[2]-this.viewport[0])/(this.viewport[3]-this.viewport[1])*window.size.x/window.size.y; - var raspect = this.size.x/this.size.y; - - var left = this.viewport[0]*window.size.x; - var bottom = this.viewport[1]*window.size.y; - - var usemode = this.mode; - - if (this.break && this.size.x > window.size.x && this.size.y > window.size.y) - usemode = this.break; - - if (usemode === "fit") - if (raspect < aspect) usemode = "height"; - else usemode = "width"; - - switch(usemode) { - case "stretch": - case "expand": - return [0, 0, window.size.x, window.size.y]; - case "keep": - return [left, bottom, left+this.size.x, bottom+this.size.y]; - case "height": - var ret = [left, 0, this.size.x*(window.size.y/this.size.y), window.size.y]; - ret[0] = (window.size.x-(ret[2]-ret[0]))/2; - return ret; - case "width": - var ret = [0, bottom, window.size.x, this.size.y*(window.size.x/this.size.x)]; - ret[1] = (window.size.y-(ret[3]-ret[1]))/2; - return ret; - } - - return [0, 0, window.size.x, window.size.y]; + var script = io.slurp(file); + script = `(function() { var self = this; ${script}; })`; + Object.assign(globalThis, os.eval(file, script).call(globalThis)); } -// pos is pixels on the screen, lower left[0,0] -function camscreen2world(pos) -{ - var view = this.screen2cam(pos); - view.x *= this.size.x; - view.y *= this.size.y; - view = view.sub([this.size.x/2, this.size.y/2]); - view = view.add(this.pos.xy); - return view; -} +profile.enabled = false; -camscreen2world.doc = "Convert a view position for a camera to world." +bare_use("scripts/base.js"); +bare_use("scripts/profile.js"); -function screen2cam(pos) -{ - var viewport = this.view(); - var width = viewport[2]-viewport[0]; - var height = viewport[3]-viewport[1]; - var left = pos.x-viewport[0]; - var bottom = pos.y-viewport[1]; - var p = [left/width, bottom/height]; - return p; -} +if (!profile.enabled) + use = stripped_use; -screen2cam.doc = "Convert a screen space position in pixels to a normalized viewport position in a camera." - -function make_camera() -{ - var cam = world.spawn(); - cam.near = 0.1; - cam.far = 1000; - cam.ortho = true; - cam.viewport = [0,0,1,1]; - cam.size = window.size.slice(); // The render size of this camera in pixels - // In ortho mode, this determines how many pixels it will see - cam.mode = "stretch"; - cam.screen2world = camscreen2world; - cam.screen2cam = screen2cam; - - cam.mousepos = function() { return this.screen2world(input.mouse.screenpos()); } - cam.view = camviewport; - cam.offscreen = false; - return cam; -} - -var camera; -var hudcam; -var appcam; -var screencolor; - -prosperon.render = function() -{ - profile.frame("world"); - render.set_camera(camera); - profile.frame("sprites"); - render.sprites(); - profile.endframe(); - profile.frame("draws"); - prosperon.draw(); - profile.endframe(); - hudcam.size = camera.size; - hudcam.transform.pos = [hudcam.size.x/2, hudcam.size.y/2, -100]; - render.set_camera(hudcam); - - profile.endframe(); - profile.frame("hud"); - - prosperon.hud(); - render.flush_text(); - - render.end_pass(); - - profile.endframe(); - - profile.frame("post process"); - /* draw the image of the game world first */ - render.glue_pass(); - render.viewport(...camera.view()); - render.use_shader(render.postshader); - render.use_mat({diffuse:screencolor}); - render.draw(shape.quad); - - profile.endframe(); - - profile.frame("app"); - - // Flush & render - appcam.transform.pos = [window.size.x/2, window.size.y/2, -100]; - appcam.size = window.size.slice(); - if (os.sys() !== 'macos') - appcam.size.y *= -1; - - render.set_camera(appcam); - render.viewport(...appcam.view()); - - // Call gui functions - mum.style = mum.dbg_style; - prosperon.gui(); - if (mum.drawinput) mum.drawinput(); - prosperon.gui_dbg(); - render.flush_text(); - mum.style = mum.base; - - profile.endframe(); - - profile.frame("imgui"); - - render.imgui_new(window.size.x, window.size.y, 0.01); - prosperon.imgui(); - render.imgui_end(); - - profile.endframe(); - - render.end_pass(); - render.commit(); -} - -function process() { - profile.frame("frame"); - var dt = profile.secs(profile.now()) - frame_t; - frame_t = profile.secs(profile.now()); - - profile.frame("app update"); - prosperon.appupdate(dt); - profile.endframe(); - - profile.frame("input"); - input.procdown(); - profile.endframe(); - - if (sim.mode === "play" || sim.mode === "step") { - profile.frame("update"); - prosperon.update(dt * game.timescale); - profile.endframe(); - if (sim.mode === "step") sim.pause(); - - profile.frame("physics"); - physlag += dt; - - while (physlag > physics.delta) { - physlag -= physics.delta; - prosperon.phys2d_step(physics.delta * game.timescale); - prosperon.physupdate(physics.delta * game.timescale); - } - profile.endframe(); - } - - profile.frame("render"); - prosperon.window_render(window.size); - prosperon.render(); - profile.endframe(); - - profile.endframe(); -} - -globalThis.fps = function () { - return 0; -// var sum = 0; -// for (var i = 0; i < frames.length; i++) sum += frames[i]; -// return frames.length / sum; -}; - -game.timescale = 1; - -var eachobj = function (obj, fn) { - var val = fn(obj); - if (val) return val; - for (var o in obj.objects) { - if (obj.objects[o] === obj) - console.error(`Object ${obj.toString()} is referenced by itself.`); - val = eachobj(obj.objects[o], fn); - if (val) return val; - } -}; - -game.all_objects = function (fn, startobj = world) { - return eachobj(startobj, fn); -}; -game.find_object = function (fn, startobj = world) {}; - -game.tags = {}; -game.tag_add = function (tag, obj) { - game.tags[tag] ??= {}; - game.tags[tag][obj.guid] = obj; -}; - -game.tag_rm = function (tag, obj) { - delete game.tags[tag][obj.guid]; -}; - -game.tag_clear_guid = function (guid) { - for (var tag in game.tags) delete game.tags[tag][guid]; -}; - -game.objects_with_tag = function (tag) { - if (!game.tags[tag]) return []; - return Object.values(game.tags[tag]); -}; - -game.doc = {}; -game.doc.object = "Returns the entity belonging to a given id."; -game.doc.pause = "Pause game simulation."; -game.doc.play = "Resume or start game simulation."; -game.doc.camera = "Current camera."; - -game.texture = function (path, force = false) { - if (force && game.texture.cache[path]) return game.texture.cache[path]; - - if (!io.exists(path)) { - console.warn(`Missing texture: ${path}`); - game.texture.cache[path] = game.texture("icons/no_tex.gif"); - } else game.texture.cache[path] ??= os.make_texture(path); - - return game.texture.cache[path]; -}; -game.texture.cache = {}; - -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.`; - -prosperon.iconified = function (icon) {}; -prosperon.focus = function (focus) {}; -prosperon.resize = function (dimensions) { - window.size.x = dimensions.x; - window.size.y = dimensions.y; -}; -prosperon.suspended = function (sus) {}; -prosperon.mouseenter = function () {}; -prosperon.mouseleave = function () {}; -prosperon.touchpress = function (touches) {}; -prosperon.touchrelease = function (touches) {}; -prosperon.touchmove = function (touches) {}; -prosperon.clipboardpaste = function (str) {}; -prosperon.quit = function () { - if (profile.disabled) return; - - say("===START CACHE REPORTS===\n"); - for (var i in profile.report_cache) { - say(profile.printreport(profile.report_cache[i],i)); - } - - say("===FRAME AVERAGES===\n"); - say(profile.print_frame_avg()); - say("\n"); - - profile.print_cpu_instr(); -}; - -window.size = [640, 480]; -window.mode = "keep"; -window.toggle_fullscreen = function() { window.fullscreen = !window.fullscreen; } - -window.set_icon.doc = "Set the icon of the window using the PNG image at path."; - -window.doc = {}; -window.doc.dimensions = "Window width and height packaged in an array [width,height]"; -window.doc.title = "Name in the title bar of the window."; -window.doc.boundingbox = "Boundingbox of the window, with top and right being its height and width."; - -global.mixin("scripts/input"); -global.mixin("scripts/std"); -global.mixin("scripts/diff"); -global.mixin("scripts/color"); -global.mixin("scripts/gui"); -global.mixin("scripts/tween"); -global.mixin("scripts/ai"); -global.mixin("scripts/particle"); - -var timer = { - update(dt) { - this.remain -= dt; - if (this.remain <= 0) { - this.fn(); - this.kill(); - } - }, - - kill() { - this.end(); - delete this.fn; - }, - - delay(fn, secs) { - var t = Object.create(this); - t.time = secs; - t.remain = secs; - t.fn = fn; - t.end = Register.update.register(timer.update.bind(t)); - var returnfn = timer.kill.bind(t); - returnfn.remain = secs; - return returnfn; - }, -}; - -global.mixin("scripts/physics"); -global.mixin("scripts/geometry"); - -/* -Factory for creating registries. Register one with 'X.register', -which returns a function that, when invoked, cancels the registry. -*/ -var Register = { - registries: [], - - add_cb(name, e_event = false) { - var n = {}; - var fns = []; - - n.register = function (fn, oname) { - if (!(fn instanceof Function)) return; - - var dofn = function(...args) { - var st = profile.now(); - fn(...args); - profile.addreport(name, oname, st); - } - - fns.push(dofn); - return function () { - fns.remove(dofn); - }; - }; - - prosperon[name] = function (...args) { - fns.forEach(x => x(...args)); - }; - - prosperon[name].fns = fns; - n.clear = function () { - fns = []; - }; - - Register[name] = n; - Register.registries[name] = n; - - return n; - }, -}; - -Register.add_cb("appupdate", true); -Register.add_cb("update", true).doc = "Called once per frame."; -Register.add_cb("physupdate", true); -Register.add_cb("gui", true); -Register.add_cb("hud", true); -Register.add_cb("draw_dbg", true); -Register.add_cb("gui_dbg", true); -Register.add_cb("hud_dbg", true); -Register.add_cb("draw", true); -Register.add_cb("imgui", true); - -var Event = { - events: {}, - - observe(name, obj, fn) { - this.events[name] ??= []; - this.events[name].push([obj, fn]); - }, - - unobserve(name, obj) { - this.events[name] = this.events[name].filter((x) => x[0] !== obj); - }, - - rm_obj(obj) { - Object.keys(this.events).forEach((name) => Event.unobserve(name, obj)); - }, - - notify(name, ...args) { - if (!this.events[name]) return; - this.events[name].forEach(function (x) { - x[1].call(x[0], ...args); - }); - }, -}; - -global.mixin("scripts/spline"); -global.mixin("scripts/components"); -global.mixin("scripts/actor"); -global.mixin("scripts/entity"); - -function world_start() { - globalThis.world = Object.create(entity); - world.transform = os.make_transform(); - world.objects = {}; - world.toString = function () { - return "world"; - }; - world.ur = "world"; - world.kill = function () { - this.clear(); - }; - world.phys = 2; - world.zoom = 1; - world._ed = { selectable: false }; - world.ur = {}; - world.ur.fresh = {}; - game.cam = world; -} - -global.mixin("scripts/physics"); -global.mixin("scripts/widget"); -global.mixin("scripts/mum"); - -window.title = `Prosperon v${prosperon.version}`; -window.size = [500, 500]; +Object.assign(globalThis, use("scripts/prosperon.js")); diff --git a/scripts/entity.js b/scripts/entity.js index 5e7e689..199b9eb 100644 --- a/scripts/entity.js +++ b/scripts/entity.js @@ -138,7 +138,7 @@ var entity = { }, spawn(text, config, callback) { - var st = profile.now(); + var ent = Object.create(entity); ent.transform = os.make_transform(); @@ -165,7 +165,8 @@ var entity = { use(text, ent); else if (Array.isArray(text)) for (var path of text) use(path,ent); - + profile.cache("ENTITY TIME", ent.ur.name); + var st = profile.now(); if (typeof config === 'string') Object.merge(ent, json.decode(Resources.replstrs(config))); else if (Array.isArray(config)) @@ -230,7 +231,7 @@ var entity = { for (var i in ent.objects) ent.ur.fresh.objects[i] = ent.objects[i].instance_obj(); - profile.addreport("ENTITY TIME", ent.ur.name, st); + profile.endcache(); return ent; }, diff --git a/scripts/profile.js b/scripts/profile.js index 67ff7bd..fa67687 100644 --- a/scripts/profile.js +++ b/scripts/profile.js @@ -1,3 +1,5 @@ +var t_units = ["ns", "us", "ms", "s", "m", "h"]; + profile.cpu = function(fn, times = 1, q = "unnamed") { var start = profile.now(); for (var i = 0; i < times; i++) @@ -33,6 +35,7 @@ function add_callgraph(fn, line, time) { var hittar = 500; var hitpct = 0.2; var start_gather = profile.now(); +if (profile.enabled) profile.gather(hittar, function() { var time = profile.now()-st; @@ -141,19 +144,43 @@ profile.print_frame_avg = function() print_frame(profile_frames[i], "", 'frame'); } -profile.report_cache = {}; +var report_cache = {}; -profile.addreport = function (group, line, start) { +var cachest = 0; +var cachegroup; +var cachetitle; +profile.cache = function(group, title) +{ + cachest = profile.now(); + cachegroup = group; + cachetitle = title; +} + +profile.endcache = function(tag = "") +{ + addreport(cachegroup, cachetitle + tag, cachest); +} + +profile.print_cache_report = function() +{ + var str = "===START CACHE REPORTS===\n"; + for (var i in report_cache) + str += printreport(report_cache[i], i) + "\n"; + + return str; +} + +function addreport(group, line, start) { if (typeof group !== 'string') group = 'UNGROUPED'; - profile.report_cache[group] ??= {}; - var cache = profile.report_cache[group]; + report_cache[group] ??= {}; + var cache = report_cache[group]; cache[line] ??= []; var t = profile.now(); cache[line].push(t - start); return t; }; -profile.printreport = function (cache, name) { +function printreport(cache, name) { var report = `==${name}==` + "\n"; var reports = []; @@ -176,12 +203,3 @@ profile.printreport = function (cache, name) { return report; }; - -var null_fn = function(){}; -profile.disable = function() -{ - profile.gather_stop(); - profile.frame = null_fn; - profile.endframe = null_fn; - profile.disabled = true; -} \ No newline at end of file diff --git a/scripts/prosperon.js b/scripts/prosperon.js new file mode 100644 index 0000000..a618844 --- /dev/null +++ b/scripts/prosperon.js @@ -0,0 +1,425 @@ +global.check_registers = function (obj) { + for (var reg in Register.registries) { + if (typeof obj[reg] === 'function') { + var fn = obj[reg].bind(obj); + var name = obj.ur ? obj.ur.name : obj.toString(); + obj.timers.push(Register.registries[reg].register(fn, name)); + } + } + + for (var k in obj) { + if (!k.startsWith("on_")) continue; + var signal = k.fromfirst("on_"); + Event.observe(signal, obj, obj[k]); + } +}; + +global.obscure("global"); +global.mixin("scripts/render"); +global.mixin("scripts/debug"); + +var frame_t = profile.secs(profile.now()); + +var sim = {}; +sim.mode = "play"; +sim.play = function () { + this.mode = "play"; + os.reindex_static(); +}; +sim.playing = function () { + return this.mode === "play"; +}; +sim.pause = function () { + this.mode = "pause"; +}; +sim.paused = function () { + return this.mode === "pause"; +}; +sim.step = function () { + this.mode = "step"; +}; +sim.stepping = function () { + return this.mode === "step"; +}; + +var physlag = 0; + +var gggstart = game.engine_start; +game.engine_start = function (s) { + game.startengine = 1; + gggstart( + function () { + global.mixin("scripts/sound.js"); + world_start(); + window.set_icon(os.make_texture("icons/moon.gif")); + Object.readonly(window.__proto__, "vsync"); + Object.readonly(window.__proto__, "enable_dragndrop"); + Object.readonly(window.__proto__, "enable_clipboard"); + Object.readonly(window.__proto__, "high_dpi"); + Object.readonly(window.__proto__, "sample_count"); + s(); + + shape.quad = { + pos: os.make_buffer([0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0], 0), + verts: 4, + uv: os.make_buffer([0, 1, 1, 1, 0, 0, 1, 0], 2), + index: os.make_buffer([0, 1, 2, 2, 1, 3], 1), + count: 6, + }; + + shape.triangle = { + pos: os.make_buffer([0, 0, 0, 0.5, 1, 0, 1, 0, 0], 0), + uv: os.make_buffer([0, 0, 0.5, 1, 1, 0], 2), + verts: 3, + count: 3, + index: os.make_buffer([0, 2, 1], 1), + }; + + shape.centered_quad = { + pos: os.make_buffer([-0.5, -0.5, -0.5, 0.5, -0.5, -0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5], 0), + verts: 4, + uv: os.make_buffer([0,1,1,1,0,0,1,0],2), + index: os.make_buffer([0,1,2,2,1,3],1), + count: 6 + }; + + render.init(); + + prosperon.camera = prosperon.make_camera(); + var camera = prosperon.camera; + camera.transform.pos = [0,0,-100]; + camera.mode = "keep"; + camera.break = "fit"; + camera.size = game.size; + gamestate.camera = camera; + + prosperon.hudcam = prosperon.make_camera(); + var hudcam = prosperon.hudcam; + hudcam.near = 0; + hudcam.size = camera.size; + hudcam.mode = "keep"; + hudcam.break = "fit"; + + prosperon.appcam = prosperon.make_camera(); + var appcam = prosperon.appcam; + appcam.near = 0; + appcam.size = window.size; + appcam.transform.pos = [window.size.x,window.size.y,-100]; + prosperon.screencolor = render.screencolor(); + }, + prosperon.process, + window.size.x, + window.size.y, + ); +}; + +game.startengine = 0; +var frames = []; + +prosperon.release_mode = function() +{ + prosperon.debug = false; + mum.debug = false; + debug.kill(); +} +prosperon.debug = true; + +globalThis.fps = function () { + return 0; +// var sum = 0; +// for (var i = 0; i < frames.length; i++) sum += frames[i]; +// return frames.length / sum; +}; + +game.timescale = 1; + +var eachobj = function (obj, fn) { + var val = fn(obj); + if (val) return val; + for (var o in obj.objects) { + if (obj.objects[o] === obj) + console.error(`Object ${obj.toString()} is referenced by itself.`); + val = eachobj(obj.objects[o], fn); + if (val) return val; + } +}; + +game.all_objects = function (fn, startobj = world) { + return eachobj(startobj, fn); +}; +game.find_object = function (fn, startobj = world) {}; + +game.tags = {}; +game.tag_add = function (tag, obj) { + game.tags[tag] ??= {}; + game.tags[tag][obj.guid] = obj; +}; + +game.tag_rm = function (tag, obj) { + delete game.tags[tag][obj.guid]; +}; + +game.tag_clear_guid = function (guid) { + for (var tag in game.tags) delete game.tags[tag][guid]; +}; + +game.objects_with_tag = function (tag) { + if (!game.tags[tag]) return []; + return Object.values(game.tags[tag]); +}; + +game.doc = {}; +game.doc.object = "Returns the entity belonging to a given id."; +game.doc.pause = "Pause game simulation."; +game.doc.play = "Resume or start game simulation."; +game.doc.camera = "Current camera."; + +game.texture = function (path, force = false) { + if (force && game.texture.cache[path]) return game.texture.cache[path]; + + if (!io.exists(path)) { + console.warn(`Missing texture: ${path}`); + game.texture.cache[path] = game.texture("icons/no_tex.gif"); + } else game.texture.cache[path] ??= os.make_texture(path); + + return game.texture.cache[path]; +}; +game.texture.cache = {}; + +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.`; + +prosperon.iconified = function (icon) {}; +prosperon.focus = function (focus) {}; +prosperon.resize = function (dimensions) { + window.size.x = dimensions.x; + window.size.y = dimensions.y; +}; +prosperon.suspended = function (sus) {}; +prosperon.mouseenter = function () {}; +prosperon.mouseleave = function () {}; +prosperon.touchpress = function (touches) {}; +prosperon.touchrelease = function (touches) {}; +prosperon.touchmove = function (touches) {}; +prosperon.clipboardpaste = function (str) {}; +prosperon.quit = function () { + if (!profile.enabled) return; + + say(profile.print_cache_report()); + + say("===FRAME AVERAGES===\n"); + say(profile.print_frame_avg()); + say("\n"); + + profile.print_cpu_instr(); +}; + +window.size = [640, 480]; +window.mode = "keep"; +window.toggle_fullscreen = function() { window.fullscreen = !window.fullscreen; } + +window.set_icon.doc = "Set the icon of the window using the PNG image at path."; + +window.doc = {}; +window.doc.dimensions = "Window width and height packaged in an array [width,height]"; +window.doc.title = "Name in the title bar of the window."; +window.doc.boundingbox = "Boundingbox of the window, with top and right being its height and width."; + +global.mixin("scripts/input"); +global.mixin("scripts/std"); +global.mixin("scripts/diff"); +global.mixin("scripts/color"); +global.mixin("scripts/gui"); +global.mixin("scripts/tween"); +global.mixin("scripts/ai"); +global.mixin("scripts/particle"); + +var timer = { + update(dt) { + this.remain -= dt; + if (this.remain <= 0) { + this.fn(); + this.kill(); + } + }, + + kill() { + this.end(); + delete this.fn; + }, + + delay(fn, secs) { + var t = Object.create(this); + t.time = secs; + t.remain = secs; + t.fn = fn; + t.end = Register.update.register(timer.update.bind(t)); + var returnfn = timer.kill.bind(t); + returnfn.remain = secs; + return returnfn; + }, +}; + +global.mixin("scripts/physics"); +global.mixin("scripts/geometry"); + +/* +Factory for creating registries. Register one with 'X.register', +which returns a function that, when invoked, cancels the registry. +*/ +var Register = { + registries: [], + + add_cb(name, e_event = false) { + var n = {}; + var fns = []; + + n.register = function (fn, oname) { + if (!(fn instanceof Function)) return; + + var dofn = function(...args) { + profile.cache(name,oname); + var st = profile.now(); + fn(...args); + profile.endcache(); + } + + fns.push(dofn); + return function () { + fns.remove(dofn); + }; + }; + + prosperon[name] = function (...args) { + fns.forEach(x => x(...args)); + }; + + prosperon[name].fns = fns; + n.clear = function () { + fns = []; + }; + + Register[name] = n; + Register.registries[name] = n; + + return n; + }, +}; + +Register.add_cb("appupdate", true); +Register.add_cb("update", true).doc = "Called once per frame."; +Register.add_cb("physupdate", true); +Register.add_cb("gui", true); +Register.add_cb("hud", true); +Register.add_cb("draw_dbg", true); +Register.add_cb("gui_dbg", true); +Register.add_cb("hud_dbg", true); +Register.add_cb("draw", true); +Register.add_cb("imgui", true); + +var Event = { + events: {}, + + observe(name, obj, fn) { + this.events[name] ??= []; + this.events[name].push([obj, fn]); + }, + + unobserve(name, obj) { + this.events[name] = this.events[name].filter((x) => x[0] !== obj); + }, + + rm_obj(obj) { + Object.keys(this.events).forEach((name) => Event.unobserve(name, obj)); + }, + + notify(name, ...args) { + if (!this.events[name]) return; + this.events[name].forEach(function (x) { + x[1].call(x[0], ...args); + }); + }, +}; + +global.mixin("scripts/spline"); +global.mixin("scripts/components"); +global.mixin("scripts/actor"); +global.mixin("scripts/entity"); + +function world_start() { + globalThis.world = Object.create(entity); + world.transform = os.make_transform(); + world.objects = {}; + world.toString = function () { + return "world"; + }; + world.ur = "world"; + world.kill = function () { + this.clear(); + }; + world.phys = 2; + world.zoom = 1; + world._ed = { selectable: false }; + world.ur = {}; + world.ur.fresh = {}; + game.cam = world; +} + +global.mixin("scripts/physics"); +global.mixin("scripts/widget"); +global.mixin("scripts/mum"); + +window.title = `Prosperon v${prosperon.version}`; +window.size = [500, 500]; + +return { + Register, + sim, + frame_t, + physlag, + Event +} diff --git a/scripts/render.js b/scripts/render.js index 2f700ac..7df1e94 100644 --- a/scripts/render.js +++ b/scripts/render.js @@ -182,7 +182,8 @@ render.make_shader = function(shader) shader = io.slurp(`shaders/${file}`); } var writejson = `.prosperon/${file.name()}.shader.json`; - var st = profile.now(); + + profile.cache("shader", file); breakme: if (io.exists(writejson)) { var data = json.decode(io.slurp(writejson)); @@ -193,8 +194,8 @@ render.make_shader = function(shader) break breakme; } } - - profile.addreport("shader [cached]", file, st); + + profile.endcache(" [cached]"); var shaderobj = json.decode(io.slurp(writejson)); var obj = shaderobj[os.sys()]; obj.pipe = render.pipeline(obj); @@ -310,7 +311,7 @@ render.make_shader = function(shader) compiled.files = files; io.slurpwrite(writejson, json.encode(compiled)); - profile.addreport('shader', file, st); + profile.endcache(); var obj = compiled[os.sys()]; obj.pipe = render.pipeline(obj); @@ -791,4 +792,199 @@ render.draw = function(mesh, ssbo, inst = 1) render.spdraw(cur.bind.count, inst); } + + + +// Returns an array in the form of [left, bottom, right, top] in pixels of the camera to render to +// Camera viewport is [left,bottom,right,top] in relative values +function camviewport() +{ + var aspect = (this.viewport[2]-this.viewport[0])/(this.viewport[3]-this.viewport[1])*window.size.x/window.size.y; + var raspect = this.size.x/this.size.y; + + var left = this.viewport[0]*window.size.x; + var bottom = this.viewport[1]*window.size.y; + + var usemode = this.mode; + + if (this.break && this.size.x > window.size.x && this.size.y > window.size.y) + usemode = this.break; + + if (usemode === "fit") + if (raspect < aspect) usemode = "height"; + else usemode = "width"; + + switch(usemode) { + case "stretch": + case "expand": + return [0, 0, window.size.x, window.size.y]; + case "keep": + return [left, bottom, left+this.size.x, bottom+this.size.y]; + case "height": + var ret = [left, 0, this.size.x*(window.size.y/this.size.y), window.size.y]; + ret[0] = (window.size.x-(ret[2]-ret[0]))/2; + return ret; + case "width": + var ret = [0, bottom, window.size.x, this.size.y*(window.size.x/this.size.x)]; + ret[1] = (window.size.y-(ret[3]-ret[1]))/2; + return ret; + } + + return [0, 0, window.size.x, window.size.y]; +} + +// pos is pixels on the screen, lower left[0,0] +function camscreen2world(pos) +{ + var view = this.screen2cam(pos); + view.x *= this.size.x; + view.y *= this.size.y; + view = view.sub([this.size.x/2, this.size.y/2]); + view = view.add(this.pos.xy); + return view; +} + +camscreen2world.doc = "Convert a view position for a camera to world." + +function screen2cam(pos) +{ + var viewport = this.view(); + var width = viewport[2]-viewport[0]; + var height = viewport[3]-viewport[1]; + var left = pos.x-viewport[0]; + var bottom = pos.y-viewport[1]; + var p = [left/width, bottom/height]; + return p; +} + +screen2cam.doc = "Convert a screen space position in pixels to a normalized viewport position in a camera." + +prosperon.make_camera = function() +{ + var cam = world.spawn(); + cam.near = 0.1; + cam.far = 1000; + cam.ortho = true; + cam.viewport = [0,0,1,1]; + cam.size = window.size.slice(); // The render size of this camera in pixels + // In ortho mode, this determines how many pixels it will see + cam.mode = "stretch"; + cam.screen2world = camscreen2world; + cam.screen2cam = screen2cam; + + cam.mousepos = function() { return this.screen2world(input.mouse.screenpos()); } + cam.view = camviewport; + cam.offscreen = false; + return cam; +} + +var screencolor; + +prosperon.render = function() +{ + profile.frame("world"); + render.set_camera(prosperon.camera); + profile.frame("sprites"); + render.sprites(); + profile.endframe(); + profile.frame("draws"); + prosperon.draw(); + profile.endframe(); + prosperon.hudcam.size = prosperon.camera.size; + prosperon.hudcam.transform.pos = [prosperon.hudcam.size.x/2, prosperon.hudcam.size.y/2, -100]; + render.set_camera(prosperon.hudcam); + + profile.endframe(); + profile.frame("hud"); + + prosperon.hud(); + render.flush_text(); + + render.end_pass(); + + profile.endframe(); + + profile.frame("post process"); + /* draw the image of the game world first */ + render.glue_pass(); + render.viewport(...prosperon.camera.view()); + render.use_shader(render.postshader); + render.use_mat({diffuse:prosperon.screencolor}); + render.draw(shape.quad); + + profile.endframe(); + + profile.frame("app"); + + // Flush & render + prosperon.appcam.transform.pos = [window.size.x/2, window.size.y/2, -100]; + prosperon.appcam.size = window.size.slice(); + if (os.sys() !== 'macos') + prosperon.appcam.size.y *= -1; + + render.set_camera(prosperon.appcam); + render.viewport(...prosperon.appcam.view()); + + // Call gui functions + mum.style = mum.dbg_style; + prosperon.gui(); + if (mum.drawinput) mum.drawinput(); + prosperon.gui_dbg(); + render.flush_text(); + mum.style = mum.base; + + profile.endframe(); + + profile.frame("imgui"); + + render.imgui_new(window.size.x, window.size.y, 0.01); + prosperon.imgui(); + render.imgui_end(); + + profile.endframe(); + + render.end_pass(); + render.commit(); +} + +prosperon.process = function() { + profile.frame("frame"); + var dt = profile.secs(profile.now()) - frame_t; + frame_t = profile.secs(profile.now()); + + profile.frame("app update"); + prosperon.appupdate(dt); + profile.endframe(); + + profile.frame("input"); + input.procdown(); + profile.endframe(); + + if (sim.mode === "play" || sim.mode === "step") { + profile.frame("update"); + prosperon.update(dt * game.timescale); + profile.endframe(); + if (sim.mode === "step") sim.pause(); + + profile.frame("physics"); + physlag += dt; + + while (physlag > physics.delta) { + physlag -= physics.delta; + prosperon.phys2d_step(physics.delta * game.timescale); + prosperon.physupdate(physics.delta * game.timescale); + } + profile.endframe(); + } + + profile.frame("render"); + prosperon.window_render(window.size); + prosperon.render(); + profile.endframe(); + + profile.endframe(); +} + + + return {render};