Fix render bug

This commit is contained in:
John Alanbrook 2024-04-10 16:21:46 -05:00
parent 941106ced5
commit 33ac36ae5c
20 changed files with 224 additions and 144 deletions

View file

@ -344,6 +344,20 @@ Object.rkeys = function(o)
return keys; return keys;
} }
Object.readonly = function(o, name, msg)
{
var tmp = {};
var prop = Object.getOwnPropertyDescriptor(o, name);
if (!prop) {
console.error(`Attempted to make property ${name} readonly, but it doesn't exist on ${o}.`);
return;
}
Object.defineProperty(tmp, name, prop);
prop.get = function() { return tmp[name]; }
prop.set = function() { console.warn(`Attempted to set readonly property ${name}`); }
Object.defineProperty(o, name, prop);
}
Object.extend = function(from) Object.extend = function(from)
{ {
var n = {}; var n = {};

View file

@ -232,23 +232,25 @@ Object.seal(sprite);
loop: true if it should be looped loop: true if it should be looped
*/ */
var animcache = {}; var animcache = {};
var SpriteAnim = { var SpriteAnim = {};
make(path) { SpriteAnim.make = function(path) {
if (animcache[path]) return animcache[path]; if (animcache[path]) return animcache[path];
var anim; var anim;
if (io.exists(path.set_ext(".json"))) if (io.exists(path.set_ext(".ase")))
anim = SpriteAnim.aseprite(path.set_ext(".ase"));
else if (io.exists(path.set_ext(".json")))
anim = SpriteAnim.aseprite(path.set_ext(".json")); anim = SpriteAnim.aseprite(path.set_ext(".json"));
else if (path.ext() === 'gif')
anim = SpriteAnim.gif(path);
else if (path.ext() === 'ase') else if (path.ext() === 'ase')
anim = SpriteAnim.aseprite(path); anim = SpriteAnim.aseprite(path);
else if (path.ext() === 'gif')
anim = SpriteAnim.gif(path);
else else
return undefined; return undefined;
animcache[path] = anim; animcache[path] = anim;
return animcache[path]; return animcache[path];
}, };
gif(path) { SpriteAnim.gif = function(path) {
console.info(`making an anim from ${path}`); console.info(`making an anim from ${path}`);
var anim = {}; var anim = {};
anim.frames = []; anim.frames = [];
@ -278,9 +280,9 @@ var SpriteAnim = {
dim.y /= frames; dim.y /= frames;
anim.dim = dim; anim.dim = dim;
return {0:anim}; return {0:anim};
}, };
strip(path, frames, time=0.05) { SpriteAnim.strip = function(path, frames, time=0.05) {
var anim = {}; var anim = {};
anim.frames = []; anim.frames = [];
anim.path = path; anim.path = path;
@ -294,9 +296,9 @@ var SpriteAnim = {
anim.dim = Resources.texture.dimensions(path); anim.dim = Resources.texture.dimensions(path);
anim.dim.x /= frames; anim.dim.x /= frames;
return anim; return anim;
}, };
aseprite(path) { SpriteAnim.aseprite = function(path) {
function aseframeset2anim(frameset, meta) { function aseframeset2anim(frameset, meta) {
var anim = {}; var anim = {};
anim.frames = []; anim.frames = [];
@ -334,20 +336,18 @@ var SpriteAnim = {
} }
return anims; return anims;
}, };
validate(anim) SpriteAnim.validate = function(anim) {
{
if (!Object.isObject(anim)) return false; if (!Object.isObject(anim)) return false;
if (typeof anim.path !== 'string') return false; if (typeof anim.path !== 'string') return false;
if (typeof anim.dim !== 'object') return false; if (typeof anim.dim !== 'object') return false;
return true; return true;
}, };
find(path) { SpriteAnim.find = function(path) {
if (!io.exists(path + ".asset")) return; if (!io.exists(path + ".asset")) return;
var asset = JSON.parse(io.slurp(path + ".asset")); var asset = JSON.parse(io.slurp(path + ".asset"));
},
}; };
SpriteAnim.doc = 'Functions to create Primum animations from varying sources.'; SpriteAnim.doc = 'Functions to create Primum animations from varying sources.';

View file

@ -8,7 +8,6 @@ game.loadurs();
console.info(`window size: ${window.size}, render size: ${window.rendersize}`); console.info(`window size: ${window.size}, render size: ${window.rendersize}`);
player[0].control(debug); player[0].control(debug);
Register.gui.register(debug.draw, debug);
var show_frame = true; var show_frame = true;
@ -423,7 +422,7 @@ var editor = {
var depth = 0; var depth = 0;
var alldirty = false; var alldirty = false;
for (var lvl of lvlchain) { for (var lvl of lvlchain) {
if (!lvl._ed) continue; if (!lvl._ed?.selectable) continue;
if (alldirty) if (alldirty)
lvl._ed.dirty = true; lvl._ed.dirty = true;
else { else {
@ -461,7 +460,7 @@ var editor = {
var mg = physics.pos_query(input.mouse.worldpos(),10); var mg = physics.pos_query(input.mouse.worldpos(),10);
if (mg) { if (mg && mg._ed?.selectable) {
var p = mg.path_from(thiso); var p = mg.path_from(thiso);
render.text(p, input.mouse.screenpos(),1,Color.teal); render.text(p, input.mouse.screenpos(),1,Color.teal);
} }

View file

@ -250,9 +250,15 @@ var gggstart = game.engine_start;
game.engine_start = function(s) { game.engine_start = function(s) {
game.startengine = 1; game.startengine = 1;
gggstart(function() { gggstart(function() {
global.mixin("scripts/sound.js");
world_start(); world_start();
go_init(); go_init();
window.set_icon(os.make_texture("icons/moon.gif")) 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(); s();
}, process); }, process);
} }
@ -307,9 +313,14 @@ game.timescale = 1;
var eachobj = function(obj,fn) var eachobj = function(obj,fn)
{ {
fn(obj); fn(obj);
for (var o in obj.objects) for (var o in obj.objects) {
if (obj.objects[o] === obj) {
//console.error(`Object ${obj.toString()} is referenced by itself.`);
continue;
}
eachobj(obj.objects[o],fn); eachobj(obj.objects[o],fn);
} }
}
game.all_objects = function(fn, startobj = world) { eachobj(startobj,fn); }; game.all_objects = function(fn, startobj = world) { eachobj(startobj,fn); };
@ -552,6 +563,7 @@ function world_start() {
world.kill = function() { this.clear(); }; world.kill = function() { this.clear(); };
world.phys = 2; world.phys = 2;
world.zoom = 1; world.zoom = 1;
world._ed = { selectable: false };
game.cam = world; game.cam = world;
} }

View file

@ -279,6 +279,7 @@ var gameobject = {
var o = ent.objects; var o = ent.objects;
delete ent.objects; delete ent.objects;
for (var i in o) { for (var i in o) {
console.info(`creating ${i} on ${ent.toString()}`);
var newur = o[i].ur; var newur = o[i].ur;
delete o[i].ur; delete o[i].ur;
var n = ent.spawn(ur[newur], o[i]); var n = ent.spawn(ur[newur], o[i]);

View file

@ -1,20 +1,20 @@
input.keycodes = { input.keycodes = {
259: "backspace",
258: "tab",
257: "enter",
256: "escape",
32: "space", 32: "space",
45: "minus",
256: "escape",
257: "enter",
258: "tab",
259: "backspace",
260: "insert",
261: "delete",
262: "right",
263: "left",
264: "down",
265: "up",
266: "pgup", 266: "pgup",
267: "pgdown", 267: "pgdown",
268: "home", 268: "home",
269: "end", 269: "end",
263: "left",
265: "up",
262: "right",
265: "down",
260: "insert",
261: "delete",
45: "minus",
}; };
input.codekeys = {}; input.codekeys = {};
@ -117,7 +117,7 @@ prosperon.textinput = function(c){
}; };
prosperon.mousemove = function(pos, dx){ prosperon.mousemove = function(pos, dx){
mousepos = pos; mousepos = pos;
player[0].mouse_input(modstr() + "move", pos, dx); player[0].mouse_input("move", pos, dx);
}; };
prosperon.mousescroll = function(dx){ prosperon.mousescroll = function(dx){
player[0].mouse_input(modstr() + "scroll", dx); player[0].mouse_input(modstr() + "scroll", dx);
@ -126,7 +126,7 @@ prosperon.mousedown = function(b){
player[0].raw_input(modstr() + input.mouse.button[b], "pressed"); player[0].raw_input(modstr() + input.mouse.button[b], "pressed");
}; };
prosperon.mouseup = function(b){ prosperon.mouseup = function(b){
player[0].raw_input(modstr() + input.mouse.button[b], "released"); player[0].raw_input(input.mouse.button[b], "released");
}; };
input.mouse = {}; input.mouse = {};

View file

@ -1,7 +1,11 @@
var audio = {}; /* This file runs after the audio system is initiated */
var cries = {}; var cries = {};
audio.samplerate = dspsound.samplerate(); Object.readonly(audio, 'samplerate');
Object.readonly(audio, 'channels');
Object.readonly(audio, 'buffer_frames');
audio.play = function(file,bus = audio.bus.master) { audio.play = function(file,bus = audio.bus.master) {
file = Resources.find_sound(file); file = Resources.find_sound(file);
if (!file) { if (!file) {

View file

@ -250,9 +250,9 @@ Cmdline.register_order("play", function(argv) {
console.info(`Starting game with window size ${window.size} and render ${window.rendersize}.`); console.info(`Starting game with window size ${window.size} and render ${window.rendersize}.`);
game.engine_start(function() { game.engine_start(function() {
global.mixin("scripts/sound.js");
global.app = actor.spawn("game.js"); global.app = actor.spawn("game.js");
if (project.icon) window.set_icon(game.texture(project.icon)); if (project.icon) window.set_icon(game.texture(project.icon));
render.set_font("fonts/c64.ttf", 8);
game.camera = world.spawn("scripts/camera2d"); game.camera = world.spawn("scripts/camera2d");
}); });
}, "Play the game present in this folder."); }, "Play the game present in this folder.");

View file

@ -20,7 +20,7 @@
#include "font.h" #include "font.h"
#define v_amt 5000 #define v_amt 500000
struct flush { struct flush {
sg_shader shader; sg_shader shader;
@ -372,6 +372,7 @@ void draw_line(HMM_Vec2 *points, int n, struct rgba color, float seg_len, float
.size = sizeof(uint16_t)*i_c .size = sizeof(uint16_t)*i_c
}; };
if (sg_query_buffer_will_overflow(line_bind.vertex_buffers[0], vr.size) || sg_query_buffer_will_overflow(line_bind.index_buffer, ir.size)) return;
sg_append_buffer(line_bind.vertex_buffers[0], &vr); sg_append_buffer(line_bind.vertex_buffers[0], &vr);
sg_append_buffer(line_bind.index_buffer, &ir); sg_append_buffer(line_bind.index_buffer, &ir);

View file

@ -20,7 +20,6 @@ void draw_grid(float width, float span, struct rgba color);
void debug_flush(HMM_Mat4 *view); void debug_flush(HMM_Mat4 *view);
void debug_newframe(); void debug_newframe();
void debug_nextpass();
HMM_Vec2 *inflatepoints(HMM_Vec2 *p, float d, int n); HMM_Vec2 *inflatepoints(HMM_Vec2 *p, float d, int n);

View file

@ -89,9 +89,15 @@ void mYughLog(int category, int priority, int line, const char *file, const char
printf("\n"); printf("\n");
} }
//if (priority >= LOG_ERROR) if (priority >= LOG_PANIC) {
//js_stacktrace(); js_stacktrace();
//raise(SIGINT); #ifdef __WIN32
DebugBreak();
#else
raise(SIGTRAP);
#endif
}
#endif #endif
} }

View file

@ -625,7 +625,7 @@ JSC_CCALL(render_line,
JSC_CCALL(render_sprites, sprite_draw_all()) JSC_CCALL(render_sprites, sprite_draw_all())
JSC_CCALL(render_models, model_draw_all()) JSC_CCALL(render_models, model_draw_all())
JSC_CCALL(render_emitters, emitters_draw(&useproj)) JSC_CCALL(render_emitters, emitters_draw(&useproj))
JSC_CCALL(render_flush, debug_flush(&useproj); text_flush(&useproj)) JSC_CCALL(render_flush, debug_flush(&useproj); text_flush(&useproj); )
JSC_CCALL(render_end_pass, JSC_CCALL(render_end_pass,
sg_end_pass(); sg_end_pass();
sg_commit(); sg_commit();
@ -823,6 +823,16 @@ static const JSCFunctionListEntry js_console_funcs[] = {
CGETSET_ADD(global, stdout_lvl) CGETSET_ADD(global, stdout_lvl)
}; };
JSC_GETSET_GLOBAL(CHANNELS, number)
JSC_GETSET_GLOBAL(BUF_FRAMES, number)
JSC_GETSET_GLOBAL(SAMPLERATE, number)
static const JSCFunctionListEntry js_audio_funcs[] = {
CGETSET_ADD_NAME(global, CHANNELS, channels),
CGETSET_ADD_NAME(global, BUF_FRAMES, buffer_frames),
CGETSET_ADD_NAME(global, SAMPLERATE, samplerate),
};
JSC_CCALL(profile_now, return number2js(stm_now())) JSC_CCALL(profile_now, return number2js(stm_now()))
static const JSCFunctionListEntry js_profile_funcs[] = { static const JSCFunctionListEntry js_profile_funcs[] = {
@ -1110,6 +1120,11 @@ static JSValue js_window_set_title(JSContext *js, JSValue this, JSValue v)
} }
JSC_CCALL(window_get_title, return str2js(js2window(this)->title)) JSC_CCALL(window_get_title, return str2js(js2window(this)->title))
JSC_CCALL(window_set_icon, window_seticon(&mainwin, js2texture(argv[0]))) JSC_CCALL(window_set_icon, window_seticon(&mainwin, js2texture(argv[0])))
JSC_GETSET(window, vsync, boolean)
JSC_GETSET(window, enable_clipboard, boolean)
JSC_GETSET(window, enable_dragndrop, boolean)
JSC_GETSET(window, high_dpi, boolean)
JSC_GETSET(window, sample_count, number)
static const JSCFunctionListEntry js_window_funcs[] = { static const JSCFunctionListEntry js_window_funcs[] = {
CGETSET_ADD(window, size), CGETSET_ADD(window, size),
@ -1117,6 +1132,11 @@ static const JSCFunctionListEntry js_window_funcs[] = {
CGETSET_ADD(window, mode), CGETSET_ADD(window, mode),
CGETSET_ADD(window, fullscreen), CGETSET_ADD(window, fullscreen),
CGETSET_ADD(window, title), CGETSET_ADD(window, title),
CGETSET_ADD(window, vsync),
CGETSET_ADD(window, enable_clipboard),
CGETSET_ADD(window, enable_dragndrop),
CGETSET_ADD(window, high_dpi),
CGETSET_ADD(window, sample_count),
MIST_FUNC_DEF(window, set_icon, 1) MIST_FUNC_DEF(window, set_icon, 1)
}; };
@ -1229,7 +1249,6 @@ JSC_SCALL(dspsound_source,
JSC_CCALL(dspsound_mix, return dsp_node2js(make_node(NULL,NULL,NULL))) JSC_CCALL(dspsound_mix, return dsp_node2js(make_node(NULL,NULL,NULL)))
JSC_CCALL(dspsound_master, return dsp_node2js(masterbus)) JSC_CCALL(dspsound_master, return dsp_node2js(masterbus))
JSC_CCALL(dspsound_plugin_node, plugin_node(js2dsp_node(argv[0]), js2dsp_node(argv[1]));) JSC_CCALL(dspsound_plugin_node, plugin_node(js2dsp_node(argv[0]), js2dsp_node(argv[1]));)
JSC_CCALL(dspsound_samplerate, return number2js(SAMPLERATE))
JSC_SCALL(dspsound_mod, ret = dsp_node2js(dsp_mod(str))) JSC_SCALL(dspsound_mod, ret = dsp_node2js(dsp_mod(str)))
JSC_SSCALL(dspsound_midi, ret = dsp_node2js(dsp_midi(str, make_soundfont(str2)))) JSC_SSCALL(dspsound_midi, ret = dsp_node2js(dsp_midi(str, make_soundfont(str2))))
@ -1250,7 +1269,6 @@ static const JSCFunctionListEntry js_dspsound_funcs[] = {
MIST_FUNC_DEF(dspsound, mix, 0), MIST_FUNC_DEF(dspsound, mix, 0),
MIST_FUNC_DEF(dspsound, master, 0), MIST_FUNC_DEF(dspsound, master, 0),
MIST_FUNC_DEF(dspsound, plugin_node, 2), MIST_FUNC_DEF(dspsound, plugin_node, 2),
MIST_FUNC_DEF(dspsound, samplerate, 0),
MIST_FUNC_DEF(dspsound, midi, 2), MIST_FUNC_DEF(dspsound, midi, 2),
MIST_FUNC_DEF(dspsound, mod, 1) MIST_FUNC_DEF(dspsound, mod, 1)
}; };
@ -1548,6 +1566,7 @@ void ffi_load() {
QJSGLOBALCLASS(prosperon); QJSGLOBALCLASS(prosperon);
QJSGLOBALCLASS(time); QJSGLOBALCLASS(time);
QJSGLOBALCLASS(console); QJSGLOBALCLASS(console);
QJSGLOBALCLASS(audio);
QJSGLOBALCLASS(profile); QJSGLOBALCLASS(profile);
QJSGLOBALCLASS(game); QJSGLOBALCLASS(game);
QJSGLOBALCLASS(gui); QJSGLOBALCLASS(gui);

View file

@ -30,6 +30,7 @@
#define MIST_CGETET_HID(name, fgetter, fsetter) MIST_CGETSET_BASE(name, fgetter, fsetter, JS_PROP_CONFIGURABLE) #define MIST_CGETET_HID(name, fgetter, fsetter) MIST_CGETSET_BASE(name, fgetter, fsetter, JS_PROP_CONFIGURABLE)
#define MIST_GET(name, fgetter) { #fgetter , JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE, JS_DEF_CGETSET, 0, .u = { .getset = { .get = { .getter = js_##name##_get_##fgetter } } } } #define MIST_GET(name, fgetter) { #fgetter , JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE, JS_DEF_CGETSET, 0, .u = { .getset = { .get = { .getter = js_##name##_get_##fgetter } } } }
#define CGETSET_ADD_NAME(ID, ENTRY, NAME) MIST_CGETSET_DEF(#NAME, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY)
#define CGETSET_ADD(ID, ENTRY) MIST_CGETSET_DEF(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY) #define CGETSET_ADD(ID, ENTRY) MIST_CGETSET_DEF(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY)
#define CGETSET_ADD_HID(ID, ENTRY) MIST_CGETSET_BASE(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY, JS_PROP_CONFIGURABLE) #define CGETSET_ADD_HID(ID, ENTRY) MIST_CGETSET_BASE(#ENTRY, js_##ID##_get_##ENTRY, js_##ID##_set_##ENTRY, JS_PROP_CONFIGURABLE)

View file

@ -295,6 +295,8 @@ void render_init() {
.size = sizeof(crt_quad), .size = sizeof(crt_quad),
.type = SG_BUFFERTYPE_VERTEXBUFFER, .type = SG_BUFFERTYPE_VERTEXBUFFER,
.usage = SG_USAGE_IMMUTABLE, .usage = SG_USAGE_IMMUTABLE,
.data = SG_RANGE(crt_quad),
.label = "crt vert buffer", .label = "crt vert buffer",
}); });
} }

View file

@ -13,6 +13,10 @@
#define PI 3.14159265 #define PI 3.14159265
int SAMPLERATE = 44100;
int BUF_FRAMES = 2048;
int CHANNELS = 2;
dsp_node *masterbus = NULL; dsp_node *masterbus = NULL;
void iir_free(struct dsp_iir *iir) void iir_free(struct dsp_iir *iir)
@ -143,10 +147,10 @@ dsp_node *make_node(void *data, void (*proc)(void *data, soundbyte *out, int sam
dsp_node *self = malloc(sizeof(dsp_node)); dsp_node *self = malloc(sizeof(dsp_node));
memset(self, 0, sizeof(*self)); memset(self, 0, sizeof(*self));
self->data = data; self->data = data;
self->cache = calloc(BUF_FRAMES*CHANNELS*sizeof(soundbyte),1);
self->proc = proc; self->proc = proc;
self->pass = 0; self->pass = 0;
self->gain = 1; self->gain = 1;
self->id = node_count++;
return self; return self;
} }

View file

@ -1,9 +1,9 @@
#ifndef DSP_H #ifndef DSP_H
#define DSP_H #define DSP_H
#define SAMPLERATE 44100 extern int SAMPLERATE;
#define BUF_FRAMES 2048 /* At 48k, 128 needed for 240fps consistency */ extern int BUF_FRAMES;
#define CHANNELS 2 extern int CHANNELS;
#include "sound.h" #include "sound.h"
#include "cbuf.h" #include "cbuf.h"
@ -15,14 +15,13 @@ typedef struct dsp_node {
void (*proc)(void *dsp, soundbyte *buf, int samples); /* processor */ void (*proc)(void *dsp, soundbyte *buf, int samples); /* processor */
void *data; /* Node specific data to use in the proc function, passed in as dsp */ void *data; /* Node specific data to use in the proc function, passed in as dsp */
void (*data_free)(void *data); void (*data_free)(void *data);
soundbyte cache[BUF_FRAMES*CHANNELS]; /* Cached process */ soundbyte *cache;
struct dsp_node **ins; /* Array of in nodes */ struct dsp_node **ins; /* Array of in nodes */
struct dsp_node *out; /* node this one is connected to */ struct dsp_node *out; /* node this one is connected to */
int pass; /* True if the filter should be bypassed */ int pass; /* True if the filter should be bypassed */
int off; /* True if the filter shouldn't output */ int off; /* True if the filter shouldn't output */
float gain; /* Between 0 and 1, to attenuate this output */ float gain; /* Between 0 and 1, to attenuate this output */
float pan; /* Between -100 and +100, panning left to right in the speakers */ float pan; /* Between -100 and +100, panning left to right in the speakers */
int id;
} dsp_node; } dsp_node;
void dsp_init(); void dsp_init();

View file

@ -137,6 +137,10 @@ void sound_init() {
.buffer_frames = BUF_FRAMES, .buffer_frames = BUF_FRAMES,
.logger.func = sg_logging, .logger.func = sg_logging,
}); });
SAMPLERATE = saudio_sample_rate();
CHANNELS = saudio_channels();
BUF_FRAMES = saudio_buffer_frames();
} }
typedef struct { typedef struct {

View file

@ -15,7 +15,12 @@
#include "sokol/sokol_app.h" #include "sokol/sokol_app.h"
#include "stb_image_resize2.h" #include "stb_image_resize2.h"
struct window mainwin = {0}; struct window mainwin = {
.sample_count = 1,
.vsync = 1,
.enable_clipboard = 0,
.enable_dragndrop = 0,
};
static struct window *windows = NULL; static struct window *windows = NULL;

View file

@ -23,6 +23,11 @@ struct window {
float aspect; float aspect;
float raspect; float raspect;
char *title; char *title;
int vsync;
int enable_clipboard;
int enable_dragndrop;
int high_dpi;
int sample_count;
int start; int start;
}; };
typedef struct window window; typedef struct window window;

View file

@ -246,6 +246,11 @@ void engine_start(JSValue start, JSValue procfn)
start_desc.height = mainwin.size.y; start_desc.height = mainwin.size.y;
start_desc.window_title = mainwin.title; start_desc.window_title = mainwin.title;
start_desc.fullscreen = mainwin.fullscreen; start_desc.fullscreen = mainwin.fullscreen;
start_desc.swap_interval = mainwin.vsync;
start_desc.enable_dragndrop = mainwin.enable_dragndrop;
start_desc.enable_clipboard = mainwin.enable_clipboard;
start_desc.high_dpi = mainwin.high_dpi;
start_desc.sample_count = mainwin.sample_count;
} }
double apptime() { return stm_sec(stm_now()); } double apptime() { return stm_sec(stm_now()); }