├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── build ├── alphafix.js ├── app-bundle.js ├── autosound.js ├── checks.js ├── compress.js ├── config.js ├── config.project.js ├── eslint.js ├── exec.js ├── gulpish-bundle.js ├── gulpish-tasks.js ├── gulpish.js ├── imgproc.js ├── index.js ├── json5.js ├── png.js ├── resize.js ├── spritesheet.js ├── test-runner.js ├── texpack.js ├── texproc.js ├── typescript.js ├── uglify.js ├── uglifyrc.js ├── warn-match.js ├── webfs_build.js └── yamlproc.js ├── jsjam22.sublime-project ├── package-lock.json ├── package.json ├── screenshots ├── ss1.png └── ss2.png └── src ├── client ├── account_ui.js ├── app.js ├── app_deps.js ├── crazy_wrapper.html ├── favicon.ico ├── img │ ├── crazy_banner.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.png │ ├── font │ │ ├── 04b03_8x1.json │ │ ├── 04b03_8x1.png │ │ ├── vga_8x16x1.json │ │ └── vga_8x16x1.png │ ├── itch_banner.png │ ├── particles │ │ └── circle8.png │ ├── tiles.png │ ├── tiles_ui.png │ ├── title.psd │ ├── title_text.png │ └── ui │ │ ├── button.png │ │ ├── button_disabled.png │ │ ├── button_down.png │ │ ├── panel.png │ │ └── pixely │ │ ├── menu_down.png │ │ ├── menu_entry.png │ │ ├── menu_header.png │ │ ├── menu_selected.png │ │ ├── progress_bar.png │ │ ├── progress_bar_trough.png │ │ ├── scrollbar_bottom.png │ │ ├── scrollbar_handle.png │ │ ├── scrollbar_handle_grabber.png │ │ ├── scrollbar_top.png │ │ ├── scrollbar_trough.png │ │ ├── slider.png │ │ └── slider_handle.png ├── index.html ├── main.css ├── main.js ├── main2.ts ├── particle_data.js ├── shaders │ └── test.fp ├── sounds │ ├── bg.ceol │ ├── bg.mp3 │ ├── button_click.mp3 │ ├── button_click.wav │ ├── down1.mp3 │ ├── down1.wav │ ├── down2.mp3 │ ├── down2.wav │ ├── down3.mp3 │ ├── down3.wav │ ├── fanfare.mp3 │ ├── fanfare.tg │ ├── fanfare.wav │ ├── msg_err.mp3 │ ├── msg_err.wav │ ├── msg_in.mp3 │ ├── msg_in.wav │ ├── msg_out.mp3 │ ├── msg_out.wav │ ├── msg_out_err.mp3 │ ├── msg_out_err.wav │ ├── rollover.mp3 │ ├── rollover.wav │ ├── up1.mp3 │ ├── up1.wav │ ├── up2.mp3 │ ├── up2.wav │ ├── up3.mp3 │ ├── up3.wav │ ├── upchord1.mp3 │ ├── upchord1.wav │ ├── upchord2.mp3 │ ├── upchord2.wav │ ├── upchord3.mp3 │ ├── upchord3.wav │ ├── user_join.mp3 │ ├── user_join.wav │ ├── user_leave.mp3 │ └── user_leave.wav ├── transitioner.js ├── worker.js └── worker_deps.js ├── glov ├── client │ ├── aabbtree.js │ ├── abtest.ts │ ├── animation.ts │ ├── auto_reset.ts │ ├── bootstrap.js │ ├── browser.js │ ├── build_ui.js │ ├── camera2d.js │ ├── chat_ui.js │ ├── client_config.ts │ ├── cmds.js │ ├── collapsagories.ts │ ├── color_picker.js │ ├── draw_list.js │ ├── dyn_geom.js │ ├── edit_box.d.ts │ ├── edit_box.js │ ├── effects.js │ ├── engine.js │ ├── entity_base_client.ts │ ├── entity_manager_client.ts │ ├── entity_manager_offline.ts │ ├── entity_position_manager.ts │ ├── environments.ts │ ├── error_report.js │ ├── external_user_info.ts │ ├── external_users_client.ts │ ├── fetch.js │ ├── filewatch.js │ ├── font.d.ts │ ├── font.js │ ├── framebuffer.js │ ├── fscreen.js │ ├── geom.js │ ├── geom_types.ts │ ├── glb │ │ ├── decode-utf8.js │ │ ├── gltf-type-utils.js │ │ ├── parser.js │ │ ├── unpack-binary-json.js │ │ └── unpack-glb-buffers.js │ ├── global.d.ts │ ├── hsv.js │ ├── in_event.js │ ├── input.js │ ├── input_constants.ts │ ├── link.js │ ├── local_storage.ts │ ├── localization.ts │ ├── mat2d.js │ ├── mat43.js │ ├── mat4ScaleRotateTranslate.js │ ├── models.js │ ├── models │ │ ├── box_textured_embed.glb │ │ └── box_textured_embed.gltf │ ├── net.js │ ├── net_position_manager.js │ ├── particles.js │ ├── perf.js │ ├── perf_net.ts │ ├── pico8.js │ ├── platformer.js │ ├── pointer_lock.js │ ├── polyfill.js │ ├── profiler.js │ ├── profiler_ui.js │ ├── quat.js │ ├── rand_fast.js │ ├── require.js │ ├── round_robinable.ts │ ├── score.ts │ ├── score_ui.ts │ ├── scroll_area.ts │ ├── selection_box.d.ts │ ├── selection_box.js │ ├── settings.js │ ├── shader_debug_ui.js │ ├── shaders.js │ ├── shaders │ │ ├── default.fp │ │ ├── default.vp │ │ ├── effects_bloom_merge.fp │ │ ├── effects_bloom_threshold.fp │ │ ├── effects_color_matrix.fp │ │ ├── effects_copy.fp │ │ ├── effects_copy.vp │ │ ├── effects_distort.fp │ │ ├── effects_gaussian_blur.fp │ │ ├── error.fp │ │ ├── error.vp │ │ ├── error_gl2.fp │ │ ├── font_aa.fp │ │ ├── font_aa_glow.fp │ │ ├── font_aa_outline.fp │ │ ├── font_aa_outline_glow.fp │ │ ├── pixely_expand.fp │ │ ├── snapshot.fp │ │ ├── sprite.fp │ │ ├── sprite.vp │ │ ├── sprite3d.vp │ │ ├── sprite_dual.fp │ │ └── transition_pixelate.fp │ ├── shims │ │ ├── assert.js │ │ ├── buffer.js │ │ ├── empty.js │ │ └── timers.js │ ├── simple_menu.d.ts │ ├── simple_menu.js │ ├── slider.js │ ├── snapshot.js │ ├── social.ts │ ├── sound.ts │ ├── soundscape.ts │ ├── soundscape_types.ts │ ├── spine.js │ ├── spot.ts │ ├── sprite_animation.ts │ ├── sprite_sets.js │ ├── sprites.d.ts │ ├── sprites.js │ ├── spritesheet.js │ ├── subscription_manager.js │ ├── terminal.js │ ├── terminal_settings.js │ ├── test.ts │ ├── textures.js │ ├── transition.js │ ├── ui.d.ts │ ├── ui.js │ ├── ui_test.ts │ ├── uistyle.ts │ ├── urlhash.js │ ├── walltime.js │ ├── webfs.js │ ├── words │ │ ├── profanity.js │ │ └── replacements.txt │ ├── worker_comm.js │ ├── worker_perf.js │ ├── worker_thread.js │ └── wsclient.js ├── common │ ├── ack.js │ ├── base32.js │ ├── base64.js │ ├── chunked_send.js │ ├── cmd_parse.js │ ├── crc32.js │ ├── data_error.ts │ ├── differ.ts │ ├── dot-prop.js │ ├── entity_base_common.ts │ ├── enums.ts │ ├── execute_with_retry.ts │ ├── external_users_common.ts │ ├── fifo.ts │ ├── friends_data.ts │ ├── fsapi.ts │ ├── gl-matrix-types.d.ts │ ├── global.d.ts │ ├── md5.js │ ├── packet.d.ts │ ├── packet.js │ ├── perfcounters.js │ ├── platform.ts │ ├── rand_alea.js │ ├── replacement_chars.js │ ├── texpack_common.js │ ├── tiny-events.js │ ├── trait_factory.ts │ ├── types.ts │ ├── util.ts │ ├── verify.ts │ ├── vmath.ts │ ├── words │ │ ├── exceptions.txt │ │ ├── filter.gkg │ │ └── profanity_common.js │ └── wscommon.js ├── package.json ├── server │ ├── channel_data_differ.js │ ├── channel_server.js │ ├── channel_server_worker.js │ ├── channel_worker.ts │ ├── chattable_worker.ts │ ├── class_proxy.js │ ├── client_comm.js │ ├── client_worker.js │ ├── data_store.js │ ├── data_store_image.js │ ├── data_store_limited.js │ ├── data_store_mirror.js │ ├── data_store_shield.js │ ├── data_stores_init.js │ ├── default_workers.js │ ├── entity_base_server.ts │ ├── entity_manager_server.ts │ ├── error_reports.js │ ├── exchange.ts │ ├── exchange_gmx_client.ts │ ├── exchange_gmx_common.ts │ ├── exchange_gmx_server.ts │ ├── exchange_hashed.ts │ ├── exchange_local_bypass.ts │ ├── external_users_validation.ts │ ├── global_worker.ts │ ├── idmapper_worker.ts │ ├── ip_ban.ts │ ├── key_metrics.ts │ ├── load_bias_map.js │ ├── log.js │ ├── master_worker.js │ ├── metrics.js │ ├── must_import.js │ ├── packet_log.js │ ├── perm_token_worker.ts │ ├── random_names.js │ ├── ready_data.ts │ ├── request_utils.js │ ├── server.js │ ├── server_config.js │ ├── server_filewatch.ts │ ├── server_globals.ts │ ├── server_util.ts │ ├── serverfs.ts │ ├── shader_stats.js │ ├── test.ts │ ├── usertime.ts │ ├── version_management.ts │ └── wsserver.js └── tests │ ├── client │ ├── test-glov-client.ts │ └── test-round-robinable.ts │ ├── common │ ├── dummyfs.ts │ ├── test-differ.ts │ ├── test-traitfactory.ts │ ├── test-traitstate.ts │ ├── test-util-promisesprotection.js │ └── test-util.ts │ └── server │ └── test-load_bias_map.ts ├── server ├── index.js └── test_worker.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | insert_final_newline = true 13 | 14 | [*.js] 15 | insert_final_newline = true 16 | 17 | [*.yaml] 18 | insert_final_newline = true 19 | [*.entdef] 20 | insert_final_newline = true 21 | [*.texopt] 22 | insert_final_newline = true 23 | 24 | [*.json] 25 | insert_final_newline = true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data_store/ 2 | artifacts/ 3 | node_modules/ 4 | logs/ 5 | ynpm-debug.log 6 | npm-debug.log 7 | build.dev/ 8 | build.prod/ 9 | build.test/ 10 | .gbstate/ 11 | *.sublime-workspace 12 | Bfxr 13 | *.00? 14 | config/*.json 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gamedev.js Jam 2022 - _RAW_ 2 | ============================ 3 | 4 | Game jam entry by Jimbly and Benjaminsen - "Aaron's Quest IV: When Moses Was Away" 5 | 6 | * Play here: [dashingstrike.com/LudumDare/JS22/](http://www.dashingstrike.com/LudumDare/JS22/) 7 | * Using [Javascript libGlov/GLOV.js framework](https://github.com/Jimbly/glovjs) 8 | -------------------------------------------------------------------------------- /build/checks.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const JSON5 = require('json5'); 3 | const args = require('minimist')(process.argv.slice(2)); 4 | 5 | function requireVersion(dep, required) { 6 | let ver; 7 | if (dep === 'nodejs') { 8 | ver = process.versions.node; 9 | } else { 10 | try { 11 | // eslint-disable-next-line global-require, import/no-dynamic-require 12 | ver = require(`${dep}/package.json`).version; 13 | } catch (e) { 14 | return `"${dep}": missing`; 15 | } 16 | } 17 | ver = ver.split('.').map(Number); 18 | if (ver.length !== 3) { 19 | return `"${dep}": unable to parse version for package`; 20 | } 21 | required = required.split('.').map(Number); 22 | if (ver[0] !== required[0] || ver[1] < required[1] || ver[1] === required[1] && ver[2] < required[2]) { 23 | return `"${dep}": expected ${required.join('.')}+, found ${ver.join('.')}`; 24 | } 25 | return null; 26 | } 27 | function requireVersions(versions) { 28 | let errors = []; 29 | for (let key in versions) { 30 | let err = requireVersion(key, versions[key]); 31 | if (err) { 32 | errors.push(err); 33 | } 34 | } 35 | if (errors.length) { 36 | console.error('Required dependencies missing or out of date:'); 37 | for (let ii = 0; ii < errors.length; ++ii) { 38 | console.error(` ${errors[ii]}`); 39 | } 40 | console.error('Please run `npm i` to install them.'); 41 | process.exit(-1); 42 | } 43 | } 44 | 45 | module.exports = function (filename) { 46 | if (fs.readFileSync(filename, 'utf8').includes('\r\n')) { 47 | // CRLF Line endings currently break gulp-ifdef, mess up with git diff/log/blame, and 48 | // cause unnecessary diffs when pushing builds to production servers. 49 | console.error('ERROR: Windows line endings detected'); 50 | console.error('Check your git config and make sure core.autocrlf is false:\n' + 51 | ' git config --get core.autocrlf\n' + 52 | ' git config --global --add core.autocrlf false\n' + 53 | ' (or --local if you want it on for other projects)'); 54 | process.exit(-1); 55 | } 56 | 57 | function prettyInterface() { 58 | // eslint-disable-next-line global-require 59 | const console_api = require('console-api'); 60 | console_api.setPalette(console_api.palettes.desaturated); 61 | let project_name = 'glov'; 62 | try { 63 | let pkg = JSON5.parse(fs.readFileSync('./package.json', 'utf8')); 64 | if (pkg && pkg.name) { 65 | project_name = pkg.name; 66 | } 67 | } catch (e) { 68 | // ignored, use default 69 | } 70 | console_api.setTitle(args.title || `build ${args._ || filename} | ${project_name}`); 71 | } 72 | prettyInterface(); 73 | 74 | requireVersions({ 75 | 'nodejs': '16.13.0', 76 | 'glov-build': '1.0.43', 77 | 'glov-build-browserify': '1.0.8', 78 | 'glov-build-cache': '1.1.0', 79 | 'glov-build-concat': '1.0.10', 80 | 'glov-build-preresolve': '1.2.0', 81 | '@jimbly/howler': '0.0.9', 82 | '@jimbly/babel-plugin-transform-modules-simple-commonjs': '0.0.3', 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /build/compress.js: -------------------------------------------------------------------------------- 1 | const { brotliCompress, gzip } = require('zlib'); 2 | const gb = require('glov-build'); 3 | const micromatch = require('micromatch'); 4 | 5 | function gbif(globs, fn) { 6 | return function (job, done) { 7 | let file = job.getFile(); 8 | if (micromatch(file.relative, globs).length) { 9 | fn(job, done); 10 | } else { 11 | job.out(file); 12 | done(); 13 | } 14 | }; 15 | } 16 | 17 | 18 | module.exports = function (globs) { 19 | 20 | function compressFunc(job, done) { 21 | let file = job.getFile(); 22 | job.out(file); // pass through uncompressed file 23 | brotliCompress(file.contents, function (err, buffer_br) { 24 | if (err) { 25 | return void done(err); 26 | } 27 | job.out({ 28 | relative: `${file.relative}.br`, 29 | contents: buffer_br, 30 | }); 31 | gzip(file.contents, function (err, buffer_gz) { 32 | if (err) { 33 | return void done(err); 34 | } 35 | job.out({ 36 | relative: `${file.relative}.gz`, 37 | contents: buffer_gz, 38 | }); 39 | done(); 40 | }); 41 | }); 42 | } 43 | 44 | return { 45 | type: gb.SINGLE, 46 | func: gbif(globs, compressFunc), 47 | version: [ 48 | globs, 49 | compressFunc, 50 | ], 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /build/config.project.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.extra_index = [{ 3 | name: 'crazy', 4 | defines: { 5 | PLATFORM: 'crazy', 6 | ENV: '', 7 | }, 8 | zip: true, 9 | }, { 10 | name: 'itch', 11 | defines: { 12 | PLATFORM: 'itch', 13 | ENV: '', 14 | }, 15 | zip: true, 16 | }]; 17 | }; 18 | -------------------------------------------------------------------------------- /build/gulpish-tasks.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require:off */ 2 | const gulpish = require('./gulpish.js'); 3 | 4 | // example, superseded by `build/eslint.js` 5 | // unused in this project 6 | exports.eslint = function () { 7 | return gulpish(null, function (stream) { 8 | const eslint = require('gulp-eslint'); 9 | let ret = stream.pipe(eslint()) 10 | .pipe(eslint.format()); 11 | ret = ret.pipe(eslint.failAfterError()); 12 | return ret; 13 | }); 14 | }; 15 | 16 | exports.client_html_default = function (target, default_defines) { 17 | return gulpish(target, function (stream) { 18 | const ifdef = require('gulp-ifdef'); 19 | const lazypipe = require('lazypipe'); 20 | const sourcemaps = require('gulp-sourcemaps'); 21 | const useref = require('gulp-useref'); 22 | const replace = require('gulp-replace'); 23 | return stream.pipe(useref({}, lazypipe().pipe(sourcemaps.init, { loadMaps: true }))) 24 | //.on('error', log.error.bind(log, 'client_html Error')) 25 | .pipe(ifdef(default_defines, { extname: ['html'] })) 26 | .pipe(replace(/#\{([^}]+)\}/g, function (a, b) { 27 | return (b in default_defines) ? default_defines[b] : 'UKNOWN_DEFINE'; 28 | })) 29 | .pipe(sourcemaps.write('./')); // writes .map file 30 | }); 31 | }; 32 | 33 | exports.client_html_custom = function (target, elem) { 34 | return gulpish(target, function (stream) { 35 | const ifdef = require('gulp-ifdef'); 36 | const rename = require('gulp-rename'); 37 | const replace = require('gulp-replace'); 38 | return stream 39 | .pipe(ifdef(elem.defines, { extname: ['html'] })) 40 | .pipe(rename(`client/index_${elem.name}.html`)) 41 | .pipe(replace(/#\{([^}]+)\}/g, function (a, b) { 42 | return (b in elem.defines) ? elem.defines[b] : 'UKNOWN_DEFINE'; 43 | })) 44 | .pipe(replace(/[^!]+/g, function (a, b) { 45 | // already bundled in client_html_default, just export filename reference 46 | return ``; 47 | })); 48 | }); 49 | }; 50 | 51 | exports.zip = function (target, elem) { 52 | return gulpish(target, function (stream) { 53 | const gulpif = require('gulp-if'); 54 | const ignore = require('gulp-ignore'); 55 | const rename = require('gulp-rename'); 56 | const zip = require('gulp-zip'); 57 | return stream 58 | .pipe(rename(function (path) { 59 | path.dirname = path.dirname.replace(/^client[/\\]?/, ''); 60 | })) 61 | .pipe(ignore.exclude('index.html')) 62 | .pipe(ignore.exclude('*.map')) 63 | .pipe(gulpif(`index_${elem.name}.html`, rename('index.html'))) 64 | .pipe(ignore.exclude('index_*.html')) 65 | .pipe(zip(`client/${elem.name}.zip`)); 66 | }); 67 | }; 68 | 69 | // example, superseded by `build/compress.js` 70 | // unused in this project 71 | exports.compress = function (target, compress_files) { 72 | return gulpish(target, function (stream) { 73 | const gulpif = require('gulp-if'); 74 | const web_compress = require('gulp-web-compress'); 75 | return stream 76 | // skipLarger so we don't end up with orphaned old compressed files 77 | // - not strictly needed after migrating to `glov-build` though! 78 | .pipe(gulpif(compress_files, web_compress({ skipLarger: false }))); 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /build/json5.js: -------------------------------------------------------------------------------- 1 | const gb = require('glov-build'); 2 | const JSON5 = require('json5'); 3 | 4 | module.exports = function (options) { 5 | options = options || {}; 6 | options.beautify = options.beautify === undefined ? true : options.beautify; 7 | 8 | function parseJSON5(job, done) { 9 | let file = job.getFile(); 10 | let obj; 11 | try { 12 | obj = JSON5.parse(String(file.contents)); 13 | } catch (err) { 14 | return void done(err); 15 | } 16 | job.out({ 17 | relative: file.relative.replace(/\.json5$/, '.json'), 18 | contents: Buffer.from(JSON.stringify(obj, null, options.beautify ? 2 : null)), 19 | }); 20 | done(); 21 | } 22 | 23 | return { 24 | type: gb.SINGLE, 25 | func: parseJSON5, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /build/png.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { PNG } = require('pngjs'); 3 | 4 | const { min } = Math; 5 | 6 | // const PNG_GRAYSCALE = 0; 7 | const PNG_RGB = 2; 8 | const PNG_RGBA = 6; 9 | 10 | // Good bilinear reduction, might not be great for expansion 11 | exports.drawImageBilinear = function drawImageBilinear( 12 | dest, dbpp, dx, dy, dw, dh, src, sbpp, sx0, sy0, sw, sh, channel_mask 13 | ) { 14 | let dd = dest.data; 15 | let target_width = dest.width; 16 | let sd = src.data; 17 | let source_width = src.width; 18 | let ratiox = sw / dw; 19 | let ratioy = sh / dh; 20 | for (let jj = 0; jj < dh; ++jj) { 21 | let sy = ratioy * (jj + 0.5) - 0.5; 22 | let sy_low = sy | 0; 23 | let sy_high = min(sh - 1, sy_low + 1); 24 | let sy_w = sy - sy_low; 25 | let inv_sy_w = 1 - sy_w; 26 | let dyidx = (dy + jj) * target_width; 27 | sy_low += sy0; 28 | sy_high += sy0; 29 | sy_low *= source_width * sbpp; 30 | sy_high *= source_width * sbpp; 31 | for (let ii = 0; ii < dw; ++ii) { 32 | let sx = ratiox * (ii + 0.5) - 0.5; 33 | let sx_low = sx | 0; 34 | let sx_high = min(sw - 1, sx_low + 1); 35 | let sx_w = sx - sx_low; 36 | let inv_sx_w = 1 - sx_w; 37 | sx_low += sx0; 38 | sx_high += sx0; 39 | sx_low *= sbpp; 40 | sx_high *= sbpp; 41 | let idxa = sx_low + sy_low; 42 | let idxb = sx_low + sy_high; 43 | let idxc = sx_high + sy_low; 44 | let idxd = sx_high + sy_high; 45 | for (let kk = 0; kk < dbpp; ++kk) { 46 | if ((1 << kk) & channel_mask) { 47 | let a = sd[idxa + kk]; 48 | let b = sd[idxb + kk]; 49 | let c = sd[idxc + kk]; 50 | let d = sd[idxd + kk]; 51 | let ab = a * inv_sy_w + b * sy_w; 52 | let cd = c * inv_sy_w + d * sy_w; 53 | dd[(dx + ii + dyidx) * dbpp + kk] = ab * inv_sx_w + cd * sx_w; 54 | } 55 | } 56 | } 57 | } 58 | }; 59 | 60 | // Returns { err, img: { width, height, data } } 61 | function pngRead(file_contents) { 62 | let img; 63 | try { 64 | img = PNG.sync.read(file_contents); 65 | } catch (e) { 66 | if (e.toString().indexOf('at end of stream') !== -1) { 67 | // Chrome stated adding an extra 0?! 68 | // Also, Photoshop sometimes adds an entire extra PNG file?! 69 | // Slice down to the expected location derived from IEND (repeatedly, in case that's part of a zlib string) 70 | let contents = file_contents; 71 | while (true) { 72 | let idx = contents.lastIndexOf('IEND'); 73 | if (idx === -1) { 74 | // something else at the end 75 | return { err: e }; 76 | } 77 | contents = contents.slice(0, idx + 8); 78 | try { 79 | img = PNG.sync.read(contents); 80 | break; 81 | } catch (e2) { 82 | contents = contents.slice(0, idx); 83 | } 84 | } 85 | } else { 86 | return { err: e }; 87 | } 88 | } 89 | let { width, height, data } = img; 90 | assert.equal(width * height * 4, data.length); 91 | return { img }; 92 | } 93 | exports.pngRead = pngRead; 94 | 95 | 96 | function pngAlloc({ width, height, byte_depth }) { 97 | let colorType = byte_depth === 3 ? PNG_RGB : PNG_RGBA; 98 | let ret = new PNG({ width, height, colorType }); 99 | let num_bytes = width * height * 4; 100 | assert.equal(ret.data.length, num_bytes); 101 | return ret; 102 | } 103 | exports.pngAlloc = pngAlloc; 104 | 105 | // img is from pngAlloc or pngRead 106 | function pngWrite(img) { 107 | return PNG.sync.write(img); 108 | } 109 | exports.pngWrite = pngWrite; 110 | -------------------------------------------------------------------------------- /build/texpack.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const gb = require('glov-build'); 3 | const { 4 | FORMAT_PACK, 5 | TEXPACK_MAGIC, 6 | } = require('../src/glov/common/texpack_common'); 7 | 8 | function texPackMakeTXP(flags, out) { 9 | let num_files = out.length; 10 | assert(num_files > 1); 11 | assert(flags & FORMAT_PACK); 12 | 13 | let header = Buffer.alloc((num_files + 3) * 4); 14 | let header_idx = 0; 15 | header.writeUInt32LE(TEXPACK_MAGIC, header_idx); 16 | header_idx += 4; 17 | header.writeUInt32LE(num_files, header_idx); 18 | header_idx += 4; 19 | header.writeUInt32LE(flags, header_idx); 20 | header_idx += 4; 21 | let buffs = [header]; 22 | for (let ii = 0; ii < out.length; ++ii) { 23 | let buf = out[ii]; 24 | header.writeUInt32LE(buf.length, header_idx); 25 | header_idx += 4; 26 | buffs.push(buf); 27 | } 28 | assert.equal(header_idx, header.length); 29 | return Buffer.concat(buffs); 30 | } 31 | exports.texPackMakeTXP = texPackMakeTXP; 32 | 33 | function texPackParseTXP(buf) { 34 | let offs = 0; 35 | let magic = buf.readUint32LE(offs); 36 | offs+=4; 37 | assert.equal(magic, TEXPACK_MAGIC); 38 | let num_files = buf.readUint32LE(offs); 39 | offs+=4; 40 | let flags = buf.readUint32LE(offs); 41 | offs+=4; 42 | let lens = []; 43 | for (let ii = 0; ii < num_files; ++ii) { 44 | lens.push(buf.readUint32LE(offs)); 45 | offs+=4; 46 | } 47 | let bufs = []; 48 | for (let ii = 0; ii < num_files; ++ii) { 49 | bufs.push(buf.slice(offs, offs + lens[ii])); 50 | offs += lens[ii]; 51 | } 52 | assert.equal(offs, buf.length); 53 | return { 54 | flags, 55 | bufs, 56 | }; 57 | } 58 | 59 | exports.texPackExtractPNG = function () { 60 | function extractPNG(job, done) { 61 | let file = job.getFile(); 62 | assert(file.relative.endsWith('.txp')); 63 | let basename = file.relative.slice(0, -'.txp'.length); 64 | let { flags, bufs } = texPackParseTXP(file.contents); 65 | for (let ii = 0; ii < bufs.length; ++ii) { 66 | job.out({ 67 | relative: `${basename}.extract.${ii}.${flags}.png`, 68 | contents: bufs[ii], 69 | }); 70 | } 71 | done(); 72 | } 73 | return { 74 | type: gb.SINGLE, 75 | func: extractPNG, 76 | }; 77 | }; 78 | 79 | exports.texPackRecombinePNG = function () { 80 | function recombinePNG(job, done) { 81 | let files = job.getFiles(); 82 | let by_keys = {}; 83 | let flags; 84 | for (let ii = 0; ii < files.length; ++ii) { 85 | let file = files[ii]; 86 | let m = file.relative.match(/^(.*)\.extract.(\d+).(\d+)\.png$/); 87 | if (!m) { 88 | job.out(file); 89 | } else { 90 | let base = m[1]; 91 | by_keys[base] = (by_keys[base] || []); 92 | by_keys[base].push([Number(m[2]), file]); 93 | let this_flags = Number(m[3]); 94 | if (flags === undefined) { 95 | flags = this_flags; 96 | } else { 97 | assert.equal(flags, this_flags); 98 | } 99 | } 100 | } 101 | for (let key in by_keys) { 102 | let list = by_keys[key]; 103 | list.sort((a, b) => a[0] - b[0]); 104 | job.out({ 105 | relative: `${key}.txp`, 106 | contents: texPackMakeTXP(flags, list.map((a) => a[1].contents)), 107 | }); 108 | } 109 | done(); 110 | } 111 | return { 112 | type: gb.ALL, 113 | func: recombinePNG, 114 | }; 115 | }; 116 | -------------------------------------------------------------------------------- /build/uglify.js: -------------------------------------------------------------------------------- 1 | const gb = require('glov-build'); 2 | const sourcemap = require('glov-build-sourcemap'); 3 | 4 | module.exports = function (opts, uglify_opts) { 5 | let uglify; 6 | return { 7 | type: gb.SINGLE, 8 | init: function (next) { 9 | uglify = require('uglify-js'); // eslint-disable-line global-require 10 | next(); 11 | }, 12 | func: function (job, done) { 13 | let file = job.getFile(); 14 | job.depReset(); 15 | let uglify_options = { 16 | ...uglify_opts 17 | }; 18 | function doit() { 19 | let files = {}; 20 | files[file.relative] = String(file.contents); 21 | 22 | let mangled = uglify.minify(files, uglify_options); 23 | if (!mangled || mangled.error) { 24 | return void done(mangled && mangled.error || 'Uglify error'); 25 | } 26 | if (mangled.warnings) { 27 | mangled.warnings.forEach(function (warn) { 28 | job.warn(warn); 29 | }); 30 | } 31 | 32 | if (opts.no_sourcemap) { 33 | job.out({ 34 | relative: file.relative, 35 | contents: mangled.code, 36 | }); 37 | } else { 38 | sourcemap.out(job, { 39 | relative: file.relative, 40 | contents: mangled.code, 41 | map: mangled.map, 42 | inline: opts.inline, 43 | }); 44 | } 45 | done(); 46 | } 47 | if (opts.no_sourcemap) { 48 | doit(); 49 | } else { 50 | sourcemap.init(job, file, function (err, map) { 51 | if (err && !opts.no_sourcemap) { 52 | return void done(err); 53 | } 54 | uglify_options.sourceMap = { 55 | filename: map.file, 56 | includeSources: true, 57 | content: map, 58 | }; 59 | doit(); 60 | }); 61 | } 62 | }, 63 | version: [ 64 | opts, 65 | uglify_opts, 66 | ], 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /build/warn-match.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const gb = require('glov-build'); 3 | 4 | // patterns = { 'No eval': /\beval\b/ } 5 | module.exports = function warnMatch(patterns) { 6 | assert.equal(typeof patterns, 'object'); 7 | for (let key in patterns) { 8 | assert(patterns[key] instanceof RegExp || typeof patterns[key] === 'string'); 9 | } 10 | return { 11 | type: gb.SINGLE, 12 | func: function (job, done) { 13 | let file = job.getFile(); 14 | let data = file.contents.toString(); 15 | for (let key in patterns) { 16 | if (data.match(patterns[key])) { 17 | job.warn(`${file.relative}: failed ${key}`); 18 | } 19 | } 20 | done(); 21 | }, 22 | version: [ 23 | patterns, 24 | ], 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /build/webfs_build.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const path = require('path'); 3 | const { forwardSlashes } = require('glov-build'); 4 | const concat = require('glov-build-concat'); 5 | const JSON5 = require('json5'); 6 | 7 | const preamble = `(function () { 8 | var fs = window.glov_webfs = window.glov_webfs || {};`; 9 | const postamble = '}());'; 10 | 11 | let chars = (function () { 12 | const ESC = String.fromCharCode(27); 13 | let ret = []; 14 | for (let ii = 0; ii < 256; ++ii) { 15 | ret[ii] = String.fromCharCode(ii); 16 | } 17 | // ASCII text must encode directly 18 | // single-byte nulls 19 | ret[0] = String.fromCharCode(126); 20 | // escape our escape character and otherwise overlapped values 21 | ret[27] = `${ESC}${String.fromCharCode(27)}`; 22 | ret[126] = `${ESC}${String.fromCharCode(126)}`; 23 | // escape things not valid in Javascript strings 24 | ret[8] = '\\b'; 25 | ret[9] = '\\t'; 26 | ret[10] = '\\n'; 27 | ret[11] = '\\v'; 28 | ret[12] = '\\f'; 29 | ret[13] = '\\r'; 30 | ret['\''.charCodeAt(0)] = '\\\''; 31 | ret['\\'.charCodeAt(0)] = '\\\\'; 32 | // All other characters are fine (though many get turned into 2-byte UTF-8 strings) 33 | return ret; 34 | }()); 35 | 36 | function encodeString(buf) { 37 | let ret = []; 38 | for (let ii = 0; ii < buf.length; ++ii) { 39 | let c = buf[ii]; 40 | ret.push(chars[c]); 41 | } 42 | return ret.join(''); 43 | } 44 | 45 | function encodeObj(obj) { 46 | return JSON5.stringify(obj); 47 | } 48 | 49 | function fileFSName(opts, name) { 50 | name = forwardSlashes(name).replace('autogen/', ''); 51 | if (opts.base) { 52 | name = forwardSlashes(path.relative(opts.base, name)); 53 | } 54 | // Remap `../glov/client/shaders/foo.fp` to be just `shaders/foo.fp` 55 | let non_glov_name = name.replace(/(.*glov\/(?:client|common)\/)/, ''); 56 | if (name !== non_glov_name) { 57 | return { name: non_glov_name, priority: 1 }; 58 | } else { 59 | return { name, priority: 2 }; 60 | } 61 | } 62 | 63 | module.exports = function webfsBuild(opts) { 64 | let { output, embed, strip } = opts; 65 | let ext_list = embed || ['.json']; 66 | let strip_ext_list = strip || ['.json']; 67 | assert(output); 68 | 69 | let embed_exts = {}; 70 | for (let ii = 0; ii < ext_list.length; ++ii) { 71 | embed_exts[ext_list[ii]] = true; 72 | } 73 | let strip_exts = {}; 74 | for (let ii = 0; ii < strip_ext_list.length; ++ii) { 75 | strip_exts[strip_ext_list[ii]] = true; 76 | } 77 | 78 | return concat({ 79 | preamble, 80 | postamble, 81 | output: output, 82 | key: 'name', 83 | proc: function (job, file, next) { 84 | let { name, priority } = fileFSName(opts, file.relative); 85 | let data = file.contents; 86 | let line; 87 | let ext_idx = name.lastIndexOf('.'); 88 | let ext = ''; 89 | if (ext_idx !== -1) { 90 | ext = name.slice(ext_idx); 91 | } 92 | if (strip_exts[ext]) { 93 | name = name.slice(0, -ext.length); 94 | } 95 | if (embed_exts[ext]) { 96 | line = `fs['${name}'] = ${encodeObj(JSON.parse(data))};`; 97 | } else { 98 | line = `fs['${name}'] = [${data.length},'${encodeString(data)}'];`; 99 | } 100 | next(null, { name, contents: line, priority }); 101 | }, 102 | version: [ 103 | encodeObj, 104 | encodeString, 105 | fileFSName, 106 | ext_list, 107 | strip_ext_list, 108 | ], 109 | }); 110 | }; 111 | -------------------------------------------------------------------------------- /jsjam22.sublime-project: -------------------------------------------------------------------------------- 1 | //json5 2 | { 3 | "folders": 4 | [ 5 | { 6 | "folder_exclude_patterns": 7 | [ 8 | "node_modules", 9 | ".gbstate", 10 | "build.dev", 11 | "build.prod", 12 | "build.test", 13 | "logs", 14 | "data_store", 15 | ], 16 | "file_exclude_patterns": ["jquery-*.min*", "*.00?", "*.wav", "*.mp3", "*.ogg", "package-lock.json", "*.glb"], 17 | "path": "." 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsjam22", 3 | "version": "0.1.0", 4 | "description": "Template for a JavaScript game project using GLOV.js", 5 | "main": "server/index.js", 6 | "keywords": [ 7 | "template", 8 | "glov", 9 | "glovjs", 10 | "glov-build", 11 | "browserify", 12 | "babel" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:Jimbly/jsjam22.git" 17 | }, 18 | "scripts": { 19 | "start": "nodemon -w build -w ../glov-build/ -- build default --watch", 20 | "clean": "node build clean", 21 | "build": "node build build", 22 | "cache": "node build build.prod.png client_autosound --cache-rebuild", 23 | "prod": "node build build && node dist/game/build.prod/server/index.js --master", 24 | "test_watch": "nodemon -w build -- build test --watch", 25 | "test": "node build lint test" 26 | }, 27 | "author": "Jimb Esser (https://github.com/Jimbly)", 28 | "contributors": [ 29 | "Jimb Esser (https://github.com/Jimbly)" 30 | ], 31 | "license": "MIT", 32 | "dependencies": { 33 | "express": "^4.17.1", 34 | "express-static-gzip": "^2.0.5", 35 | "fs-store": "^0.3.2", 36 | "fs-store-async": "^0.3.3", 37 | "gl-mat3": "^2.0.0", 38 | "gl-mat4": "^1.2.0", 39 | "glov-async": "^1.0.3", 40 | "glslang-validator-prebuilt-predownloaded": "^0.0.2", 41 | "json5": "^2.1.3", 42 | "minimist": "^1.2.5", 43 | "mkdirp": "^0.5.1", 44 | "request": "^2.88.2", 45 | "winston": "^3.2.1", 46 | "winston-daily-rotate-file": "^4.5.2", 47 | "ws": "^7.1.1" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.10.2", 51 | "@babel/preset-env": "7.15.6", 52 | "@babel/preset-typescript": "^7.17.12", 53 | "@jimbly/babel-plugin-transform-modules-simple-commonjs": "0.0.3", 54 | "@jimbly/howler": "0.0.9", 55 | "@jimbly/vorbis-encoder-js": "0.0.1", 56 | "@types/express": "^4.17.13", 57 | "@types/node": "^16.9.6", 58 | "@types/request": "^2.48.7", 59 | "@typescript-eslint/eslint-plugin": "^4.33.0", 60 | "@typescript-eslint/parser": "^4.33.0", 61 | "babel-plugin-replace-ts-export-assignment": "^0.0.2", 62 | "babel-plugin-static-fs": "^3.0.0", 63 | "babel-plugin-transform-preprocessor": "^1.0.0", 64 | "babelify": "^10.0.0", 65 | "browser-sync": "^2.26.7", 66 | "console-api": "0.0.5", 67 | "eslint": "^7.32.0", 68 | "eslint-plugin-html": "^7.1.0", 69 | "eslint-plugin-import": "^2.26.0", 70 | "glov-build": "1.0.43", 71 | "glov-build-babel": "1.0.4", 72 | "glov-build-browserify": "1.0.8", 73 | "glov-build-cache": "1.1.0", 74 | "glov-build-concat": "1.0.10", 75 | "glov-build-imagemin": "^1.0.0", 76 | "glov-build-preresolve": "1.2.0", 77 | "glov-build-sourcemap": "1.0.5", 78 | "gulp-if": "^3.0.0", 79 | "gulp-ifdef": "^0.2.0", 80 | "gulp-ignore": "^3.0.0", 81 | "gulp-rename": "^2.0.0", 82 | "gulp-replace": "^1.0.0", 83 | "gulp-sourcemaps": "^2.6.5", 84 | "gulp-useref": "^3.1.6", 85 | "gulp-zip": "^5.0.2", 86 | "imagemin-optipng": "^8.0.0", 87 | "imagemin-zopfli": "^7.0.0", 88 | "js-yaml": "^4.1.0", 89 | "lamejs": "1.2.0", 90 | "lazypipe": "^1.0.1", 91 | "micromatch": "^4.0.2", 92 | "mpg123-decoder": "^0.4.7", 93 | "node-wav": "^0.0.2", 94 | "nodemon": "^1.19.1", 95 | "pngjs": "^6.0.0", 96 | "regexp-sourcemaps": "^1.0.1", 97 | "typescript": "4.4.3", 98 | "uglify-js": "3.14.2" 99 | }, 100 | "optionalDependencies": { 101 | "bufferutil": "^4.0.1" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /screenshots/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/screenshots/ss1.png -------------------------------------------------------------------------------- /screenshots/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/screenshots/ss2.png -------------------------------------------------------------------------------- /src/client/app.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require:off */ 2 | 3 | // For debug and used internally in the build/bundling pipeline 4 | window.glov_build_version=BUILD_TIMESTAMP; 5 | 6 | // Startup code. 7 | 8 | let called_once = false; 9 | function onLoad() { 10 | if (called_once) { 11 | return; 12 | } 13 | called_once = true; 14 | window.time_load_onload = Date.now(); 15 | require('glov/client/bootstrap.js'); 16 | require('./main.js').main(); 17 | window.time_load_init = Date.now(); 18 | } 19 | 20 | window.addEventListener('DOMContentLoaded', onLoad); 21 | 22 | window.onload = onLoad; 23 | -------------------------------------------------------------------------------- /src/client/app_deps.js: -------------------------------------------------------------------------------- 1 | /* globals deps */ 2 | require('../glov/client/require.js'); 3 | 4 | // Node built-in replacements 5 | deps.assert = require('assert'); 6 | deps.buffer = require('buffer'); 7 | deps['glov-async'] = require('glov-async'); 8 | deps['gl-mat3/create'] = require('gl-mat3/create'); 9 | deps['gl-mat3/fromMat4'] = require('gl-mat3/fromMat4'); 10 | deps['gl-mat4/copy'] = require('gl-mat4/copy'); 11 | deps['gl-mat4/create'] = require('gl-mat4/create'); 12 | deps['gl-mat4/invert'] = require('gl-mat4/invert'); 13 | deps['gl-mat4/lookAt'] = require('gl-mat4/lookAt'); 14 | deps['gl-mat4/multiply'] = require('gl-mat4/multiply'); 15 | deps['gl-mat4/perspective'] = require('gl-mat4/perspective'); 16 | deps['gl-mat4/transpose'] = require('gl-mat4/transpose'); 17 | deps['@jimbly/howler/src/howler.core.js'] = require('@jimbly/howler/src/howler.core.js'); 18 | -------------------------------------------------------------------------------- /src/client/crazy_wrapper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Crazy Games 5 | 6 | 7 | 8 | 9 | 10 | 11 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/favicon.ico -------------------------------------------------------------------------------- /src/client/img/crazy_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/crazy_banner.png -------------------------------------------------------------------------------- /src/client/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/favicon-16x16.png -------------------------------------------------------------------------------- /src/client/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/favicon-32x32.png -------------------------------------------------------------------------------- /src/client/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/favicon.png -------------------------------------------------------------------------------- /src/client/img/font/04b03_8x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/font/04b03_8x1.png -------------------------------------------------------------------------------- /src/client/img/font/vga_8x16x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/font/vga_8x16x1.png -------------------------------------------------------------------------------- /src/client/img/itch_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/itch_banner.png -------------------------------------------------------------------------------- /src/client/img/particles/circle8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/particles/circle8.png -------------------------------------------------------------------------------- /src/client/img/tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/tiles.png -------------------------------------------------------------------------------- /src/client/img/tiles_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/tiles_ui.png -------------------------------------------------------------------------------- /src/client/img/title.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/title.psd -------------------------------------------------------------------------------- /src/client/img/title_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/title_text.png -------------------------------------------------------------------------------- /src/client/img/ui/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/button.png -------------------------------------------------------------------------------- /src/client/img/ui/button_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/button_disabled.png -------------------------------------------------------------------------------- /src/client/img/ui/button_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/button_down.png -------------------------------------------------------------------------------- /src/client/img/ui/panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/panel.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/menu_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/menu_down.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/menu_entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/menu_entry.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/menu_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/menu_header.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/menu_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/menu_selected.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/progress_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/progress_bar.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/progress_bar_trough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/progress_bar_trough.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/scrollbar_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/scrollbar_bottom.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/scrollbar_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/scrollbar_handle.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/scrollbar_handle_grabber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/scrollbar_handle_grabber.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/scrollbar_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/scrollbar_top.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/scrollbar_trough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/scrollbar_trough.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/slider.png -------------------------------------------------------------------------------- /src/client/img/ui/pixely/slider_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/img/ui/pixely/slider_handle.png -------------------------------------------------------------------------------- /src/client/particle_data.js: -------------------------------------------------------------------------------- 1 | export let defs = {}; 2 | 3 | defs.explosion = { 4 | particles: { 5 | part0: { 6 | blend: 'alpha', 7 | texture: 'particles/circle8', 8 | color: [1,1,1,1], // multiplied by animation track, default 1,1,1,1, can be omitted 9 | color_track: [ 10 | // just values, NOT random range 11 | { t: 0.0, v: [1,1,1,0] }, 12 | { t: 0.2, v: [1,1,1,1] }, 13 | { t: 0.8, v: [1,1,1,1] }, 14 | { t: 1.0, v: [1,1,1,0] }, 15 | ], 16 | size: [[12,4], [12,4]], // multiplied by animation track 17 | size_track: [ 18 | // just values, NOT random range 19 | { t: 0.0, v: [1,1] }, 20 | { t: 0.2, v: [2,2] }, 21 | { t: 0.4, v: [1,1] }, 22 | { t: 1.0, v: [1.5,1.5] }, 23 | ], 24 | accel: [0,0,0], 25 | rot: [0,360], // degrees 26 | rot_vel: [10,2], // degrees per second 27 | lifespan: [500,0], // milliseconds 28 | kill_time_accel: 5, 29 | }, 30 | }, 31 | emitters: { 32 | part0: { 33 | particle: 'part0', 34 | // Random ranges affect each emitted particle: 35 | pos: [[-8,16], [-8,16], 0], 36 | vel: [0,0,0], 37 | emit_rate: [15,0], // emissions per second 38 | // Random ranges only calculated upon instantiation: 39 | emit_time: [0,500], 40 | emit_initial: 10, 41 | max_parts: Infinity, 42 | }, 43 | }, 44 | system_lifespan: 1000, 45 | }; 46 | -------------------------------------------------------------------------------- /src/client/shaders/test.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | precision mediump float; 3 | precision mediump int; 4 | 5 | varying lowp vec4 interp_color; 6 | varying highp vec2 interp_texcoord; 7 | uniform vec4 params; 8 | 9 | // Partially From: https://www.shadertoy.com/view/lsl3RH 10 | // Created by inigo quilez - iq/2013 11 | // License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. 12 | // See here for a tutorial on how to make this: http://www.iquilezles.org/www/articles/warp/warp.htm 13 | 14 | const mat2 m = mat2( 0.80, 0.60, -0.60, 0.80 ); 15 | 16 | float noise( in vec2 x ) 17 | { 18 | return sin(1.5*x.x)*sin(1.5*x.y); 19 | } 20 | 21 | float fbm4( vec2 p ) 22 | { 23 | float f = 0.0; 24 | f += 0.5000*noise( p ); p = m*p*2.02; 25 | f += 0.2500*noise( p ); p = m*p*2.03; 26 | f += 0.1250*noise( p ); p = m*p*2.01; 27 | f += 0.0625*noise( p ); 28 | return f/0.9375; 29 | } 30 | 31 | float fbm6( vec2 p ) 32 | { 33 | float f = 0.0; 34 | f += 0.500000*(0.5+0.5*noise( p )); p = m*p*2.02; 35 | f += 0.250000*(0.5+0.5*noise( p )); p = m*p*2.03; 36 | f += 0.125000*(0.5+0.5*noise( p )); p = m*p*2.01; 37 | f += 0.062500*(0.5+0.5*noise( p )); p = m*p*2.04; 38 | f += 0.031250*(0.5+0.5*noise( p )); p = m*p*2.01; 39 | f += 0.015625*(0.5+0.5*noise( p )); 40 | return f/0.96875; 41 | } 42 | 43 | 44 | float func( vec2 q ) 45 | { 46 | float iTime = params.w; 47 | float ql = length( q ); 48 | q.x += 0.05*sin(0.27*iTime+ql*4.1); 49 | q.y += 0.05*sin(0.23*iTime+ql*4.3); 50 | q *= 0.5; 51 | 52 | vec2 o = vec2(0.0); 53 | o.x = 0.5 + 0.5*fbm4( vec2(2.0*q ) ); 54 | o.y = 0.5 + 0.5*fbm4( vec2(2.0*q+vec2(5.2)) ); 55 | 56 | float ol = length( o ); 57 | o.x += 0.02*sin(0.12*iTime+ol)/ol; 58 | o.y += 0.02*sin(0.14*iTime+ol)/ol; 59 | 60 | vec2 n; 61 | n.x = fbm6( vec2(4.0*o+vec2(9.2)) ); 62 | n.y = fbm6( vec2(4.0*o+vec2(5.7)) ); 63 | 64 | vec2 p = 4.0*q + 4.0*n; 65 | 66 | float f = 0.5 + 0.5*fbm4( p ); 67 | 68 | f = mix( f, f*f*f*3.5, f*abs(n.x) ); 69 | 70 | float g = 0.5 + 0.5*sin(4.0*p.x)*sin(4.0*p.y); 71 | f *= 1.0-0.5*pow( g, 8.0 ); 72 | 73 | return f; 74 | } 75 | 76 | 77 | 78 | vec3 doMagic(vec2 p) 79 | { 80 | vec2 q = p*5.0; 81 | 82 | float f = func(q); 83 | 84 | vec3 col = mix(interp_color.rgb, params.rgb, f ); 85 | return col; 86 | } 87 | 88 | void main() 89 | { 90 | gl_FragColor = vec4( doMagic( interp_texcoord ), 1.0 ); 91 | } 92 | -------------------------------------------------------------------------------- /src/client/sounds/bg.ceol: -------------------------------------------------------------------------------- 1 | 3,0,0,0,90,12,4,1,45,0,3,68,0,256,4,5,1,0,3,12,29,1,0,0,33,1,1,0,36,1,2,0,41,1,3,0,36,1,4,0,33,1,5,0,29,1,6,0,33,1,7,0,36,1,8,0,41,1,9,0,36,1,10,0,33,1,11,0,0,5,1,0,3,12,29,1,0,0,34,1,1,0,38,1,2,0,41,1,3,0,38,1,4,0,34,1,5,0,29,1,6,0,34,1,7,0,38,1,8,0,41,1,9,0,38,1,10,0,34,1,11,0,0,5,1,0,3,12,28,1,0,0,31,1,1,0,36,1,2,0,40,1,3,0,36,1,4,0,31,1,5,0,28,1,6,0,31,1,7,0,36,1,8,0,40,1,9,0,36,1,10,0,33,1,11,0,0,5,1,0,3,12,31,1,0,0,34,1,1,0,36,1,2,0,40,1,3,0,36,1,4,0,34,1,5,0,31,1,6,0,34,1,7,0,36,1,8,0,40,1,9,0,36,1,10,0,34,1,11,0,0,8,0,8,0,-1,-1,-1,-1,-1,-1,-1,1,-1,-1,-1,-1,-1,-1,-1,2,-1,-1,-1,-1,-1,-1,-1,1,-1,-1,-1,-1,-1,-1,-1,3,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,-1,-1,-1,-1,-1,1,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,-1,-1,-1,-1,-1, -------------------------------------------------------------------------------- /src/client/sounds/bg.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/bg.mp3 -------------------------------------------------------------------------------- /src/client/sounds/button_click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/button_click.mp3 -------------------------------------------------------------------------------- /src/client/sounds/button_click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/button_click.wav -------------------------------------------------------------------------------- /src/client/sounds/down1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/down1.mp3 -------------------------------------------------------------------------------- /src/client/sounds/down1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/down1.wav -------------------------------------------------------------------------------- /src/client/sounds/down2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/down2.mp3 -------------------------------------------------------------------------------- /src/client/sounds/down2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/down2.wav -------------------------------------------------------------------------------- /src/client/sounds/down3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/down3.mp3 -------------------------------------------------------------------------------- /src/client/sounds/down3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/down3.wav -------------------------------------------------------------------------------- /src/client/sounds/fanfare.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/fanfare.mp3 -------------------------------------------------------------------------------- /src/client/sounds/fanfare.tg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/fanfare.tg -------------------------------------------------------------------------------- /src/client/sounds/fanfare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/fanfare.wav -------------------------------------------------------------------------------- /src/client/sounds/msg_err.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_err.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_err.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_err.wav -------------------------------------------------------------------------------- /src/client/sounds/msg_in.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_in.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_in.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_in.wav -------------------------------------------------------------------------------- /src/client/sounds/msg_out.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_out.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_out.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_out.wav -------------------------------------------------------------------------------- /src/client/sounds/msg_out_err.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_out_err.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_out_err.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/msg_out_err.wav -------------------------------------------------------------------------------- /src/client/sounds/rollover.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/rollover.mp3 -------------------------------------------------------------------------------- /src/client/sounds/rollover.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/rollover.wav -------------------------------------------------------------------------------- /src/client/sounds/up1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/up1.mp3 -------------------------------------------------------------------------------- /src/client/sounds/up1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/up1.wav -------------------------------------------------------------------------------- /src/client/sounds/up2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/up2.mp3 -------------------------------------------------------------------------------- /src/client/sounds/up2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/up2.wav -------------------------------------------------------------------------------- /src/client/sounds/up3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/up3.mp3 -------------------------------------------------------------------------------- /src/client/sounds/up3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/up3.wav -------------------------------------------------------------------------------- /src/client/sounds/upchord1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/upchord1.mp3 -------------------------------------------------------------------------------- /src/client/sounds/upchord1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/upchord1.wav -------------------------------------------------------------------------------- /src/client/sounds/upchord2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/upchord2.mp3 -------------------------------------------------------------------------------- /src/client/sounds/upchord2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/upchord2.wav -------------------------------------------------------------------------------- /src/client/sounds/upchord3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/upchord3.mp3 -------------------------------------------------------------------------------- /src/client/sounds/upchord3.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/upchord3.wav -------------------------------------------------------------------------------- /src/client/sounds/user_join.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/user_join.mp3 -------------------------------------------------------------------------------- /src/client/sounds/user_join.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/user_join.wav -------------------------------------------------------------------------------- /src/client/sounds/user_leave.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/user_leave.mp3 -------------------------------------------------------------------------------- /src/client/sounds/user_leave.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/client/sounds/user_leave.wav -------------------------------------------------------------------------------- /src/client/worker.js: -------------------------------------------------------------------------------- 1 | 2 | const worker = require('glov/client/worker_thread.js'); 3 | 4 | worker.addHandler('test', function () { 5 | console.log('Worker Test!'); 6 | }); 7 | -------------------------------------------------------------------------------- /src/client/worker_deps.js: -------------------------------------------------------------------------------- 1 | /* globals deps */ 2 | require('../glov/client/require.js'); 3 | 4 | deps.assert = require('assert'); 5 | -------------------------------------------------------------------------------- /src/glov/client/animation.ts: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | /* 5 | example usage: 6 | 7 | // trigger 8 | let alpha = 0; 9 | let anim = createAnimationSequencer(); 10 | let t = anim.add(0, 300, (progress) => alpha = progress); 11 | t = anim.add(t + 1000, 300, (progress) => alpha = 1 - progress); 12 | 13 | // tick 14 | if (anim) { 15 | if (!anim.update(dt)) 16 | anim = null; 17 | else 18 | glov_input.eatAllInput(); 19 | drawSomething(alpha); 20 | } 21 | */ 22 | 23 | type AnimationFunc = (progress: number) => void; 24 | export type AnimationSequencer = AnimationSequencerImpl; 25 | class AnimationSequencerImpl { 26 | time = 0; 27 | fns: { 28 | done: boolean; 29 | fn: AnimationFunc; 30 | start: number; 31 | end: number; 32 | duration: number; 33 | }[]; 34 | constructor() { 35 | this.fns = []; 36 | } 37 | 38 | // Calls fn(progress) with progress >0 and <= 1; guaranteed call with === 1 39 | add(start: number, duration: number, fn: AnimationFunc): number { 40 | let end = start + duration; 41 | this.fns.push({ 42 | done: false, 43 | fn, 44 | start, 45 | end, 46 | duration, 47 | }); 48 | return end; 49 | } 50 | 51 | update(dt: number): boolean { 52 | this.time += dt; 53 | let any_left = false; 54 | for (let ii = 0; ii < this.fns.length; ++ii) { 55 | let e = this.fns[ii]; 56 | if (e.start < this.time && this.time < e.end) { 57 | any_left = true; 58 | e.fn((this.time - e.start) / e.duration); 59 | } else if (this.time >= e.end && !e.done) { 60 | e.fn(1); 61 | e.done = true; 62 | } else if (e.start >= this.time) { 63 | any_left = true; 64 | } 65 | } 66 | return any_left; 67 | } 68 | } 69 | 70 | export function animationSequencerCreate(): AnimationSequencer { 71 | return new AnimationSequencerImpl(); 72 | } 73 | 74 | exports.createAnimationSequencer = animationSequencerCreate; 75 | exports.create = animationSequencerCreate; 76 | -------------------------------------------------------------------------------- /src/glov/client/auto_reset.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 2 | export const autoReset = autoResetSkippedFrames; 3 | 4 | import * as engine from './engine'; 5 | import type { TSMap } from 'glov/common/types'; 6 | 7 | let auto_reset_data: TSMap = Object.create(null); 8 | export function autoResetSkippedFrames(key: string): boolean { 9 | let last_value: number | undefined = auto_reset_data[key]; 10 | auto_reset_data[key] = engine.frame_index; 11 | return !(last_value! >= engine.frame_index - 1); 12 | } 13 | 14 | export function autoResetEachFrame(key: string): boolean { 15 | let last_value: number | undefined = auto_reset_data[key]; 16 | auto_reset_data[key] = engine.frame_index; 17 | return (last_value !== engine.frame_index); 18 | } 19 | -------------------------------------------------------------------------------- /src/glov/client/browser.js: -------------------------------------------------------------------------------- 1 | let ua = window.navigator.userAgent; 2 | export let is_mac_osx = ua.match(/Mac OS X/); 3 | export let is_ios = !window.MSStream && ua.match(/iPad|iPhone|iPod/); 4 | export let is_windows_phone = ua.match(/windows phone/i); 5 | export let is_android = !is_windows_phone && ua.match(/android/i); 6 | export let is_webkit = ua.match(/WebKit/i); 7 | export let is_ios_safari = is_ios && is_webkit && !ua.match(/CriOS/i); 8 | export let is_firefox = ua.match(/Firefox/i); 9 | export let is_itch_app = String(window.location.protocol).indexOf('itch') !== -1; // Note: itch.io APP, not web site 10 | 11 | export let is_discrete_gpu = false; 12 | 13 | function init() { 14 | try { 15 | let canvas = document.createElement('canvas'); 16 | canvas.width = 4; 17 | canvas.height = 4; 18 | let gltest = canvas.getContext('webgl'); 19 | if (gltest) { 20 | let debug_info = gltest.getExtension('WEBGL_debug_renderer_info'); 21 | if (debug_info) { 22 | let renderer_unmasked = gltest.getParameter(debug_info.UNMASKED_RENDERER_WEBGL); 23 | is_discrete_gpu = Boolean(renderer_unmasked && ( 24 | renderer_unmasked.match(/nvidia|radeon/i) || 25 | renderer_unmasked.match(/apple gpu/i) && is_mac_osx && !is_ios 26 | )); 27 | } 28 | } 29 | } catch (e) { 30 | // ignored 31 | } 32 | } 33 | init(); 34 | -------------------------------------------------------------------------------- /src/glov/client/client_config.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { 3 | PlatformID, 4 | platformIsValid, 5 | platformOverrideParameter, 6 | platformParameter, 7 | } from 'glov/common/platform'; 8 | 9 | // Platform 10 | assert(platformIsValid(window.conf_platform)); 11 | export const PLATFORM = window.conf_platform as PlatformID; 12 | 13 | let override_platform = PLATFORM; 14 | export function platformOverrideID(id: PlatformID): void { 15 | override_platform = id; 16 | } 17 | export function platformGetID(): PlatformID { 18 | return override_platform; 19 | } 20 | 21 | const platform_devmode = platformParameter(PLATFORM, 'devmode'); 22 | export const MODE_DEVELOPMENT = platform_devmode === 'on' || platform_devmode === 'auto' && 23 | Boolean(String(document.location).match(/^https?:\/\/localhost/)); 24 | export const MODE_PRODUCTION = !MODE_DEVELOPMENT; 25 | 26 | // Abilities 27 | export function getAbilityReload(): boolean { 28 | return platformParameter(platformGetID(), 'reload'); 29 | } 30 | export function setAbilityReload(value: boolean): void { 31 | platformOverrideParameter('reload', platformParameter(platformGetID(), 'reload') && value); 32 | } 33 | 34 | export function getAbilityReloadUpdates(): boolean { 35 | return platformParameter(platformGetID(), 'reload_updates'); 36 | } 37 | export function setAbilityReloadUpdates(value: boolean): void { 38 | platformOverrideParameter('reload_updates', platformParameter(platformGetID(), 'reload_updates') && value); 39 | } 40 | 41 | let ability_chat = true; 42 | export function getAbilityChat(): boolean { 43 | return ability_chat; 44 | } 45 | export function setAbilityChat(value: boolean): void { 46 | ability_chat = value; 47 | } 48 | -------------------------------------------------------------------------------- /src/glov/client/draw_list.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | const assert = require('assert'); 5 | const { dynGeomDrawAlpha, dynGeomDrawOpaque } = require('./dyn_geom.js'); 6 | const engine = require('./engine.js'); 7 | const { mat_vp } = engine; 8 | 9 | let list = []; 10 | 11 | // obj must have .mat and .drawAlpha() and optionally .sort_bias 12 | export function alphaQueue(obj) { 13 | //let transformed_pos = vec4(); 14 | //vec4.transformMat4(transformed_pos, obj.mat.slice(12), mat_vp); 15 | //let sort_z = transformed_pos[2]; 16 | 17 | let sort_z = mat_vp[2] * obj.mat[12] + 18 | mat_vp[6] * obj.mat[13] + 19 | mat_vp[10] * obj.mat[14] + 20 | mat_vp[14]; // * obj.mat[15]; should be 1? 21 | 22 | list.push([sort_z + (obj.sort_bias || 0), obj]); 23 | } 24 | 25 | function cmpAlpha(a, b) { 26 | return b[0] - a[0]; 27 | } 28 | 29 | export function alphaDraw() { 30 | gl.enable(gl.BLEND); 31 | gl.depthMask(false); 32 | 33 | list.sort(cmpAlpha); 34 | for (let ii = 0; ii < list.length; ++ii) { 35 | list[ii][1].drawAlpha(list[ii][0]); 36 | } 37 | list.length = 0; 38 | dynGeomDrawAlpha(); // TODO: merge sort with `list` 39 | 40 | gl.depthMask(true); 41 | gl.disable(gl.BLEND); 42 | } 43 | 44 | export function alphaDrawListSize() { 45 | return list.length; 46 | } 47 | 48 | let list_stack = null; 49 | export function alphaListPush() { 50 | assert(!list_stack); 51 | list_stack = list; 52 | list = []; 53 | } 54 | export function alphaListPop() { 55 | assert(!list.length); // should have been drawn 56 | assert(list_stack); 57 | list = list_stack; 58 | list_stack = null; 59 | } 60 | 61 | let opaque_list = []; 62 | export function opaqueQueue(fn) { 63 | opaque_list.push(fn); 64 | } 65 | 66 | export function opaqueDraw() { 67 | for (let ii = 0; ii < opaque_list.length; ++ii) { 68 | opaque_list[ii](); 69 | } 70 | opaque_list.length = 0; 71 | dynGeomDrawOpaque(); 72 | } 73 | -------------------------------------------------------------------------------- /src/glov/client/edit_box.d.ts: -------------------------------------------------------------------------------- 1 | import type { FontStyle } from './font'; 2 | import type { ROVec4 } from 'glov/common/vmath'; 3 | 4 | export type EditBoxResult = null | 'submit' | 'cancel'; 5 | 6 | export interface EditBoxOptsAll { 7 | key: string; 8 | x: number; 9 | y: number; 10 | z: number; 11 | w: number; 12 | type: 'text' | 'number' | 'password' | 'email'; 13 | font_height: number; 14 | text: string | number; 15 | placeholder: string; 16 | max_len: number; 17 | zindex: null | number; 18 | uppercase: boolean; 19 | initial_focus: boolean; 20 | // internal state: onetime_focus: boolean; 21 | focus_steal: boolean; 22 | auto_unfocus: boolean; 23 | initial_select: boolean; 24 | spellcheck: boolean; 25 | esc_clears: boolean; 26 | esc_unfocuses: boolean; 27 | multiline: number; 28 | enforce_multiline: boolean; 29 | autocomplete: boolean; 30 | suppress_up_down: boolean; 31 | // custom_nav: Partial>; 32 | canvas_render: null | { 33 | // if set, will do custom canvas rendering instead of DOM rendering 34 | // requires a fixed-width font and near-perfectly aligned font rendering (tweak setDOMFontPixelScale) 35 | char_width: number; 36 | char_height: number; 37 | color_selection: ROVec4; 38 | color_caret: ROVec4; 39 | style_text: FontStyle; 40 | }; 41 | } 42 | 43 | export type EditBoxOpts = Partial; 44 | 45 | export interface EditBox extends Readonly { 46 | run(params?: EditBoxOpts): EditBoxResult; 47 | getText(): string; 48 | setText(new_text: string | number): void; 49 | isFocused(): boolean; 50 | hadOverflow(): boolean; 51 | getSelection(): [[number, number], [number, number]]; // [column, row], [column, row] 52 | 53 | readonly SUBMIT: 'submit'; 54 | readonly CANCEL: 'cancel'; 55 | } 56 | 57 | export function editBoxCreate(params?: EditBoxOpts): EditBox; 58 | 59 | // Pure immediate-mode API 60 | export function editBox(params: EditBoxOpts, current: T): { 61 | result: EditBoxResult; 62 | text: T; 63 | edit_box: EditBox; 64 | }; 65 | -------------------------------------------------------------------------------- /src/glov/client/external_user_info.ts: -------------------------------------------------------------------------------- 1 | export interface ExternalUserInfo { 2 | external_id: string; // '' if unspecified 3 | name?: string; 4 | profile_picture_url?: string; 5 | email?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/glov/client/filewatch.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | let by_ext = {}; 4 | let by_match = []; 5 | 6 | // cb(filename) 7 | export function filewatchOn(ext_or_search, cb) { 8 | if (ext_or_search[0] === '.') { 9 | assert(!by_ext[ext_or_search]); 10 | by_ext[ext_or_search] = cb; 11 | } else { 12 | by_match.push([ext_or_search, cb]); 13 | } 14 | } 15 | 16 | let message_cb; 17 | // cb(message) 18 | export function filewatchMessageHandler(cb) { 19 | message_cb = cb; 20 | } 21 | 22 | function onFileChange(filename) { 23 | console.log(`FileWatch change: ${filename}`); 24 | let ext_idx = filename.lastIndexOf('.'); 25 | let did_reload = false; 26 | if (ext_idx !== -1) { 27 | let ext = filename.slice(ext_idx); 28 | if (by_ext[ext]) { 29 | if (by_ext[ext](filename) !== false) { 30 | did_reload = true; 31 | } 32 | } 33 | } 34 | for (let ii = 0; ii < by_match.length; ++ii) { 35 | if (filename.match(by_match[ii][0])) { 36 | if (by_match[ii][1](filename) !== false) { 37 | did_reload = true; 38 | } 39 | } 40 | } 41 | if (message_cb && did_reload) { 42 | message_cb(`Reloading: ${filename}`); 43 | } 44 | } 45 | 46 | export function filewatchTriggerChange(filename) { 47 | onFileChange(filename); 48 | } 49 | 50 | export function filewatchStartup(client) { 51 | client.onMsg('filewatch', onFileChange); 52 | } 53 | -------------------------------------------------------------------------------- /src/glov/client/fscreen.js: -------------------------------------------------------------------------------- 1 | // From 'fscreen' module, https://github.com/rafrex/fscreen, MIT Licensed 2 | 3 | // Changes: 4 | // Fixed depending on Object.keys() ordering 5 | // Standardized exports with more meaningful names 6 | // Added ability to disable 7 | 8 | const { eatPossiblePromise } = require('glov/common/util.js'); 9 | 10 | const key = { 11 | fullscreenEnabled: 0, 12 | fullscreenElement: 1, 13 | requestFullscreen: 2, 14 | exitFullscreen: 3, 15 | // fullscreenchange: 4, 16 | // fullscreenerror: 5, 17 | }; 18 | 19 | const html5 = [ 20 | 'fullscreenEnabled', 21 | 'fullscreenElement', 22 | 'requestFullscreen', 23 | 'exitFullscreen', 24 | // 'fullscreenchange', 25 | // 'fullscreenerror', 26 | ]; 27 | 28 | const webkit = [ 29 | 'webkitFullscreenEnabled', 30 | 'webkitFullscreenElement', 31 | 'webkitRequestFullscreen', 32 | 'webkitExitFullscreen', 33 | // 'webkitfullscreenchange', 34 | // 'webkitfullscreenerror', 35 | ]; 36 | 37 | const moz = [ 38 | 'mozFullScreenEnabled', 39 | 'mozFullScreenElement', 40 | 'mozRequestFullScreen', 41 | 'mozCancelFullScreen', 42 | // 'mozfullscreenchange', 43 | // 'mozfullscreenerror', 44 | ]; 45 | 46 | const ms = [ 47 | 'msFullscreenEnabled', 48 | 'msFullscreenElement', 49 | 'msRequestFullscreen', 50 | 'msExitFullscreen', 51 | // 'MSFullscreenChange', 52 | // 'MSFullscreenError', 53 | ]; 54 | 55 | // const document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {}; 56 | 57 | const vendor = ( 58 | (html5[0] in document && html5) || 59 | (webkit[0] in document && webkit) || 60 | (moz[0] in document && moz) || 61 | (ms[0] in document && ms) || 62 | [] 63 | ); 64 | 65 | export function fscreenEnter() { 66 | let element = document.documentElement; 67 | eatPossiblePromise(element[vendor[key.requestFullscreen]]()); 68 | } 69 | export function fscreenExit() { 70 | eatPossiblePromise(document[vendor[key.exitFullscreen]]()); 71 | } 72 | // export function addEventListener(type, handler, options) { 73 | // document.addEventListener(vendor[key[type]], handler, options); 74 | // } 75 | // export function removeEventListener(type, handler, options) { 76 | // document.removeEventListener(vendor[key[type]], handler, options); 77 | // } 78 | let disabled = false; 79 | export function fscreenAvailable() { 80 | return !disabled && Boolean(document[vendor[key.fullscreenEnabled]]); 81 | } 82 | export function fscreenDisable() { 83 | disabled = true; 84 | } 85 | export function fscreenActive() { 86 | return document[vendor[key.fullscreenElement]]; 87 | } 88 | // set onfullscreenchange(handler) { return document[`on${vendor[key.fullscreenchange]}`.toLowerCase()] = handler; }, 89 | // set onfullscreenerror(handler) { return document[`on${vendor[key.fullscreenerror]}`.toLowerCase()] = handler; }, 90 | -------------------------------------------------------------------------------- /src/glov/client/geom_types.ts: -------------------------------------------------------------------------------- 1 | export type Box = { 2 | x: number; 3 | y: number; 4 | w: number; 5 | h: number; 6 | }; 7 | 8 | export type Point2D = { 9 | x: number; 10 | y: number; 11 | }; 12 | -------------------------------------------------------------------------------- /src/glov/client/glb/decode-utf8.js: -------------------------------------------------------------------------------- 1 | /* eslint no-bitwise:off */ 2 | // From https://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript 3 | 4 | let charCache = new Array(128); // Preallocate the cache for the common single byte chars 5 | let charFromCodePt = String.fromCodePoint || String.fromCharCode; 6 | let result = []; 7 | export function decode(array) { 8 | let codePt; 9 | let byte1; 10 | let buffLen = array.length; 11 | 12 | result.length = 0; 13 | 14 | for (let i = 0; i < buffLen;) { 15 | byte1 = array[i++]; 16 | 17 | if (byte1 <= 0x7F) { 18 | codePt = byte1; 19 | } else if (byte1 <= 0xDF) { 20 | codePt = ((byte1 & 0x1F) << 6) | (array[i++] & 0x3F); 21 | } else if (byte1 <= 0xEF) { 22 | codePt = ((byte1 & 0x0F) << 12) | ((array[i++] & 0x3F) << 6) | (array[i++] & 0x3F); 23 | } else if (String.fromCodePoint) { 24 | codePt = ((byte1 & 0x07) << 18) | ((array[i++] & 0x3F) << 12) | ((array[i++] & 0x3F) << 6) | (array[i++] & 0x3F); 25 | } else { 26 | codePt = 63; // Cannot convert four byte code points, so use "?" instead 27 | i += 3; 28 | } 29 | 30 | result.push(charCache[codePt] || (charCache[codePt] = charFromCodePt(codePt))); 31 | } 32 | 33 | return result.join(''); 34 | } 35 | -------------------------------------------------------------------------------- /src/glov/client/glb/gltf-type-utils.js: -------------------------------------------------------------------------------- 1 | // Derived from (MIT Licensed) https://github.com/uber-web/loaders.gl/tree/master/modules/gltf 2 | 3 | const TYPES = ['SCALAR', 'VEC2', 'VEC3', 'VEC4']; 4 | 5 | export function getAccessorTypeFromSize(size) { 6 | const type = TYPES[size - 1]; 7 | return type || TYPES[0]; 8 | } 9 | 10 | // glTF ACCESSOR CONSTANTS 11 | 12 | export const ATTRIBUTE_TYPE_TO_COMPONENTS = { 13 | SCALAR: 1, 14 | VEC2: 2, 15 | VEC3: 3, 16 | VEC4: 4, 17 | MAT2: 4, 18 | MAT3: 9, 19 | MAT4: 16 20 | }; 21 | 22 | export const ATTRIBUTE_COMPONENT_TYPE_TO_BYTE_SIZE = { 23 | 5120: 1, 24 | 5121: 1, 25 | 5122: 2, 26 | 5123: 2, 27 | 5125: 4, 28 | 5126: 4 29 | }; 30 | 31 | export const ATTRIBUTE_COMPONENT_TYPE_TO_ARRAY = { 32 | 5120: Int8Array, 33 | 5121: Uint8Array, 34 | 5122: Int16Array, 35 | 5123: Uint16Array, 36 | 5125: Uint32Array, 37 | 5126: Float32Array 38 | }; 39 | -------------------------------------------------------------------------------- /src/glov/client/glb/unpack-binary-json.js: -------------------------------------------------------------------------------- 1 | // Derived from (MIT Licensed) https://github.com/uber-web/loaders.gl/tree/master/modules/gltf 2 | 3 | function parseJSONPointer(value) { 4 | if (typeof value === 'string') { 5 | // Remove escape character 6 | if (value.indexOf('##/') === 0) { 7 | return value.slice(1); 8 | } 9 | 10 | let matches = value.match(/#\/([a-z]+)\/([0-9]+)/); 11 | if (matches) { 12 | const index = parseInt(matches[2], 10); 13 | return [matches[1], index]; 14 | } 15 | 16 | // Legacy: `$$$i` 17 | matches = value.match(/\$\$\$([0-9]+)/); 18 | if (matches) { 19 | const index = parseInt(matches[1], 10); 20 | return ['accessors', index]; 21 | } 22 | } 23 | 24 | return null; 25 | } 26 | 27 | function decodeJSONPointer(object, buffers) { 28 | const pointer = parseJSONPointer(object); 29 | if (pointer) { 30 | const field = pointer[0]; 31 | const index = pointer[1]; 32 | const buffer = buffers[field] && buffers[field][index]; 33 | if (buffer) { 34 | return buffer; 35 | } 36 | console.error(`Invalid JSON pointer ${object}: #/${field}/${index}`); 37 | } 38 | return null; 39 | } 40 | 41 | // Recursively unpacks objects, replacing "JSON pointers" with typed arrays 42 | function unpackJsonArraysRecursive(json, topJson, buffers, options = {}) { 43 | const object = json; 44 | 45 | const buffer = decodeJSONPointer(object, buffers); 46 | if (buffer) { 47 | return buffer; 48 | } 49 | 50 | // Copy array 51 | if (Array.isArray(object)) { 52 | return object.map((element) => unpackJsonArraysRecursive(element, topJson, buffers, options)); 53 | } 54 | 55 | // Copy object 56 | if (object !== null && typeof object === 'object') { 57 | const newObject = {}; 58 | for (const key in object) { 59 | newObject[key] = unpackJsonArraysRecursive(object[key], topJson, buffers, options); 60 | } 61 | return newObject; 62 | } 63 | 64 | return object; 65 | } 66 | 67 | export function unpackBinaryJson(json, buffers, options = {}) { 68 | return unpackJsonArraysRecursive(json, json, buffers, options); 69 | } 70 | -------------------------------------------------------------------------------- /src/glov/client/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | declare module 'glov/client/global' { 3 | global { 4 | interface Window { 5 | // GLOV injected variables 6 | conf_platform?: string; 7 | conf_env?: string; 8 | 9 | // GLOV bootstrap 10 | debugmsg: (msg: string, clear: boolean) => void; 11 | Z: Record; 12 | } 13 | 14 | const BUILD_TIMESTAMP: string; 15 | const __funcname: string; // eslint-disable-line no-underscore-dangle 16 | 17 | // GLOV ui.js 18 | const Z: Record; 19 | // GL context 20 | let gl: WebGLRenderingContext | WebGL2RenderingContext; 21 | // GLOV profiler 22 | function profilerStart(name: string): void; 23 | function profilerStop(name?: string): void; 24 | function profilerStopStart(name: string): void; 25 | function profilerStartFunc(): void; 26 | function profilerStopFunc(): void; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/glov/client/hsv.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | // From libGlov, MIT Licensed, (c) 2005-2017 Jimb Esser, various authors 5 | 6 | const { max, min, floor } = Math; 7 | 8 | // out([0,360), [0,1], [0,1]) 9 | // rgb [0,1](3) 10 | exports.rgbToHSV = function rgbToHSV(out, rgb) { 11 | let r = rgb[0]; 12 | let g = rgb[1]; 13 | let b = rgb[2]; 14 | let mn = min(r, g, b); 15 | let mx = max(r, g, b); 16 | out[2] = mx; // v 17 | let delta = mx - mn; 18 | if (delta !== 0) { // mx == 0 -> delta == 0 19 | out[1] = delta / mx; // s 20 | } else { 21 | // r = g = b = 0 // s = 0, v is undefined 22 | out[1] = 0; 23 | out[0] = 0; 24 | return; 25 | } 26 | if (r === mx) { 27 | out[0] = (g - b) / delta; // between yellow & magenta 28 | } else if (g === mx) { 29 | out[0] = 2 + (b - r) / delta; // between cyan & yellow 30 | } else { 31 | out[0] = 4 + (r - g) / delta; // between magenta & cyan 32 | } 33 | out[0] *= 60; // degrees 34 | if (out[0] < 0) { 35 | out[0] += 360; 36 | } 37 | }; 38 | 39 | // out [0,1](3) 40 | // hue [0,360) 41 | // s [0,1] 42 | // v [0,1] 43 | exports.hsvToRGB = function hsvToRGB(out, h, s, v) { 44 | if (s === 0) { 45 | // achromatic (grey) 46 | out[0] = out[1] = out[2] = v; 47 | return out; 48 | } 49 | h /= 60; // sector 0 to 5 50 | if (h>=6) { 51 | h-=6; 52 | } 53 | let i = floor(h); 54 | let f = h - i; // factorial part of h 55 | let p = v * (1 - s); 56 | let q = v * (1 - s * f); 57 | let t = v * (1 - s * (1 - f)); 58 | switch (i) { 59 | case 0: 60 | out[0] = v; 61 | out[1] = t; 62 | out[2] = p; 63 | break; 64 | case 1: 65 | out[0] = q; 66 | out[1] = v; 67 | out[2] = p; 68 | break; 69 | case 2: 70 | out[0] = p; 71 | out[1] = v; 72 | out[2] = t; 73 | break; 74 | case 3: 75 | out[0] = p; 76 | out[1] = q; 77 | out[2] = v; 78 | break; 79 | case 4: 80 | out[0] = t; 81 | out[1] = p; 82 | out[2] = v; 83 | break; 84 | default: // case 5: 85 | out[0] = v; 86 | out[1] = p; 87 | out[2] = q; 88 | break; 89 | } 90 | return out; 91 | }; 92 | -------------------------------------------------------------------------------- /src/glov/client/in_event.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | // System for registering callbacks to run in event handlers on the next frame 5 | // Used to get around restrictions on APIs like pointer lock, fullscreen, or 6 | // screen orientation. 7 | 8 | const assert = require('assert'); 9 | 10 | let cbs = {}; 11 | export function topOfFrame() { 12 | cbs = {}; 13 | } 14 | 15 | export function on(type, code_or_pos, cb) { 16 | let list = cbs[type] = cbs[type] || []; 17 | if (typeof code_or_pos === 'number') { 18 | list[code_or_pos] = cb; 19 | } else { 20 | list.push([code_or_pos, cb]); 21 | } 22 | } 23 | 24 | export function handle(type, event) { 25 | let list = cbs[type]; 26 | if (!list) { 27 | return; 28 | } 29 | switch (type) { 30 | case 'keydown': 31 | case 'keyup': 32 | if (list[event.keyCode]) { 33 | list[event.keyCode](type, event); 34 | } 35 | break; 36 | case 'mouseup': 37 | case 'mousedown': { 38 | let x = event.pageX; 39 | let y = event.pageY; 40 | let button = event.button; 41 | for (let ii = 0; ii < list.length; ++ii) { 42 | let elem = list[ii]; 43 | let pos = elem[0]; 44 | if (x >= pos.x && x < pos.x + pos.w && 45 | y >= pos.y && y < pos.y + pos.h && 46 | (pos.button < 0 || pos.button === button) 47 | ) { 48 | elem[1](type, event); 49 | break; 50 | } 51 | } 52 | } break; 53 | default: 54 | assert(false); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/glov/client/input_constants.ts: -------------------------------------------------------------------------------- 1 | export type ButtonIndex = 0 | 1 | 2 | -1 | -2; 2 | 3 | export const BUTTON_LEFT = 0; 4 | export const BUTTON_MIDDLE = 1; 5 | export const BUTTON_RIGHT = 2; 6 | export const BUTTON_ANY = -2; 7 | export const BUTTON_POINTERLOCK = -1; 8 | export const ANY = BUTTON_ANY; 9 | export const POINTERLOCK = BUTTON_POINTERLOCK; 10 | -------------------------------------------------------------------------------- /src/glov/client/localization.ts: -------------------------------------------------------------------------------- 1 | export interface LocalizableString { 2 | toLocalString(): string; 3 | } 4 | 5 | export function getStringFromLocalizable(s: string | LocalizableString): string { 6 | return s && (s as LocalizableString).toLocalString ? 7 | (s as LocalizableString).toLocalString() : 8 | (s as string); 9 | } 10 | 11 | export function getStringIfLocalizable(s: T | LocalizableString): T | string { 12 | return s && (s as LocalizableString).toLocalString ? 13 | (s as LocalizableString).toLocalString() : 14 | (s as T); 15 | } 16 | -------------------------------------------------------------------------------- /src/glov/client/mat2d.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | // Some code from https://github.com/toji/gl-matrix/blob/master/src/mat2d.js 4 | 5 | const { cos, sin } = Math; 6 | 7 | // Last column is always 0,0,1 8 | export function mat2d() { 9 | let r = new Float32Array(6); 10 | r[0] = r[3] = 1; 11 | return r; 12 | } 13 | 14 | export const identity_mat2d = mat2d(); 15 | 16 | export function m2translate(out, a, v) { 17 | if (a !== out) { 18 | out[0] = a[0]; 19 | out[1] = a[1]; 20 | out[2] = a[2]; 21 | out[3] = a[3]; 22 | } 23 | out[4] = a[4] + v[0]; 24 | out[5] = a[5] + v[1]; 25 | return out; 26 | } 27 | 28 | export function m2mul(out, a, b) { 29 | let a0 = a[0]; 30 | let a1 = a[1]; 31 | let a2 = a[2]; 32 | let a3 = a[3]; 33 | let a4 = a[4]; 34 | let a5 = a[5]; 35 | let b0 = b[0]; 36 | let b1 = b[1]; 37 | let b2 = b[2]; 38 | let b3 = b[3]; 39 | let b4 = b[4]; 40 | let b5 = b[5]; 41 | out[0] = a0 * b0 + a2 * b1; 42 | out[1] = a1 * b0 + a3 * b1; 43 | out[2] = a0 * b2 + a2 * b3; 44 | out[3] = a1 * b2 + a3 * b3; 45 | out[4] = a0 * b4 + a2 * b5 + a4; 46 | out[5] = a1 * b4 + a3 * b5 + a5; 47 | return out; 48 | } 49 | 50 | /** 51 | * Rotates a mat2d by the given angle 52 | * 53 | * @param {mat2d} out the receiving matrix 54 | * @param {mat2d} a the matrix to rotate 55 | * @param {Number} rad the angle to rotate the matrix by 56 | * @returns {mat2d} out 57 | */ 58 | export function m2rot(out, a, rad) { 59 | let a0 = a[0]; 60 | let a1 = a[1]; 61 | let a2 = a[2]; 62 | let a3 = a[3]; 63 | let a4 = a[4]; 64 | let a5 = a[5]; 65 | let s = sin(rad); 66 | let c = cos(rad); 67 | out[0] = a0 * c + a2 * s; 68 | out[1] = a1 * c + a3 * s; 69 | out[2] = a0 * -s + a2 * c; 70 | out[3] = a1 * -s + a3 * c; 71 | out[4] = a4; 72 | out[5] = a5; 73 | return out; 74 | } 75 | 76 | export function m2scale(out, a, v) { 77 | let a0 = a[0]; 78 | let a1 = a[1]; 79 | let a2 = a[2]; 80 | let a3 = a[3]; 81 | let a4 = a[4]; 82 | let a5 = a[5]; 83 | let v0 = v[0]; 84 | let v1 = v[1]; 85 | out[0] = a0 * v0; 86 | out[1] = a1 * v0; 87 | out[2] = a2 * v1; 88 | out[3] = a3 * v1; 89 | out[4] = a4; 90 | out[5] = a5; 91 | return out; 92 | } 93 | 94 | export function m2v2transform(out, a, m) { 95 | let x = a[0]; 96 | let y = a[1]; 97 | out[0] = m[0] * x + m[2] * y + m[4]; 98 | out[1] = m[1] * x + m[3] * y + m[5]; 99 | return out; 100 | } 101 | -------------------------------------------------------------------------------- /src/glov/client/mat43.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | // Some code from Turbulenz: Copyright (c) 2012-2013 Turbulenz Limited 4 | // Released under MIT License: https://opensource.org/licenses/MIT 5 | 6 | export function mat43() { 7 | let r = new Float32Array(12); 8 | r[0] = r[4] = r[8] = 1; 9 | return r; 10 | } 11 | 12 | export function m43identity(out) { 13 | out[0] = 1; 14 | out[1] = 0; 15 | out[2] = 0; 16 | out[3] = 0; 17 | out[4] = 1; 18 | out[5] = 0; 19 | out[6] = 0; 20 | out[7] = 0; 21 | out[8] = 1; 22 | out[9] = 0; 23 | out[10] = 0; 24 | out[11] = 0; 25 | } 26 | 27 | export function m43mul(out, a, b) { 28 | let a0 = a[0]; 29 | let a1 = a[1]; 30 | let a2 = a[2]; 31 | let a3 = a[3]; 32 | let a4 = a[4]; 33 | let a5 = a[5]; 34 | let a6 = a[6]; 35 | let a7 = a[7]; 36 | let a8 = a[8]; 37 | let a9 = a[9]; 38 | let a10 = a[10]; 39 | let a11 = a[11]; 40 | 41 | let b0 = b[0]; 42 | let b1 = b[1]; 43 | let b2 = b[2]; 44 | let b3 = b[3]; 45 | let b4 = b[4]; 46 | let b5 = b[5]; 47 | let b6 = b[6]; 48 | let b7 = b[7]; 49 | let b8 = b[8]; 50 | 51 | out[0] = (b0 * a0 + b3 * a1 + b6 * a2); 52 | out[1] = (b1 * a0 + b4 * a1 + b7 * a2); 53 | out[2] = (b2 * a0 + b5 * a1 + b8 * a2); 54 | out[3] = (b0 * a3 + b3 * a4 + b6 * a5); 55 | out[4] = (b1 * a3 + b4 * a4 + b7 * a5); 56 | out[5] = (b2 * a3 + b5 * a4 + b8 * a5); 57 | out[6] = (b0 * a6 + b3 * a7 + b6 * a8); 58 | out[7] = (b1 * a6 + b4 * a7 + b7 * a8); 59 | out[8] = (b2 * a6 + b5 * a7 + b8 * a8); 60 | out[9] = (b0 * a9 + b3 * a10 + b6 * a11 + b[9]); 61 | out[10] = (b1 * a9 + b4 * a10 + b7 * a11 + b[10]); 62 | out[11] = (b2 * a9 + b5 * a10 + b8 * a11 + b[11]); 63 | 64 | return out; 65 | } 66 | -------------------------------------------------------------------------------- /src/glov/client/mat4ScaleRotateTranslate.js: -------------------------------------------------------------------------------- 1 | // Derived from https://github.com/toji/gl-matrix/blob/master/src/mat4.js 2 | // Under MIT License 3 | 4 | module.exports = function (out, uniform_scale, quat, pos) { 5 | // Quaternion math 6 | let x = quat[0]; 7 | let y = quat[1]; 8 | let z = quat[2]; 9 | let w = quat[3]; 10 | let x2 = x + x; 11 | let y2 = y + y; 12 | let z2 = z + z; 13 | 14 | let xx = x * x2; 15 | let xy = x * y2; 16 | let xz = x * z2; 17 | let yy = y * y2; 18 | let yz = y * z2; 19 | let zz = z * z2; 20 | let wx = w * x2; 21 | let wy = w * y2; 22 | let wz = w * z2; 23 | 24 | out[0] = (1 - (yy + zz)) * uniform_scale; 25 | out[1] = (xy + wz) * uniform_scale; 26 | out[2] = (xz - wy) * uniform_scale; 27 | out[3] = 0; 28 | out[4] = (xy - wz) * uniform_scale; 29 | out[5] = (1 - (xx + zz)) * uniform_scale; 30 | out[6] = (yz + wx) * uniform_scale; 31 | out[7] = 0; 32 | out[8] = (xz + wy) * uniform_scale; 33 | out[9] = (yz - wx) * uniform_scale; 34 | out[10] = (1 - (xx + yy)) * uniform_scale; 35 | out[11] = 0; 36 | out[12] = pos[0]; 37 | out[13] = pos[1]; 38 | out[14] = pos[2]; 39 | out[15] = 1; 40 | 41 | return out; 42 | }; 43 | -------------------------------------------------------------------------------- /src/glov/client/models/box_textured_embed.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/jsjam22/485e23218528910f47409eb8f9ee0967c45bc836/src/glov/client/models/box_textured_embed.glb -------------------------------------------------------------------------------- /src/glov/client/models/box_textured_embed.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "COLLADA2GLTF", 4 | "version": "2.0" 5 | }, 6 | "scene": 0, 7 | "scenes": [ 8 | { 9 | "nodes": [ 10 | 0 11 | ] 12 | } 13 | ], 14 | "nodes": [ 15 | { 16 | "children": [ 17 | 1 18 | ], 19 | "matrix": [1.0, 0.0, 0.0, 0.0, 20 | 0.0, 0.0, -1.0, 0.0, 21 | 0.0, 1.0, 0.0, 0.0, 22 | 0.0, 0.0, 0.0, 1.0 ] 23 | }, 24 | { 25 | "mesh": 0 26 | } 27 | ], 28 | "meshes": [ 29 | { 30 | "primitives": [ 31 | { 32 | "attributes": { 33 | "NORMAL": 1, 34 | "POSITION": 2, 35 | "TEXCOORD_0": 3 36 | }, 37 | "indices": 0, 38 | "mode": 4, 39 | "material": 0 40 | } 41 | ], 42 | "name": "Mesh" 43 | } 44 | ], 45 | "accessors": [ 46 | { 47 | "bufferView": 0, 48 | "byteOffset": 0, 49 | "componentType": 5123, 50 | "count": 36, 51 | "max": [ 52 | 23 53 | ], 54 | "min": [ 55 | 0 56 | ], 57 | "type": "SCALAR" 58 | }, 59 | { 60 | "bufferView": 1, 61 | "byteOffset": 0, 62 | "componentType": 5126, 63 | "count": 24, 64 | "max": [1.0, 1.0, 1.0 ], 65 | "min": [-1.0, -1.0, -1.0 ], 66 | "type": "VEC3" 67 | }, 68 | { 69 | "bufferView": 1, 70 | "byteOffset": 288, 71 | "componentType": 5126, 72 | "count": 24, 73 | "max": [0.5, 0.5, 0.5 ], 74 | "min": [-0.5, -0.5, -0.5 ], 75 | "type": "VEC3" 76 | }, 77 | { 78 | "bufferView": 2, 79 | "byteOffset": 0, 80 | "componentType": 5126, 81 | "count": 24, 82 | "max": [6.0, 1.0 ], 83 | "min": [0.0, 0.0 ], 84 | "type": "VEC2" 85 | } 86 | ], 87 | "materials": [ 88 | { 89 | "pbrMetallicRoughness": { 90 | "baseColorTexture": { 91 | "index": 0 92 | }, 93 | "metallicFactor": 0.0 94 | }, 95 | "name": "Texture" 96 | } 97 | ], 98 | "textures": [ 99 | { 100 | "sampler": 0, 101 | "source": 0 102 | } 103 | ], 104 | "samplers": [ 105 | { 106 | "magFilter": 9729, 107 | "minFilter": 9986, 108 | "wrapS": 10497, 109 | "wrapT": 10497 110 | } 111 | ], 112 | "bufferViews": [ 113 | { 114 | "buffer": 0, 115 | "byteOffset": 768, 116 | "byteLength": 72, 117 | "target": 34963 118 | }, 119 | { 120 | "buffer": 0, 121 | "byteOffset": 0, 122 | "byteLength": 576, 123 | "byteStride": 12, 124 | "target": 34962 125 | }, 126 | { 127 | "buffer": 0, 128 | "byteOffset": 576, 129 | "byteLength": 192, 130 | "byteStride": 8, 131 | "target": 34962 132 | } 133 | ], 134 | "buffers": [ 135 | { 136 | "byteLength": 840, 137 | "uri": "box_textured.bin" 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /src/glov/client/net.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | import { callEach } from 'glov/common/util.js'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 7 | exports.netBuildString = buildString; 8 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 9 | exports.netInit = init; 10 | 11 | /* eslint-disable import/order */ 12 | const { filewatchStartup } = require('./filewatch.js'); 13 | const { packetEnableDebug } = require('glov/common/packet.js'); 14 | const subscription_manager = require('./subscription_manager.js'); 15 | const wsclient = require('./wsclient.js'); 16 | const WSClient = wsclient.WSClient; 17 | 18 | let client; 19 | let subs; 20 | 21 | let post_net_init = []; 22 | 23 | export function netPostInit(cb) { 24 | if (post_net_init) { 25 | post_net_init.push(cb); 26 | } else { 27 | cb(); 28 | } 29 | } 30 | 31 | export function init(params) { 32 | params = params || {}; 33 | if (params.ver) { 34 | wsclient.CURRENT_VERSION = params.ver; 35 | } 36 | if (String(document.location).match(/^https?:\/\/localhost/)) { 37 | if (!params.no_packet_debug) { 38 | console.log('PacketDebug: ON'); 39 | packetEnableDebug(true); 40 | } 41 | } 42 | client = new WSClient(params.path, params.client_app); 43 | subs = subscription_manager.create(client, params.cmd_parse); 44 | subs.auto_create_user = Boolean(params.auto_create_user); 45 | subs.no_auto_login = Boolean(params.no_auto_login); 46 | subs.allow_anon = Boolean(params.allow_anon); 47 | window.subs = subs; // for debugging 48 | exports.subs = subs; 49 | exports.client = client; 50 | callEach(post_net_init, post_net_init = null); 51 | filewatchStartup(client); 52 | 53 | if (params.engine) { 54 | params.engine.addTickFunc((dt) => { 55 | client.checkDisconnect(); 56 | subs.tick(dt); 57 | }); 58 | 59 | params.engine.onLoadMetrics((obj) => { 60 | subs.onceConnected(() => { 61 | client.send('load_metrics', obj); 62 | }); 63 | }); 64 | } 65 | } 66 | 67 | const build_timestamp_string = new Date(Number(BUILD_TIMESTAMP)) 68 | .toISOString() 69 | .replace('T', ' ') 70 | .slice(5, -8); 71 | export function buildString() { 72 | return wsclient.CURRENT_VERSION ? `${wsclient.CURRENT_VERSION} (${build_timestamp_string})` : build_timestamp_string; 73 | } 74 | 75 | export function netDisconnectedRaw() { 76 | return !client || !client.connected || client.disconnected || 77 | !client.socket || client.socket.readyState !== 1; 78 | } 79 | 80 | export function netDisconnected() { 81 | return netDisconnectedRaw() || subs.logging_in; 82 | } 83 | 84 | export function netForceDisconnect() { 85 | if (subs) { 86 | subs.was_logged_in = false; 87 | } 88 | client?.socket?.close?.(); 89 | } 90 | 91 | export function netClient() { 92 | return client; 93 | } 94 | 95 | export function netClientId() { 96 | return client.id; 97 | } 98 | 99 | export function netUserId() { 100 | return subs.getUserId(); 101 | } 102 | 103 | export function netSubs() { 104 | return subs; 105 | } 106 | -------------------------------------------------------------------------------- /src/glov/client/perf_net.ts: -------------------------------------------------------------------------------- 1 | import { wsstats, wsstats_out } from 'glov/common/wscommon'; 2 | import { cmd_parse } from './cmds'; 3 | import * as perf from './perf'; 4 | import * as settings from './settings'; 5 | 6 | const { min } = Math; 7 | 8 | type StatsType = typeof wsstats; 9 | type StatsTracking = StatsType & { 10 | dm: number; 11 | db: number; 12 | time: number; 13 | }; 14 | 15 | settings.register({ 16 | show_net: { 17 | default_value: 0, 18 | type: cmd_parse.TYPE_INT, 19 | enum_lookup: { 20 | OFF: 0, 21 | ON: 2, 22 | }, 23 | }, 24 | }); 25 | let last_wsstats: StatsTracking = { msgs: 0, bytes: 0, time: Date.now(), dm: 0, db: 0 }; 26 | let last_wsstats_out: StatsTracking = { msgs: 0, bytes: 0, time: Date.now(), dm: 0, db: 0 }; 27 | function bandwidth(stats: StatsType, last: StatsTracking): string { 28 | let now = Date.now(); 29 | if (now - last.time > 1000) { 30 | last.dm = stats.msgs - last.msgs; 31 | last.db = stats.bytes - last.bytes; 32 | last.msgs = stats.msgs; 33 | last.bytes = stats.bytes; 34 | if (now - last.time > 2000) { // stall 35 | last.time = now; 36 | } else { 37 | last.time += 1000; 38 | } 39 | } 40 | return `${(last.db/1024).toFixed(2)} kb (${last.dm})`; 41 | } 42 | perf.addMetric({ 43 | name: 'net', 44 | show_stat: 'show_net', 45 | width: 5, 46 | labels: { 47 | 'down: ': bandwidth.bind(null, wsstats, last_wsstats), 48 | 'up: ': bandwidth.bind(null, wsstats_out, last_wsstats_out), 49 | }, 50 | }); 51 | 52 | let ping_providers = 0; 53 | export type PingData = { 54 | ping: number; 55 | fade: number; 56 | }; 57 | export function registerPingProvider(fn: () => PingData | null): void { 58 | ++ping_providers; 59 | let suffix = ping_providers === 1 ? '' : `${ping_providers}`; 60 | 61 | settings.register({ 62 | [`show_ping${suffix}`]: { 63 | default_value: 0, 64 | type: cmd_parse.TYPE_INT, 65 | range: [0,1], 66 | }, 67 | }); 68 | perf.addMetric({ 69 | name: `ping${suffix}`, 70 | show_stat: `show_ping${suffix}`, 71 | labels: { 72 | 'ping: ': () => { 73 | let pt = fn(); 74 | if (!pt || pt.fade < 0.001) { 75 | return ''; 76 | } 77 | return { value: `${pt.ping.toFixed(1)}`, alpha: min(1, pt.fade * 3) }; 78 | }, 79 | }, 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /src/glov/client/pico8.js: -------------------------------------------------------------------------------- 1 | /* eslint no-bitwise: off */ 2 | const { vec4 } = require('glov/common/vmath.js'); 3 | 4 | export const colors = [ 5 | vec4(0, 0, 0, 1), 6 | vec4(0.114, 0.169, 0.326, 1), 7 | vec4(0.494, 0.145, 0.326, 1), 8 | vec4(0.000, 0.529, 0.328, 1), 9 | vec4(0.671, 0.322, 0.212, 1), 10 | vec4(0.373, 0.341, 0.310, 1), 11 | vec4(0.761, 0.765, 0.780, 1), 12 | vec4(1.000, 0.945, 0.910, 1), 13 | vec4(1.000, 0.000, 0.302, 1), 14 | vec4(1.000, 0.639, 0.000, 1), 15 | vec4(1.000, 0.925, 0.153, 1), 16 | vec4(0.000, 0.894, 0.212, 1), 17 | vec4(0.161, 0.678, 1.000, 1), 18 | vec4(0.514, 0.463, 0.612, 1), 19 | vec4(1.000, 0.467, 0.659, 1), 20 | vec4(1.000, 0.800, 0.667, 1), 21 | ]; 22 | 23 | export const font_colors = colors.map((a) => (a[0] * 255) << 24 | (a[1] * 255) << 16 | (a[2] * 255) << 8 | 255); 24 | -------------------------------------------------------------------------------- /src/glov/client/pointer_lock.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | const { eatPossiblePromise } = require('glov/common/util.js'); 5 | 6 | let user_want_locked = false; 7 | let elem; 8 | let on_ptr_lock; 9 | 10 | export function isLocked() { 11 | return user_want_locked; // Either it's locked, or there's an async attempt to lock it outstanding 12 | } 13 | 14 | function pointerLog(msg) { 15 | console.log(`PointerLock: ${msg}`); // TODO: Disable this after things settle 16 | } 17 | 18 | export function exit() { 19 | pointerLog('Lock exit requested'); 20 | user_want_locked = false; 21 | eatPossiblePromise(document.exitPointerLock()); 22 | } 23 | 24 | export function enter(when) { 25 | user_want_locked = true; 26 | on_ptr_lock(); 27 | pointerLog(`Trying pointer lock in response to ${when}`); 28 | eatPossiblePromise(elem.requestPointerLock()); 29 | } 30 | 31 | function onPointerLockChange() { 32 | if (document.pointerLockElement || document.mozPointerLockElement || document.webkitPointerLockElement) { 33 | pointerLog('Lock successful'); 34 | if (!user_want_locked) { 35 | pointerLog('User canceled lock'); 36 | eatPossiblePromise(document.exitPointerLock()); 37 | } 38 | } else { 39 | if (user_want_locked) { 40 | pointerLog('Lock lost'); 41 | user_want_locked = false; 42 | } 43 | } 44 | } 45 | 46 | function onPointerLockError(e) { 47 | pointerLog('Error'); 48 | user_want_locked = false; 49 | } 50 | 51 | export function startup(_elem, _on_ptr_lock) { 52 | elem = _elem; 53 | on_ptr_lock = _on_ptr_lock; 54 | 55 | elem.requestPointerLock = elem.requestPointerLock || elem.mozRequestPointerLock || 56 | elem.webkitRequestPointerLock || function () { /* nop */ }; 57 | document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock || 58 | document.webkitExitPointerLock || function () { /* nop */ }; 59 | 60 | document.addEventListener('pointerlockchange', onPointerLockChange, false); 61 | document.addEventListener('mozpointerlockchange', onPointerLockChange, false); 62 | document.addEventListener('webkitpointerlockchange', onPointerLockChange, false); 63 | 64 | document.addEventListener('pointerlockerror', onPointerLockError, false); 65 | document.addEventListener('mozpointerlockerror', onPointerLockError, false); 66 | document.addEventListener('webkitpointerlockerror', onPointerLockError, false); 67 | } 68 | -------------------------------------------------------------------------------- /src/glov/client/rand_fast.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | // RandSeed2 5 | // Derived from libGlov, MIT Licensed 6 | // Super-simple RNG. 2-3x faster than Alea, but probably some correlation in 7 | // anything but 1D. 8 | // Allows for fast, direct manipulation of rand.seed (if correlation between adjacent seeds is acceptable) 9 | 10 | //const MAX_INT2 = 0xFFFFFFFF; 11 | 12 | // Initialize with two steps past the seed, otherwise close seeds (e.g. 0 and 1) produce very close first results 13 | function step2(seed) { 14 | seed = (seed >>> 0) || 0x532f638c2; // arbitrary non-zero 15 | seed ^= seed << 13; 16 | seed ^= seed >>> 17; 17 | seed ^= seed << 5; 18 | seed ^= seed << 13; 19 | seed ^= seed >>> 17; 20 | seed ^= seed << 5; 21 | return seed >>> 0; 22 | } 23 | 24 | function RandSeed2(seed) { 25 | this.seed = step2(seed); 26 | } 27 | RandSeed2.prototype.reseed = function (seed) { 28 | this.seed = step2(seed); 29 | }; 30 | RandSeed2.prototype.step = function () { // as long as seed is never === 0, this never returns 0 31 | let seed = this.seed; 32 | seed ^= seed << 13; 33 | seed ^= seed >>> 17; 34 | seed ^= seed << 5; 35 | return (this.seed = (seed >>> 0)) - 1; 36 | }; 37 | RandSeed2.prototype.uint32 = RandSeed2.prototype.step; 38 | // returns [0,range-1] 39 | RandSeed2.prototype.range = function (range) { 40 | // slightly slower (esp before opt): return (this.step() * range / MAX_INT2) | 0; // faster than this.step() % range 41 | // slower: return this.step() % range; 42 | return (this.step() * range * 2.3283064376e-10) | 0; // 1/MAX_INT2 - largest float such that 0xFFFFFFFE*f < 1.0 43 | }; 44 | // returns [0,1) 45 | RandSeed2.prototype.random = function () { 46 | // slower: return this.step() / MAX_INT2 47 | return this.step() * 2.3283064376e-10; // 1/MAX_INT2 - largest float such that 0xFFFFFFFE*f < 1.0 48 | }; 49 | RandSeed2.prototype.floatBetween = function (a, b) { 50 | return a + (b - a) * this.random(); 51 | }; 52 | 53 | export function randFastCreate(seed) { 54 | return new RandSeed2(seed); 55 | } 56 | 57 | // from https://www.shadertoy.com/view/wsXfDM 58 | const RND_A = 134775813; 59 | const RND_B = 1103515245; 60 | export function randSimpleSpatial(seed, x, y, z) { 61 | y += z * 10327; 62 | 63 | return (((((x ^ y) * RND_A) ^ (seed + x)) * (((RND_B * x) << 16) ^ (RND_B * y) - RND_A)) >>> 0) / 4294967295; 64 | } 65 | -------------------------------------------------------------------------------- /src/glov/client/require.js: -------------------------------------------------------------------------------- 1 | /* globals self */ 2 | const glob = typeof window === 'undefined' ? self : window; 3 | let deps = glob.deps = glob.deps || {}; 4 | glob.require = function (mod) { 5 | if (!deps[mod]) { 6 | throw new Error(`Cannot find module '${mod}' (add it to deps.js or equivalent)`); 7 | } 8 | return deps[mod]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/glov/client/shaders/default.fp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL 4 | 5 | precision lowp float; 6 | 7 | uniform sampler2D tex0; // source 8 | 9 | uniform vec3 light_diffuse; 10 | uniform vec3 light_dir_vs; 11 | uniform vec3 ambient; 12 | 13 | varying vec4 interp_color; 14 | varying vec2 interp_texcoord; 15 | varying vec3 interp_normal_vs; 16 | 17 | void main(void) { 18 | vec4 texture0 = texture2D(tex0, interp_texcoord.xy); 19 | #ifndef NOGAMMA 20 | texture0.rgb = texture0.rgb * texture0.rgb; // pow(2) 21 | #endif 22 | vec4 albedo = texture0 * interp_color; 23 | if (albedo.a < 0.01) // TODO: Probably don't want this, but makes hacking transparent things together easier for now 24 | discard; 25 | 26 | vec3 normal_vs = normalize(interp_normal_vs); 27 | float diffuse = max(0.0, 0.5 + 0.5 * dot(normal_vs, -light_dir_vs.rgb)); 28 | 29 | vec3 light_color = diffuse * light_diffuse.rgb + ambient.rgb; 30 | gl_FragColor = vec4(light_color * albedo.rgb, albedo.a); 31 | 32 | #ifndef NOGAMMA 33 | gl_FragColor.rgb = pow(gl_FragColor.rgb, vec3(1.0/2.0)); 34 | #endif 35 | } -------------------------------------------------------------------------------- /src/glov/client/shaders/default.vp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL 4 | precision highp float; 5 | 6 | // per-vertex input 7 | attribute vec3 POSITION; 8 | //attribute vec3 COLOR; 9 | attribute vec2 TEXCOORD; 10 | attribute vec3 NORMAL; 11 | 12 | // per-drawcall input 13 | uniform mat3 mv_inv_trans; 14 | uniform mat4 projection; 15 | uniform mat4 mat_mv; 16 | uniform vec4 color; 17 | 18 | // output 19 | varying vec4 interp_color; 20 | varying vec2 interp_texcoord; 21 | varying vec3 interp_normal_vs; 22 | // varying vec3 interp_pos_vs; 23 | 24 | void main(void) { 25 | //interp_color = vec4(COLOR * color.rgb, color.a); 26 | interp_color = color; 27 | interp_texcoord = vec2(TEXCOORD); 28 | interp_normal_vs = mv_inv_trans * NORMAL; 29 | // gl_Position = vec4(POSITION, 1.0); 30 | 31 | // gl_Position = mat_vp * (mat_m * vec4(POSITION, 1.0)); 32 | // gl_Position = mvp * vec4(POSITION, 1.0); 33 | vec4 pos_vs = mat_mv * vec4(POSITION, 1.0); 34 | // interp_pos_vs = pos_vs.xyz; 35 | gl_Position = projection * pos_vs; 36 | } -------------------------------------------------------------------------------- /src/glov/client/shaders/effects_bloom_merge.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision highp float; 4 | precision highp int; 5 | 6 | varying vec2 interp_texcoord; 7 | 8 | vec4 _ret_0; 9 | vec4 _TMP3; 10 | vec4 _TMP5; 11 | float _TMP2; 12 | vec4 _TMP1; 13 | float _TMP0; 14 | vec4 _TMP36; 15 | uniform float bloomSaturation; 16 | uniform float originalSaturation; 17 | uniform float bloomIntensity; 18 | uniform float originalIntensity; 19 | uniform sampler2D inputTexture0; 20 | uniform sampler2D inputTexture1; 21 | 22 | void main() 23 | { 24 | vec4 _orig; 25 | vec4 _bloom; 26 | _orig = texture2D(inputTexture0, interp_texcoord); 27 | _bloom = texture2D(inputTexture1, interp_texcoord); 28 | _TMP0 = dot(_bloom.xyz, vec3(2.12599993E-01, 7.15200007E-01, 7.22000003E-02)); 29 | _TMP1 = vec4(_TMP0, _TMP0, _TMP0, _TMP0) + bloomSaturation * (_bloom - vec4(_TMP0, _TMP0, _TMP0, _TMP0)); 30 | _bloom = _TMP1 * bloomIntensity; 31 | _TMP2 = dot(_orig.xyz, vec3(2.12599993E-01, 7.15200007E-01, 7.22000003E-02)); 32 | _TMP3 = vec4(_TMP2, _TMP2, _TMP2, _TMP2) + originalSaturation * (_orig - vec4(_TMP2, _TMP2, _TMP2, _TMP2)); 33 | _TMP5 = min(vec4(1.0, 1.0, 1.0, 1.0), _bloom); 34 | _TMP36 = max(vec4(0.0, 0.0, 0.0, 0.0), _TMP5); 35 | _orig = (_TMP3 * (1.0 - _TMP36)) * originalIntensity; 36 | _ret_0 = _bloom + _orig; 37 | gl_FragColor = _ret_0; 38 | } 39 | -------------------------------------------------------------------------------- /src/glov/client/shaders/effects_bloom_threshold.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision highp float; 4 | precision highp int; 5 | 6 | varying vec2 interp_texcoord; 7 | 8 | vec4 _ret_0; 9 | float _TMP1; 10 | float _TMP0; 11 | float _a0025; 12 | float _x0027; 13 | uniform float bloomThreshold; 14 | uniform float thresholdCutoff; 15 | uniform sampler2D inputTexture0; 16 | 17 | void main() 18 | { 19 | vec4 _col; 20 | float _luminance; 21 | float _x; 22 | float _cut; 23 | _col = texture2D(inputTexture0, interp_texcoord); 24 | _luminance = dot(_col.xyz, vec3(2.12599993E-01, 7.15200007E-01, 7.22000003E-02)); 25 | _x = float((_luminance >= bloomThreshold)); 26 | _a0025 = 3.14159274 * (_luminance / bloomThreshold - 0.5); 27 | _TMP0 = sin(_a0025); 28 | _x0027 = 0.5 * (1.0 + _TMP0); 29 | _TMP1 = pow(_x0027, thresholdCutoff); 30 | _cut = bloomThreshold * _TMP1; 31 | _ret_0 = (_x + (1.0 - _x) * _cut) * _col; 32 | gl_FragColor = _ret_0; 33 | } 34 | -------------------------------------------------------------------------------- /src/glov/client/shaders/effects_color_matrix.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision lowp float; 4 | 5 | varying vec2 interp_texcoord; 6 | 7 | uniform vec4 colorMatrix[3]; 8 | uniform sampler2D tex0; 9 | 10 | void main() 11 | { 12 | vec4 _color; 13 | vec4 _mutc; 14 | _color = texture2D(tex0, interp_texcoord); 15 | _mutc = _color; 16 | _mutc.w = 1.0; 17 | vec3 _r0019; 18 | _r0019.x = dot(colorMatrix[0], _mutc); 19 | _r0019.y = dot(colorMatrix[1], _mutc); 20 | _r0019.z = dot(colorMatrix[2], _mutc); 21 | _mutc.xyz = _r0019; 22 | _mutc.w = _color.w; 23 | gl_FragColor = _mutc; 24 | } -------------------------------------------------------------------------------- /src/glov/client/shaders/effects_copy.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision lowp float; 4 | 5 | varying vec2 interp_texcoord; 6 | 7 | uniform sampler2D inputTexture0; 8 | void main() 9 | { 10 | gl_FragColor = texture2D(inputTexture0, interp_texcoord); 11 | } 12 | -------------------------------------------------------------------------------- /src/glov/client/shaders/effects_copy.vp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | precision highp float; 3 | 4 | varying vec2 interp_texcoord; 5 | attribute vec2 POSITION; 6 | 7 | uniform vec4 copy_uv_scale; 8 | uniform vec4 clip_space; 9 | 10 | void main() 11 | { 12 | interp_texcoord = POSITION * copy_uv_scale.xy + copy_uv_scale.zw; 13 | gl_Position = vec4(POSITION * clip_space.xy + clip_space.zw, 0, 1); 14 | } -------------------------------------------------------------------------------- /src/glov/client/shaders/effects_distort.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision highp float; 4 | precision highp int; 5 | 6 | varying vec2 interp_texcoord; 7 | 8 | vec4 _ret_0; 9 | vec2 _UV1; 10 | vec4 _TMP1; 11 | vec2 _r0020; 12 | vec2 _r0028; 13 | vec2 _v0028; 14 | uniform vec2 strength; 15 | uniform vec3 transform[2]; 16 | uniform vec2 invTransform[2]; 17 | uniform sampler2D inputTexture0; 18 | uniform sampler2D distortTexture; 19 | 20 | void main() 21 | { 22 | vec3 _uvt; 23 | _uvt = vec3(interp_texcoord.x, interp_texcoord.y, 1.0); 24 | _r0020.x = dot(transform[0], _uvt); 25 | _r0020.y = dot(transform[1], _uvt); 26 | _TMP1 = texture2D(distortTexture, _r0020); 27 | _v0028 = _TMP1.xy - 0.5; 28 | _r0028.x = dot(invTransform[0], _v0028); 29 | _r0028.y = dot(invTransform[1], _v0028); 30 | _UV1 = interp_texcoord + _r0028 * strength; 31 | _ret_0 = texture2D(inputTexture0, _UV1); 32 | gl_FragColor = _ret_0; 33 | } 34 | -------------------------------------------------------------------------------- /src/glov/client/shaders/effects_gaussian_blur.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision lowp float; 4 | 5 | varying vec2 interp_texcoord; 6 | 7 | vec4 _ret_0; 8 | vec4 _TMP2; 9 | vec4 _TMP1; 10 | vec2 _c0022; 11 | vec2 _c0024; 12 | uniform vec3 sampleRadius; 13 | uniform sampler2D inputTexture0; 14 | 15 | void main() 16 | { 17 | vec2 uv = interp_texcoord; 18 | vec2 step = sampleRadius.xy; 19 | float glow = sampleRadius.z; 20 | gl_FragColor = 21 | ((texture2D(inputTexture0, uv - step * 3.0) + texture2D(inputTexture0, uv + step * 3.0)) * 0.085625 + 22 | (texture2D(inputTexture0, uv - step * 2.0) + texture2D(inputTexture0, uv + step * 2.0)) * 0.12375 + 23 | (texture2D(inputTexture0, uv - step * 1.0) + texture2D(inputTexture0, uv + step * 1.0)) * 0.234375 + 24 | texture2D(inputTexture0, uv) * 0.3125) * 0.83333333333333333333333333333333 * glow; 25 | } 26 | -------------------------------------------------------------------------------- /src/glov/client/shaders/error.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL 2 | 3 | void main(void) { 4 | gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /src/glov/client/shaders/error.vp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL 4 | attribute vec2 POSITION; 5 | void main() { 6 | gl_Position = vec4(POSITION.xy * vec2(2.0 / 1024.0, -2.04 / 1024.0) + vec2(-1.0, 1.0), 0.0, 1.0); 7 | } 8 | -------------------------------------------------------------------------------- /src/glov/client/shaders/error_gl2.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | void main(void) { 4 | gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /src/glov/client/shaders/font_aa.fp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2022 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL2 4 | 5 | precision lowp float; 6 | 7 | varying vec2 interp_texcoord; 8 | varying lowp vec4 interp_color; 9 | uniform sampler2D tex0; 10 | uniform mediump vec4 param0; 11 | void main() 12 | { 13 | // Body 14 | float sdf = texture2D(tex0,interp_texcoord).r; 15 | float blend_t = clamp(sdf * param0.x + param0.y, 0.0, 1.0); 16 | #ifdef NOPREMUL 17 | gl_FragColor = vec4(interp_color.rgb, interp_color.a * blend_t); 18 | #else 19 | gl_FragColor = interp_color * blend_t; 20 | #endif 21 | } 22 | -------------------------------------------------------------------------------- /src/glov/client/shaders/font_aa_glow.fp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2022 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL2 4 | 5 | precision lowp float; 6 | 7 | varying vec2 interp_texcoord; 8 | varying lowp vec4 interp_color; 9 | uniform sampler2D tex0; 10 | uniform mediump vec4 param0; 11 | uniform vec4 glow_color; 12 | uniform mediump vec4 glow_params; 13 | void main() 14 | { 15 | // Body 16 | float sdf = texture2D(tex0, interp_texcoord).r; 17 | float blend_t = clamp(sdf * param0.x + param0.y, 0.0, 1.0); 18 | // Glow 19 | vec2 glow_coord = interp_texcoord + glow_params.xy; 20 | float sdf_glow = texture2D(tex0, glow_coord).r; 21 | float glow_t = clamp(sdf_glow * glow_params.z + glow_params.w, 0.0, 1.0); 22 | // Composite 23 | #ifdef NOPREMUL 24 | vec4 my_glow_color = vec4(glow_color.xyz, glow_t * glow_color.w); 25 | gl_FragColor = mix(my_glow_color, interp_color, blend_t); 26 | #else 27 | vec4 my_glow_color = glow_color * glow_t; 28 | gl_FragColor = mix(my_glow_color, interp_color, blend_t); 29 | #endif 30 | } 31 | -------------------------------------------------------------------------------- /src/glov/client/shaders/font_aa_outline.fp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2022 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL2 4 | 5 | precision lowp float; 6 | 7 | varying highp vec2 interp_texcoord; 8 | varying lowp vec4 interp_color; 9 | uniform sampler2D tex0; 10 | uniform mediump vec4 param0; 11 | uniform vec4 outline_color; 12 | void main() 13 | { 14 | // Body 15 | float sdf = texture2D(tex0, interp_texcoord).r; 16 | float blend_t = clamp(sdf * param0.x + param0.y, 0.0, 1.0); 17 | // Outline 18 | float outline_t = clamp(sdf * param0.x + param0.z, 0.0, 1.0); 19 | // Composite 20 | #ifdef NOPREMUL 21 | outline_t = outline_t * outline_color.w; 22 | vec4 outcolor = vec4(outline_color.xyz, outline_t); 23 | gl_FragColor = mix(outcolor, interp_color, blend_t); 24 | #else 25 | vec4 my_outline_color = outline_color * outline_t; 26 | gl_FragColor = mix(my_outline_color, interp_color, blend_t); 27 | #endif 28 | } 29 | -------------------------------------------------------------------------------- /src/glov/client/shaders/font_aa_outline_glow.fp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2022 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL2 4 | 5 | precision lowp float; 6 | 7 | varying highp vec2 interp_texcoord; 8 | varying lowp vec4 interp_color; 9 | uniform sampler2D tex0; 10 | uniform mediump vec4 param0; 11 | uniform vec4 outline_color; 12 | uniform vec4 glow_color; 13 | uniform mediump vec4 glow_params; 14 | 15 | void main() 16 | { 17 | // Body 18 | float sdf = texture2D(tex0, interp_texcoord).r; 19 | float blend_t = clamp(sdf * param0.x + param0.y, 0.0, 1.0); 20 | // Outline 21 | float outline_t = clamp(sdf * param0.x + param0.z, 0.0, 1.0); 22 | // Glow 23 | vec2 glow_coord = interp_texcoord + glow_params.xy; 24 | float sdf_glow = texture2D(tex0, glow_coord).r; 25 | float glow_t = clamp(sdf_glow * glow_params.z + glow_params.w, 0.0, 1.0); 26 | 27 | // Composite 28 | #ifdef NOPREMUL 29 | // Outline on top of glow 30 | vec4 my_glow_color = vec4(glow_color.xyz, glow_t * glow_color.w); 31 | // Previously had the following (blends better with soft outline on hard glow, but breaks alpha-fade of whole style, see below) 32 | // outline_t = outline_t * outline_color.w; 33 | vec4 outcolor = mix(my_glow_color, outline_color, outline_t); 34 | // Body on top of that 35 | gl_FragColor = mix(outcolor, interp_color, blend_t); 36 | #else 37 | // Outline on top of glow 38 | vec4 my_glow_color = glow_color * glow_t; 39 | 40 | // This allows a soft outline to blend well through to a glow underneath, but 41 | // causes the colors to bleed when the whole style is faded: 42 | // vec4 my_outline_color = outline_color * outline_t; 43 | // vec4 outcolor = my_outline_color + (1.0 - my_outline_color.a) * my_glow_color; // Equivalent to glBlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) 44 | 45 | // Instead, this allows fading of the entire color by an alpha to keep the relative colors: 46 | // vec4 outcolor = my_outline_color + (1.0 - outline_t) * my_glow_color; 47 | vec4 outcolor = mix(my_glow_color, outline_color, outline_t); 48 | 49 | // Body on top of that 50 | gl_FragColor = mix(outcolor, interp_color, blend_t); 51 | #endif 52 | } 53 | -------------------------------------------------------------------------------- /src/glov/client/shaders/pixely_expand.fp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL2 4 | 5 | precision mediump float; 6 | precision mediump int; 7 | 8 | varying highp vec2 interp_texcoord; 9 | uniform sampler2D inputTexture0; // source 10 | uniform sampler2D inputTexture1; // hblur 11 | uniform sampler2D inputTexture2; // hblur+vblur 12 | uniform vec4 orig_pixel_size; 13 | 14 | // 1D Gaussian. 15 | float Gaus(float pos, float scale) { 16 | return exp2(scale*pos*pos); 17 | } 18 | 19 | const float SHADE = 0.75; 20 | const float EASING = 1.25; 21 | 22 | #define DO_WARP 23 | #ifdef DO_WARP 24 | const float VIGNETTE = 0.5; 25 | // Display warp. 26 | // 0.0 = none 27 | // 1.0/8.0 = extreme 28 | const vec2 WARP=vec2(1.0/32.0,1.0/24.0); 29 | 30 | // Distortion of scanlines, and end of screen alpha. 31 | vec2 Warp(vec2 pos){ 32 | pos=pos*2.0-1.0; 33 | pos*=vec2(1.0+(pos.y*pos.y)*WARP.x,1.0+(pos.x*pos.x)*WARP.y); 34 | return pos*0.5+0.5; 35 | } 36 | #else 37 | #define Warp(v) v 38 | #endif 39 | 40 | float easeInOut(float v) { 41 | float va = pow(v, EASING); 42 | return va / (va + pow((1.0 - v), EASING)); 43 | } 44 | 45 | float easeIn(float v) { 46 | return 2.0 * easeInOut(0.5 * v); 47 | } 48 | 49 | float easeOut(float v) { 50 | return 2.0 * easeInOut(0.5 + 0.5 * v) - 1.0; 51 | } 52 | 53 | void main() 54 | { 55 | vec2 texcoords = Warp(interp_texcoord); 56 | vec2 intcoords = (floor(texcoords.xy * orig_pixel_size.xy) + 0.5) * orig_pixel_size.zw; 57 | vec2 deltacoords = (texcoords.xy - intcoords) * orig_pixel_size.xy; // -0.5 ... 0.5 58 | // for horizontal sampling, map [-0.5 .. -A .. A .. 0.5] -> [-0.5 .. 0 .. 0 .. 0.5]; 59 | float A = 0.25; 60 | float Ainv = (0.5 - A) * 2.0; 61 | float uoffs = clamp((abs(deltacoords.x) - A) / Ainv, 0.0, 1.0) * orig_pixel_size.z; 62 | uoffs *= sign(deltacoords.x); 63 | vec2 sample_coords = vec2(intcoords.x + uoffs, intcoords.y); 64 | // sample_coords = intcoords; 65 | vec3 color = texture2D(inputTexture1, sample_coords).rgb; 66 | vec3 color_scanline = texture2D(inputTexture2, texcoords.xy + vec2(0.0, 0.5 * orig_pixel_size.w)).rgb * SHADE; 67 | // color_scanline = vec3(0); 68 | 69 | // float mask = Gaus(deltacoords.y, -12.0); 70 | float mask = easeOut(2.0*(0.5 - abs(deltacoords.y))); 71 | // float mask = abs(deltacoords.y) > 0.25 ? 0.0 : 1.0; 72 | color = mix(color_scanline, color, mask); 73 | // color = vec3(mask); 74 | 75 | #ifdef DO_WARP 76 | // vignette 77 | float dist = min(1.0, 100.0 * min(0.5 - abs(texcoords.x - 0.5), 0.5 - abs(texcoords.y - 0.5))); 78 | color *= (1.0 - VIGNETTE) + VIGNETTE * dist; 79 | #endif 80 | 81 | gl_FragColor = vec4(color, 1.0); 82 | // gl_FragColor = vec4(color_scanline, 1.0); 83 | // gl_FragColor = vec4(sample_coords, 0.0, 1.0); 84 | } 85 | -------------------------------------------------------------------------------- /src/glov/client/shaders/snapshot.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision lowp float; 4 | 5 | uniform sampler2D tex0; 6 | uniform sampler2D tex1; 7 | uniform lowp vec4 color1; 8 | 9 | varying lowp vec4 interp_color; 10 | varying vec2 interp_texcoord; 11 | 12 | void main(void) { 13 | vec3 texA = texture2D(tex0,interp_texcoord).rgb; 14 | float texB = texture2D(tex1,interp_texcoord).r; 15 | float alpha = texA.r - texB + 1.0; 16 | // TODO: (perf?) (quality?) better to output pre-multiplied alpha (texA) and change state? 17 | vec3 orig_rgb = texA / max(0.01, alpha); 18 | gl_FragColor = vec4(orig_rgb, alpha * interp_color.a); 19 | } 20 | -------------------------------------------------------------------------------- /src/glov/client/shaders/sprite.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision lowp float; 4 | 5 | uniform sampler2D tex0; 6 | 7 | varying lowp vec4 interp_color; 8 | varying vec2 interp_texcoord; 9 | 10 | void main(void) { 11 | vec4 tex = texture2D(tex0, interp_texcoord); 12 | gl_FragColor = tex * interp_color; 13 | } 14 | -------------------------------------------------------------------------------- /src/glov/client/shaders/sprite.vp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL2 4 | precision highp float; 5 | 6 | // per-vertex input 7 | attribute vec2 POSITION; 8 | attribute vec4 COLOR; 9 | attribute vec2 TEXCOORD; 10 | 11 | // output 12 | varying lowp vec4 interp_color; 13 | varying vec2 interp_texcoord; 14 | 15 | // global parameters 16 | uniform vec4 clip_space; 17 | 18 | void main() 19 | { 20 | interp_texcoord = TEXCOORD; 21 | interp_color = COLOR; 22 | gl_Position = vec4(POSITION.xy * clip_space.xy + clip_space.zw, 0.0, 1.0); 23 | } 24 | -------------------------------------------------------------------------------- /src/glov/client/shaders/sprite3d.vp: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2022 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | #pragma WebGL2 4 | precision highp float; 5 | 6 | // per-vertex input 7 | attribute vec4 POSITION; 8 | attribute vec4 COLOR; 9 | attribute vec4 TEXCOORD; 10 | 11 | // per-drawcall input 12 | uniform mat4 mat_vp; 13 | 14 | // output 15 | varying lowp vec4 interp_color; 16 | varying vec2 interp_texcoord; 17 | 18 | void main(void) { 19 | interp_texcoord = TEXCOORD.xy; 20 | interp_color = COLOR; 21 | vec3 pos = POSITION.xyz; 22 | gl_Position = mat_vp * vec4(pos, 1.0); 23 | } 24 | -------------------------------------------------------------------------------- /src/glov/client/shaders/sprite_dual.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision lowp float; 4 | 5 | uniform sampler2D tex0; 6 | uniform sampler2D tex1; 7 | uniform lowp vec4 color1; 8 | 9 | varying lowp vec4 interp_color; 10 | varying vec2 interp_texcoord; 11 | 12 | void main(void) { 13 | vec4 texA = texture2D(tex0,interp_texcoord); 14 | vec2 texB = texture2D(tex1,interp_texcoord).rg; 15 | float value = dot(texA.rgb, vec3(0.2, 0.5, 0.3)); 16 | vec3 valueR = value * interp_color.rgb; 17 | vec3 valueG = value * color1.rgb; 18 | vec3 value3 = mix(texA.rgb, valueG, texB.g); 19 | value3 = mix(value3, valueR, texB.r); 20 | gl_FragColor = vec4(value3, texA.a * interp_color.a); 21 | } 22 | -------------------------------------------------------------------------------- /src/glov/client/shaders/transition_pixelate.fp: -------------------------------------------------------------------------------- 1 | #pragma WebGL2 2 | 3 | precision lowp float; 4 | 5 | varying highp vec2 interp_texcoord; 6 | 7 | uniform sampler2D tex0; 8 | uniform vec4 param0; 9 | uniform vec4 param1; 10 | 11 | void main(void) 12 | { 13 | vec2 interp_uvs = interp_texcoord; 14 | // TODO: for best look, should generate an appropriate mipmap and sample from that/just render it w/ nearest neighbor 15 | // result = texture2D(tex0, min(floor(interp_uvs.xy * param0.xy + 0.5) * param0.zw - param1.xy, param1.zw) ); 16 | 17 | // Unlike ARBfp version, shift RGB channels separately (3x slowdown) 18 | vec4 texture0r = texture2D(tex0, min(floor(interp_uvs.xy * param0.xy + vec2(0.58, 0.5)) * param0.zw - param1.xy, param1.zw) ); 19 | vec4 texture0g = texture2D(tex0, min(floor(interp_uvs.xy * param0.xy + vec2(0.5, 0.48)) * param0.zw - param1.xy, param1.zw) ); 20 | vec4 texture0b = texture2D(tex0, min(floor(interp_uvs.xy * param0.xy + vec2(0.42, 0.5)) * param0.zw - param1.xy, param1.zw) ); 21 | gl_FragColor = vec4(texture0r.r, texture0g.g, texture0b.b, 1); 22 | } 23 | -------------------------------------------------------------------------------- /src/glov/client/shims/assert.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | function ok(exp, msg) { 5 | if (exp) { 6 | return; 7 | } 8 | msg = msg ? msg : (exp === undefined || exp === false) ? '' : JSON.stringify(exp); 9 | throw new Error(`Assertion failed${msg ? `: ${msg}` : ''}`); 10 | } 11 | module.exports = ok; 12 | module.exports.ok = ok; 13 | 14 | function equal(a, b) { 15 | if (a === b) { 16 | return; 17 | } 18 | throw new Error(`Assertion failed: "${a}"==="${b}"`); 19 | } 20 | module.exports.equal = equal; 21 | -------------------------------------------------------------------------------- /src/glov/client/shims/buffer.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | export let Buffer = {}; 5 | Buffer.isBuffer = function (b) { 6 | return false; 7 | }; 8 | -------------------------------------------------------------------------------- /src/glov/client/shims/empty.js: -------------------------------------------------------------------------------- 1 | module.exports = undefined; 2 | -------------------------------------------------------------------------------- /src/glov/client/shims/timers.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | exports.setImmediate = window.setImmediate || function setImmediate(fn) { 5 | return setTimeout(fn, 0); 6 | }; 7 | exports.clearImmediate = window.clearImmediate || function clearImmediate(id) { 8 | return clearTimeout(id); 9 | }; 10 | -------------------------------------------------------------------------------- /src/glov/client/simple_menu.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem, SelectionBoxOpts } from './selection_box'; 2 | 3 | export interface SimpleMenu { 4 | run(params?: SelectionBoxOpts): number; 5 | isSelected(): boolean | string; 6 | isSelected(tag_or_index?: number | string): boolean; 7 | 8 | getSelectedIndex(): number; 9 | getSelectedItem(): MenuItem; 10 | getItem(index: number): MenuItem; 11 | } 12 | 13 | export function simpleMenuCreate(params?: SelectionBoxOpts): SimpleMenu; 14 | -------------------------------------------------------------------------------- /src/glov/client/sprite_sets.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const sprite_sets = { 4 | stone: { 5 | button: { name: 'stone/button', ws: [32, 64, 32], hs: [128] }, 6 | button_rollover: { name: 'stone/button_rollover', ws: [32, 64, 32], hs: [128] }, 7 | button_down: { name: 'stone/button_down', ws: [32, 64, 32], hs: [128] }, 8 | button_disabled: { name: 'stone/button_disabled', ws: [32, 64, 32], hs: [128] }, 9 | }, 10 | pixely: { 11 | color_set_shades: [0.8, 0.7, 0.4], 12 | slider_params: [1, 1, 0.3], 13 | 14 | button: { name: 'pixely/button', ws: [4, 5, 4], hs: [13] }, 15 | button_rollover: null, 16 | button_down: { name: 'pixely/button_down', ws: [4, 5, 4], hs: [13] }, 17 | button_disabled: { name: 'pixely/button_disabled', ws: [4, 5, 4], hs: [13] }, 18 | panel: { name: 'pixely/panel', ws: [3, 2, 3], hs: [3, 10, 3] }, 19 | menu_entry: { name: 'pixely/menu_entry', ws: [4, 5, 4], hs: [13] }, 20 | menu_selected: { name: 'pixely/menu_selected', ws: [4, 5, 4], hs: [13] }, 21 | menu_down: { name: 'pixely/menu_down', ws: [4, 5, 4], hs: [13] }, 22 | menu_header: { name: 'pixely/menu_header', ws: [4, 5, 12], hs: [13] }, 23 | slider: { name: 'pixely/slider', ws: [6, 2, 6], hs: [13] }, 24 | // slider_notch: name: 'pixely///',{ ws: [3], hs: [13] }, 25 | slider_handle: { name: 'pixely/slider_handle', ws: [9], hs: [13] }, 26 | 27 | scrollbar_bottom: { name: 'pixely/scrollbar_bottom', ws: [11], hs: [13] }, 28 | scrollbar_trough: { name: 'pixely/scrollbar_trough', ws: [11], hs: [8], wrap_t: true }, 29 | scrollbar_top: { name: 'pixely/scrollbar_top', ws: [11], hs: [13] }, 30 | scrollbar_handle_grabber: { name: 'pixely/scrollbar_handle_grabber', ws: [11], hs: [13] }, 31 | scrollbar_handle: { name: 'pixely/scrollbar_handle', ws: [11], hs: [3, 7, 3] }, 32 | progress_bar: { name: 'pixely/progress_bar', ws: [3, 7, 3], hs: [13] }, 33 | progress_bar_trough: { name: 'pixely/progress_bar_trough', ws: [3, 7, 3], hs: [13] }, 34 | 35 | collapsagories: { name: 'pixely/collapsagories', ws: [4, 5, 4], hs: [13] }, 36 | collapsagories_rollover: { name: 'pixely/collapsagories_rollover', ws: [4, 5, 4], hs: [13] }, 37 | collapsagories_shadow_down: { name: 'pixely/collapsagories_shadow_down', ws: [1, 2, 1], hs: [13] }, 38 | }, 39 | }; 40 | 41 | export function spriteSetGet(key) { 42 | assert(sprite_sets[key]); 43 | return sprite_sets[key]; 44 | } 45 | -------------------------------------------------------------------------------- /src/glov/client/spritesheet.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { vec2, vec4 } = require('glov/common/vmath.js'); 3 | const { engineStartupFunc } = require('./engine.js'); 4 | const { createSprite } = require('./sprites.js'); 5 | const { textureLoad } = require('./textures.js'); 6 | 7 | const uvs = vec4(0, 0, 1, 1); 8 | const origin_centered = vec2(0.5, 0.5); 9 | const origin_centered_x = vec2(0.5, 0); 10 | let load_opts = {}; 11 | let hit_startup = false; 12 | export function spritesheetTextureOpts(name, opts) { 13 | assert(!hit_startup); 14 | load_opts[name] = opts; 15 | } 16 | export function spritesheetRegister(runtime_data) { 17 | // Create with dummy data, will load later 18 | let texs = []; 19 | let sprite = runtime_data.sprite = createSprite({ texs, uvs }); 20 | runtime_data[`sprite_${runtime_data.name}`] = sprite; 21 | let sprite_centered = runtime_data.sprite_centered = createSprite({ texs, uvs, origin: origin_centered }); 22 | runtime_data[`sprite_${runtime_data.name}_centered`] = sprite_centered; 23 | let sprite_centered_x = runtime_data.sprite_centered_x = createSprite({ texs, uvs, origin: origin_centered_x }); 24 | runtime_data[`sprite_${runtime_data.name}_centered_x`] = sprite_centered_x; 25 | sprite.uidata = sprite_centered.uidata = sprite_centered_x.uidata = runtime_data.uidata; 26 | engineStartupFunc(function () { 27 | hit_startup = true; 28 | let opts = load_opts[runtime_data.name] || {}; 29 | if (runtime_data.layers) { 30 | for (let idx = 0; idx < runtime_data.layers; ++idx) { 31 | let tex = textureLoad({ 32 | ...opts, 33 | url: `img/${runtime_data.name}_${idx}.png`, 34 | }); 35 | texs.push(tex); 36 | } 37 | } else { 38 | let tex = textureLoad({ 39 | ...opts, 40 | url: `img/${runtime_data.name}.png`, 41 | }); 42 | texs.push(tex); 43 | } 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/glov/client/terminal_settings.js: -------------------------------------------------------------------------------- 1 | const engine = require('./engine.js'); 2 | const input = require('./input.js'); 3 | const { KEYS } = require('./input.js'); 4 | const { ansi, padRight, terminalCreate } = require('./terminal.js'); 5 | 6 | let settings_terminal; 7 | let base_terminal; 8 | let settings_up = false; 9 | 10 | const MODEMS = [ 11 | { baud: 2400, label: 'Hayes Smartmodem 2400bps' }, 12 | { baud: 9600, label: 'Motorola V.3225 9600bps' }, 13 | { baud: 28800, label: 'USR Sportster V.34 28.8kbps' }, 14 | { baud: Infinity, label: 'NULL' }, 15 | ]; 16 | 17 | export function terminalSettingsShow() { 18 | settings_up = true; 19 | } 20 | 21 | function settingsOverlay(dt) { 22 | if (input.keyDownEdge(KEYS.O)) { 23 | settings_up = !settings_up; 24 | } 25 | if (settings_up) { 26 | settings_terminal.print({ 27 | fg: 6+8, 28 | x: 10, y: 0, 29 | text: 'TERMINAL OPTIONS' 30 | }); 31 | let modem_idx = 0; 32 | for (let ii = 0; ii < MODEMS.length; ++ii) { 33 | if (MODEMS[ii].baud === base_terminal.baud) { 34 | modem_idx = ii; 35 | } 36 | } 37 | let sel = settings_terminal.menu({ 38 | x: 0, y: 1, 39 | color_sel: { fg: 7, bg: 0 }, 40 | color_unsel: { fg: 7+8, bg: 1 }, 41 | color_execute: { fg: 1, bg: 6+8 }, 42 | pre_sel: ' ■ ', 43 | pre_unsel: ' ', 44 | key: 'terminal_settings', 45 | items: [ 46 | padRight(`Modem: ${MODEMS[modem_idx].label}`, 34), 47 | padRight(`Display: ${engine.getViewportPostprocess() ? 'CRT' : 'LCD'}`, 34), 48 | padRight(`Exit ${ansi.yellow.bright('[O]')}ptions`, 34), 49 | ], 50 | }); 51 | if (sel === 0) { 52 | modem_idx = (modem_idx + settings_terminal.menu_select_delta + MODEMS.length) % MODEMS.length; 53 | base_terminal.baud = MODEMS[modem_idx].baud; 54 | } else if (sel === 1) { 55 | engine.setViewportPostprocess(!engine.getViewportPostprocess()); 56 | } else if (sel === 2 || input.keyDownEdge(KEYS.ESCAPE)) { 57 | settings_up = false; 58 | } 59 | 60 | settings_terminal.render(); 61 | } 62 | } 63 | 64 | export function terminalSettingsInit(terminal) { 65 | base_terminal = terminal; 66 | settings_terminal = terminalCreate({ 67 | auto_scroll: false, 68 | baud: 0, 69 | x: 10 * terminal.char_width, 70 | y: 2 * terminal.char_height, 71 | z: Z.DEBUG, 72 | w: 38, 73 | h: 6, 74 | draw_cursor: false, 75 | }); 76 | settings_terminal.color(7+8, 1); 77 | settings_terminal.clear(); 78 | settings_terminal.color(null, 0); 79 | settings_terminal.fill({ 80 | x: 0, y: settings_terminal.h-1, w: 1, h: 1, 81 | }); 82 | settings_terminal.fill({ 83 | x: settings_terminal.w-1, y: 0, w: 1, h: 1, 84 | }); 85 | settings_terminal.color(8, 0); 86 | settings_terminal.fill({ 87 | x: 1, y: settings_terminal.h-1, w: settings_terminal.w-1, h: 1, 88 | ch: '▓', 89 | }); 90 | settings_terminal.fill({ 91 | x: settings_terminal.w-1, y: 1, w: 1, h: settings_terminal.h-2, 92 | ch: '▓', 93 | }); 94 | 95 | engine.addTickFunc(settingsOverlay); 96 | } 97 | -------------------------------------------------------------------------------- /src/glov/client/test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { setStoragePrefix } from 'glov/client/local_storage'; 3 | import { DataObject } from 'glov/common/types'; 4 | import 'glov/server/test'; 5 | 6 | setStoragePrefix('mock'); 7 | 8 | class MockElementDebug { 9 | } 10 | let debug: MockElementDebug; 11 | 12 | class MockLocation { 13 | protocol = 'mock'; 14 | href = 'mock'; 15 | } 16 | 17 | class MockDocument { 18 | getElementById(id: string): MockElementDebug { 19 | assert.equal(id, 'debug'); 20 | if (!debug) { 21 | debug = new MockElementDebug(); 22 | } 23 | return debug; 24 | } 25 | location = new MockLocation(); 26 | } 27 | 28 | class MockNavigator { 29 | userAgent = 'glov/test/mock'; 30 | } 31 | let glob = global as DataObject; 32 | 33 | assert(!glob.addEventListener); 34 | glob.addEventListener = function () { 35 | // ignore 36 | }; 37 | glob.conf_platform = 'web'; 38 | glob.navigator = new MockNavigator(); 39 | glob.BUILD_TIMESTAMP = String(Date.now()); 40 | 41 | assert(!glob.document); 42 | let document = new MockDocument(); 43 | glob.document = document; 44 | glob.location = document.location; 45 | glob.window = glob; 46 | -------------------------------------------------------------------------------- /src/glov/client/walltime.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | const { msToSS2020 } = require('glov/common/util'); 5 | 6 | const { min } = Math; 7 | 8 | let offs = 0; 9 | function now() { 10 | return Date.now() + offs; 11 | } 12 | module.exports = exports = now; 13 | exports.now = now; 14 | let first = true; 15 | exports.sync = function (server_time) { 16 | if (first) { 17 | offs = server_time - Date.now(); 18 | } else { 19 | offs = min(offs, server_time - Date.now()); 20 | } 21 | }; 22 | exports.seconds = function () { 23 | // Seconds since Jan 1st, 2020 24 | return msToSS2020(now()); 25 | }; 26 | -------------------------------------------------------------------------------- /src/glov/client/words/profanity.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | import { fontSetReplacementChars } from 'glov/client/font'; 5 | import { randFastCreate } from 'glov/client/rand_fast'; 6 | import { getURLBase } from 'glov/client/urlhash'; 7 | import { webFSGetFile } from 'glov/client/webfs'; 8 | import { mashString } from 'glov/common/rand_alea'; 9 | import { 10 | profanityCommonStartup, 11 | profanityFilterCommon, 12 | profanitySetReplacementChars, 13 | } from 'glov/common/words/profanity_common'; 14 | 15 | let non_profanity; 16 | 17 | export function profanityStartup() { 18 | non_profanity = webFSGetFile('words/replacements.txt', 'text').split('\n').filter((a) => a); 19 | profanityCommonStartup(webFSGetFile('words/filter.gkg', 'text'), 20 | webFSGetFile('words/exceptions.txt', 'text')); 21 | 22 | } 23 | 24 | export function profanityStartupLate() { 25 | // Async load of (potentially large) unicode replacement data, after all other loading is finished 26 | let scriptTag = document.createElement('script'); 27 | scriptTag.src = `${getURLBase()}replacement_chars.min.js`; 28 | scriptTag.onload = function () { 29 | if (window.unicode_replacement_chars) { 30 | profanitySetReplacementChars(window.unicode_replacement_chars); 31 | fontSetReplacementChars(window.unicode_replacement_chars); 32 | } 33 | }; 34 | document.getElementsByTagName('head')[0].appendChild(scriptTag); 35 | } 36 | 37 | let rand = randFastCreate(); 38 | 39 | let last_word; 40 | function randWord() { 41 | if (last_word === -1 || non_profanity.length === 1) { 42 | last_word = rand.range(non_profanity.length); 43 | } else { 44 | let choice = rand.range(non_profanity.length - 1); 45 | last_word = choice < last_word ? choice : choice + 1; 46 | } 47 | return non_profanity[last_word]; 48 | } 49 | 50 | export function profanityFilter(user_str) { 51 | last_word = -1; 52 | rand.seed = mashString(user_str); 53 | return profanityFilterCommon(user_str, randWord); 54 | } 55 | -------------------------------------------------------------------------------- /src/glov/client/words/replacements.txt: -------------------------------------------------------------------------------- 1 | blast 2 | rust 3 | spark 4 | burn 5 | bloop 6 | derp 7 | spice 8 | bloom 9 | -------------------------------------------------------------------------------- /src/glov/common/base32.js: -------------------------------------------------------------------------------- 1 | /* eslint indent:off, no-multi-spaces:off */ 2 | const { floor, random } = Math; 3 | 4 | // From Crockford's Base32, no confusing letters/numbers 5 | let to_base_32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; 6 | let char_table = to_base_32.split(''); 7 | 8 | // Tables including lower case and confusing letters (L, l, i, I, o, O) 9 | 10 | // let to_binary = [ 11 | // -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 12 | // -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 13 | // -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 14 | // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1, -1,-1,-1,-1, 15 | // -1,10,11,12, 13,14,15,16, 17, 1,18,19, 1,20,21, 0, 16 | // 22,23,24,25, 26,-1,27,28, 29,30,31,-1, -1,-1,-1,-1, 17 | // -1,10,11,12, 13,14,15,16, 17, 1,18,19, 1,20,21, 0, 18 | // 22,23,24,25, 26,-1,27,28, 29,30,31,-1, -1,-1,-1,-1 19 | // ]; 20 | let to_cannon_table = [ 21 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 0, 0, 0, 0, 0, 0, 25 | 0, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', '1', 'J', 'K', '1', 'M', 'N', '0', 26 | 'P', 'Q', 'R', 'S', 'T', 0, 'V', 'W', 'X', 'Y', 'Z', 0, 0, 0, 0, 0, 27 | 0, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', '1', 'J', 'K', '1', 'M', 'N', '0', 28 | 'P', 'Q', 'R', 'S', 'T', 0, 'V', 'W', 'X', 'Y', 'Z', 29 | ]; 30 | // Also strip out ignorable characters 31 | (function () { 32 | let strip = ' -–.,\t\n\r'; 33 | for (let ii = 0; ii < strip.length; ++ii) { 34 | to_cannon_table[strip.charCodeAt(ii)] = ''; 35 | } 36 | }()); 37 | // let to_cannon = { 38 | // '0':'0', '1':'1', '2':'2', '3':'3', '4':'4', '5':'5', '6':'6', '7':'7', '8':'8', '9':'9', 39 | // A:'A', B:'B', C:'C', D:'D', E:'E', F:'F', G:'G', H:'H', I:'1', 40 | // J:'J', K:'K', L:'1', M:'M', N:'N', O:'0', P:'P', Q:'Q', R:'R', 41 | // S:'S', T:'T', V:'V', W:'W', X:'X', Y:'Y', Z:'Z', 42 | // a:'A', b:'B', c:'C', d:'D', e:'E', f:'F', g:'G', h:'H', i:'1', 43 | // j:'J', k:'K', l:'1', m:'M', n:'N', o:'0', p:'P', q:'Q', r:'R', 44 | // s:'S', t:'T', v:'V', w:'W', x:'X', y:'Y', z:'Z', 45 | // }; 46 | 47 | export function cannonize(str) { 48 | let ret = []; 49 | for (let ii = 0; ii < str.length; ++ii) { 50 | let new_char = to_cannon_table[str.charCodeAt(ii)]; 51 | if (new_char === '') { 52 | // skipable char 53 | continue; 54 | } else if (!new_char) { 55 | // invalid char 56 | return null; 57 | } 58 | ret.push(new_char); 59 | } 60 | return ret.join(''); 61 | } 62 | 63 | export function gen(length) { 64 | let ret = []; 65 | for (let ii = 0; ii < length; ++ii) { 66 | ret.push(char_table[floor(random() * 32)]); 67 | } 68 | return ret.join(''); 69 | } 70 | 71 | export function addDashes(str) { 72 | let segs = floor(str.length / 4); 73 | let ret = []; 74 | for (let ii = 0; ii < segs; ++ii) { 75 | ret.push(str.slice(ii*4, ii*4+4)); 76 | } 77 | return ret.join('-'); 78 | } 79 | -------------------------------------------------------------------------------- /src/glov/common/base64.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | /* eslint no-bitwise:off */ 4 | 5 | 6 | // Encoding is fastest with non-native calls: http://jsperf.com/base64-encode 7 | // Decoding is fastest using window.btoa: http://jsperf.com/base64-decode 8 | 9 | const { floor } = Math; 10 | 11 | const chr_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split(''); 12 | const PAD = '='; 13 | 14 | // dv is a DataView with a .u8 property 15 | function encode(dv, offset, length) { 16 | let data = dv.u8; 17 | let result = ''; 18 | let i; 19 | let effi; 20 | // Convert every three bytes to 4 ascii characters. 21 | for (i = 0; i < (length - 2); i += 3) { 22 | effi = offset + i; 23 | result += chr_table[data[effi] >> 2]; 24 | result += chr_table[((data[effi] & 0x03) << 4) + (data[effi + 1] >> 4)]; 25 | result += chr_table[((data[effi + 1] & 0x0f) << 2) + (data[effi + 2] >> 6)]; 26 | result += chr_table[data[effi + 2] & 0x3f]; 27 | } 28 | 29 | // Convert the remaining 1 or 2 bytes, pad out to 4 characters. 30 | if (length % 3) { 31 | i = length - (length % 3); 32 | effi = offset + i; 33 | result += chr_table[data[effi] >> 2]; 34 | if ((length % 3) === 2) { 35 | result += chr_table[((data[effi] & 0x03) << 4) + (data[effi + 1] >> 4)]; 36 | result += chr_table[(data[effi + 1] & 0x0f) << 2]; 37 | result += PAD; 38 | } else { 39 | result += chr_table[(data[effi] & 0x03) << 4]; 40 | result += PAD + PAD; 41 | } 42 | } 43 | 44 | return result; 45 | } 46 | 47 | function decodeNativeBrowser(data, allocator) { 48 | let str = window.atob(data); 49 | let len = str.length; 50 | let dv = allocator(len); 51 | let u8 = dv.u8; 52 | for (let ii = 0; ii < len; ++ii) { 53 | u8[ii] = str.charCodeAt(ii); 54 | } 55 | dv.decode_size = len; 56 | return dv; 57 | } 58 | 59 | function encodeNativeNode(dv, offset, length) { 60 | // Allocates a Buffer() object each time - could have our allocDataView do that if needed for perf 61 | return Buffer.from(dv.buffer).toString('base64', offset, offset + length); 62 | } 63 | // Faster, but uses an internal function that might break: 64 | // function encodeNativeNode(dv, offset, length) { 65 | // return Buffer.prototype.base64Slice.call(dv.u8, offset, offset + length); 66 | // } 67 | 68 | function decodeNativeNode(data, allocator) { 69 | let buffer_len = (data.length >> 2) * 3 + floor((data.length % 4) / 1.5); 70 | let dv = allocator(buffer_len); 71 | let buffer = Buffer.from(dv.buffer); 72 | dv.decode_size = buffer.write(data, 'base64'); 73 | return dv; 74 | } 75 | 76 | const BROWSER = typeof window !== 'undefined'; 77 | 78 | // string -> Uint8Array or Buffer 79 | exports.base64Decode = BROWSER ? decodeNativeBrowser : decodeNativeNode; 80 | // Uint8Array or Buffer -> string 81 | exports.base64Encode = BROWSER ? encode : encodeNativeNode; 82 | 83 | exports.base64CharTable = chr_table; 84 | -------------------------------------------------------------------------------- /src/glov/common/crc32.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | // Possibly originally from PNG Specification Appendix: https://www.w3.org/TR/PNG-CRCAppendix.html 4 | 5 | // Table of CRCs of all 8-bit messages. 6 | let crc_table = new Array(256); 7 | 8 | // Make the table for a fast CRC. 9 | (function () { 10 | for (let n = 0; n < 256; n++) { 11 | let c = n; 12 | for (let k = 0; k < 8; k++) { 13 | if (c & 1) { 14 | c = -306674912 ^ (c >>> 1); 15 | } else { 16 | c >>>= 1; 17 | } 18 | } 19 | crc_table[n] = c; 20 | } 21 | }()); 22 | 23 | 24 | /* Update a running CRC with the bytes buf[0..len-1]--the CRC 25 | should be initialized to all 1's, and the transmitted value 26 | is the 1's complement of the final running CRC (see the 27 | crc32() routine below)). */ 28 | 29 | function update_crc(crc, buf, len) { 30 | for (let n = 0; n < len; n++) { 31 | crc = crc_table[(crc ^ buf[n]) & 0xff] ^ (crc >>> 8); 32 | } 33 | return crc; 34 | } 35 | 36 | // Return the CRC of the bytes buf[0..len-1]. 37 | function crc32(buf, len) { 38 | len = len || buf.length; 39 | return (update_crc(0xffffffff, buf, len) ^ 0xffffffff) >>> 0; 40 | } 41 | module.exports = crc32; 42 | module.exports.crc32 = crc32; 43 | -------------------------------------------------------------------------------- /src/glov/common/data_error.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | export type DataError = { 4 | msg: string; 5 | per_frame?: boolean; 6 | }; 7 | 8 | let on_error: null | ((err: DataError) => void) = null; 9 | let enabled = false; 10 | let error_queue: DataError[] = []; 11 | let msgs_in_queue: Partial> = Object.create(null); 12 | export function dataErrorEx(err: DataError): void { 13 | if (!enabled) { 14 | return; 15 | } 16 | if (err.per_frame) { 17 | if (msgs_in_queue[err.msg]) { 18 | // Duplicate, silently ignore 19 | return; 20 | } 21 | msgs_in_queue[err.msg] = true; 22 | } 23 | if (on_error) { 24 | on_error(err); 25 | } 26 | error_queue.push(err); 27 | if (error_queue.length > 25) { 28 | let removed = error_queue.splice(0, 1)[0]; 29 | if (removed.per_frame) { 30 | delete msgs_in_queue[removed.msg]; 31 | } 32 | } 33 | } 34 | 35 | export function dataError(msg: string): void { 36 | dataErrorEx({ msg }); 37 | } 38 | 39 | export function dataErrorQueueEnable(val: boolean): void { 40 | enabled = val; 41 | } 42 | 43 | export function dataErrorOnError(cb: (err: DataError) => void): void { 44 | assert(!on_error); 45 | on_error = cb; 46 | } 47 | 48 | export function dataErrorQueueGet(): DataError[] { 49 | return error_queue; 50 | } 51 | 52 | export function dataErrorQueueClear(): void { 53 | error_queue = []; 54 | msgs_in_queue = Object.create(null); 55 | } 56 | -------------------------------------------------------------------------------- /src/glov/common/enums.ts: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | export const PRESENCE_OFFLINE = 0; // for invisible, etc 5 | export const PRESENCE_ACTIVE = 1; 6 | export const PRESENCE_INACTIVE = 2; 7 | 8 | export type NumberEnum = Record & Partial>; 9 | export type StringEnum = Record; 10 | 11 | export function getStringEnumValues(e: StringEnum): V[] { 12 | return Object.values(e); 13 | } 14 | export function isValidNumberEnumKey(e: NumberEnum, k: string): k is K { 15 | return typeof e[k] === 'number'; 16 | } 17 | export function isValidStringEnumKey(e: StringEnum, k: string): k is K { 18 | return k in e; 19 | } 20 | export function isValidStringEnumValue( 21 | e: StringEnum, 22 | v: string | undefined | null, 23 | ): v is V { 24 | for (let key in e) { 25 | if (e[key] === v) { 26 | return true; 27 | } 28 | } 29 | return false; 30 | } 31 | -------------------------------------------------------------------------------- /src/glov/common/external_users_common.ts: -------------------------------------------------------------------------------- 1 | // Errors 2 | export const ERR_INVALID_DATA = 'ERR_INVALID_DATA'; 3 | export const ERR_INVALID_PROVIDER = 'ERR_INVALID_PROVIDER'; 4 | export const ERR_UNAUTHORIZED = 'ERR_UNAUTHORIZED'; 5 | export const ERR_NO_USER_ID = 'ERR_NO_USER_ID'; 6 | export const ERR_UNCONFIRMED_EMAIL = 'ERR_UNCONFIRMED_EMAIL'; 7 | export const ERR_SERVER = 'ERR_SERVER'; 8 | export const ERR_EMAIL_ALREADY_USED = 'ERR_EMAIL_ALREADY_USED'; 9 | export const ERR_NOT_AVAILABLE = 'ERR_NOT_AVAILABLE'; 10 | -------------------------------------------------------------------------------- /src/glov/common/fifo.ts: -------------------------------------------------------------------------------- 1 | // FIFO queue implemented as a doubly-linked list (e.g. allows removal of any element) 2 | 3 | import assert from 'assert'; 4 | 5 | let last_queue_id = 0; 6 | 7 | type FIFONode = Partial>; 8 | 9 | class FIFOImpl { 10 | private head: T | null = null; 11 | private tail: T | null = null; 12 | private count = 0; 13 | private nkey = `n${++last_queue_id}`; 14 | private pkey = `p${last_queue_id}`; 15 | 16 | length(): number { 17 | return this.count; 18 | } 19 | size(): number { 20 | return this.count; 21 | } 22 | 23 | add(item: T): void { 24 | let node = item as FIFONode; 25 | assert(!node[this.nkey]); 26 | assert(!node[this.pkey]); 27 | if (this.tail) { 28 | node[this.pkey] = this.tail; 29 | } 30 | if (this.tail) { 31 | (this.tail as FIFONode)[this.nkey] = item; 32 | this.tail = item; 33 | } else { 34 | this.head = this.tail = item; 35 | } 36 | ++this.count; 37 | } 38 | 39 | remove(item: T): void { 40 | let node = item as FIFONode; 41 | let prev = node[this.pkey]; 42 | let next = node[this.nkey]; 43 | if (prev) { 44 | (prev as FIFONode)[this.nkey] = next; 45 | delete node[this.pkey]; 46 | } else { 47 | assert.equal(this.head, item); 48 | assert(item !== next); 49 | this.head = next || null; 50 | } 51 | if (next) { 52 | (next as FIFONode)[this.pkey] = prev; 53 | delete node[this.nkey]; 54 | } else { 55 | assert.equal(this.tail, item); 56 | this.tail = prev || null; 57 | } 58 | --this.count; 59 | } 60 | 61 | contains(item: T): boolean { 62 | return this.head === item || (item as FIFONode)[this.pkey] !== undefined; 63 | } 64 | 65 | peek(): T | null { 66 | return this.head; 67 | } 68 | 69 | pop(): T | null { 70 | if (!this.count) { 71 | return null; 72 | } 73 | assert(this.head); 74 | let head = this.head; 75 | this.remove(head); 76 | return head; 77 | } 78 | } 79 | export type FIFO = FIFOImpl; 80 | 81 | export function fifoCreate(): FIFO { 82 | return new FIFOImpl(); 83 | } 84 | -------------------------------------------------------------------------------- /src/glov/common/friends_data.ts: -------------------------------------------------------------------------------- 1 | export enum FriendStatus { 2 | Added = 1, 3 | AddedAuto = 2, 4 | Removed = 3, 5 | Blocked = 4, 6 | } 7 | 8 | export interface FriendData { 9 | status: FriendStatus; 10 | ids?: Record; 11 | } 12 | 13 | export type FriendsData = Record; 14 | -------------------------------------------------------------------------------- /src/glov/common/fsapi.ts: -------------------------------------------------------------------------------- 1 | 2 | export type FilewatchCB = (filename: string) => void | boolean; 3 | 4 | export type FSAPI = { 5 | getFileNames(directory: string): string[]; 6 | getFile(filename: string, encoding: 'jsobj'): T; 7 | getFile(filename: string, encoding: 'buffer'): Buffer; 8 | filewatchOn(ext_or_search: RegExp | string, cb: FilewatchCB): void; 9 | }; 10 | 11 | // filename from webfs or serverfs, convert to same base name 12 | export function fileBaseName(filename: string): string { 13 | let idx = filename.lastIndexOf('/'); 14 | if (idx !== -1) { 15 | filename = filename.slice(idx + 1); 16 | } 17 | idx = filename.indexOf('.'); 18 | if (idx !== -1) { 19 | filename = filename.slice(0, idx); 20 | } 21 | return filename; 22 | } 23 | -------------------------------------------------------------------------------- /src/glov/common/gl-matrix-types.d.ts: -------------------------------------------------------------------------------- 1 | // Reference if needed: 2 | // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a56cefca02ee51c96bd57c70eca5e109c4290c15/types/gl-matrix/index.d.ts 3 | // (Not quite the same type names we use, though) 4 | 5 | /* eslint-disable no-duplicate-imports */ 6 | 7 | declare module 'gl-mat3/fromMat4' { 8 | import type { Mat3, Mat4 } from 'glov/common/vmath'; 9 | export default function fromMat4(a: Readonly): Mat3; 10 | } 11 | declare module 'gl-mat4/copy' { 12 | import type { Mat4 } from 'glov/common/vmath'; 13 | export default function copy(out: Mat4, a: Readonly): Mat4; 14 | } 15 | declare module 'gl-mat4/invert' { 16 | import type { Mat4 } from 'glov/common/vmath'; 17 | export default function invert(out: Mat4, a: Readonly): Mat4; 18 | } 19 | declare module 'gl-mat4/lookAt' { 20 | import type { Mat4, ROVec3 } from 'glov/common/vmath'; 21 | export default function lookAt(out: Mat4, eye: ROVec3, center: ROVec3, up: ROVec3): Mat4; 22 | } 23 | declare module 'gl-mat4/multiply' { 24 | import type { Mat4 } from 'glov/common/vmath'; 25 | export default function multiply(out: Mat4, a: Readonly, b: Readonly): Mat4; 26 | } 27 | declare module 'gl-mat4/perspective' { 28 | import type { Mat4 } from 'glov/common/vmath'; 29 | export default function perspective(out: Mat4, fov_y: number, aspect: number, znear: number, zfar: number): Mat4; 30 | } 31 | declare module 'gl-mat4/translate' { 32 | import type { Mat4, ROVec3 } from 'glov/common/vmath'; 33 | export default function translate(out: Mat4, a: Readonly, v: ROVec3): Mat4; 34 | } 35 | declare module 'gl-mat4/transpose' { 36 | import type { Mat4 } from 'glov/common/vmath'; 37 | export default function transpose(out: Mat4, a: Readonly): Mat4; 38 | } 39 | -------------------------------------------------------------------------------- /src/glov/common/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'glov/common/global' { 2 | global { 3 | 4 | /** 5 | * From: https://www.typescriptlang.org/docs/handbook/mixins.html 6 | * A constructor for a type which extends T 7 | * Note: `typeof T` is usually a better choice (doesn't lose static methods) 8 | */ 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 10 | type Constructor = new (...args: any[]) => T; 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/glov/common/packet.d.ts: -------------------------------------------------------------------------------- 1 | import { NetErrorCallback } from './types'; 2 | 3 | export const PACKET_DEBUG = 1; 4 | 5 | type PacketFlags = 0 | typeof PACKET_DEBUG; 6 | 7 | export function packetDefaultFlags(): PacketFlags; 8 | export function packetEnableDebug(enable: true): void; 9 | 10 | export interface Packet { 11 | readU8(): number; 12 | writeU8(value: number): void; 13 | readU32(): number; 14 | writeU32(value: number): void; 15 | readInt(): number; 16 | writeInt(value: number): void; 17 | readFloat(): number; 18 | writeFloat(value: number): void; 19 | readString(): string; 20 | writeString(value: string): void; 21 | readAnsiString(): string; 22 | writeAnsiString(value: string): void; 23 | readJSON(): T; 24 | readJSON(): unknown; 25 | writeJSON(value: T): void; 26 | writeJSON(value: unknown): void; 27 | readBool(): boolean; 28 | writeBool(value: boolean): void; 29 | readBuffer(do_copy: boolean): Uint8Array; 30 | writeBuffer(value: Uint8Array): void; 31 | appendBuffer(value: Uint8Array): void; 32 | 33 | append(other: Packet): void; 34 | appendRemaining(other: Packet): void; 35 | send(resp_func?: NetErrorCallback): void; 36 | ended(): boolean; 37 | updateFlags(flags: number): void; 38 | readFlags(): void; 39 | writeFlags(): void; 40 | getFlags(): number; 41 | getBuffer(): Uint8Array; 42 | getBufferLen(): number; 43 | getInternalFlags(): number; 44 | getOffset(): number; 45 | getRefCount(): number; 46 | makeReadable(): void; 47 | pool(): void; 48 | ref(): void; 49 | seek(offs: number): void; 50 | totalSize(): number; 51 | 52 | no_local_bypass?: true; // Internal-ish: poked by channel_server.js 53 | } 54 | 55 | export function packetCreate(flags?: PacketFlags, init_size?: number): Packet; 56 | export function packetFromBuffer(buf: Uint8Array | Buffer, buf_len: number, need_copy?: boolean): Packet; 57 | export function packetFromJSON(js_obj: unknown): Packet; 58 | export function isPacket(thing: unknown): thing is Packet; 59 | export function packetSizeInt(v: number): number; 60 | export function packetSizeAnsiString(v: string): number; 61 | export type PacketSpeculativeReadRet = { v: number; offs: number }; 62 | export function packetReadIntFromBuffer(buf: Buffer, offs: number, buf_len: number): PacketSpeculativeReadRet | null; 63 | -------------------------------------------------------------------------------- /src/glov/common/perfcounters.js: -------------------------------------------------------------------------------- 1 | const BUCKET_TIME = 10000; 2 | const NUM_BUCKETS = 5; 3 | 4 | let counters = { time_start: Date.now() }; 5 | let hist = [counters]; 6 | let countdown = BUCKET_TIME; 7 | 8 | export function perfCounterAdd(key) { 9 | counters[key] = (counters[key] || 0) + 1; 10 | } 11 | 12 | export function perfCounterAddValue(key, value) { 13 | counters[key] = (counters[key] || 0) + value; 14 | } 15 | 16 | export function perfCounterTick(dt, log) { 17 | countdown -= dt; 18 | if (countdown <= 0) { 19 | countdown = BUCKET_TIME; 20 | if (hist.length === NUM_BUCKETS) { 21 | hist.splice(0, 1); 22 | } 23 | let now = Date.now(); 24 | counters.time_end = now; 25 | if (log) { 26 | log(counters); 27 | } 28 | counters = {}; 29 | counters.time_start = now; 30 | hist.push(counters); 31 | } 32 | } 33 | 34 | export function perfCounterHistory() { 35 | return hist; 36 | } 37 | -------------------------------------------------------------------------------- /src/glov/common/platform.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import type { DataObject } from './types'; 4 | 5 | export type PlatformID = string; 6 | 7 | export type PlatformDef = { 8 | // devmode: if `auto`, will enable MODE_DEVELOPMENT if the host starts with `localhost` 9 | devmode: 'auto' | 'on' | 'off'; 10 | // reload: whether or not we can call document.reload() to reload the page 11 | reload: boolean; 12 | // reload_updates: whether or not calling document.reload() will cause us to get an updated version of the client 13 | reload_updates: boolean; 14 | }; 15 | 16 | let platforms: Partial> = Object.create(null); 17 | 18 | let too_late_to_register = false; 19 | 20 | export function platformRegister(id: PlatformID, def: PlatformDef): void { 21 | assert(!too_late_to_register); 22 | assert(!platforms[id]); 23 | platforms[id] = def; 24 | } 25 | 26 | export function platformGetValidIDs(): PlatformID[] { 27 | return Object.keys(platforms); 28 | } 29 | export function platformIsValid(v: string | undefined | null): boolean { 30 | too_late_to_register = true; // all registering must be done before the first querying 31 | return Boolean(typeof v === 'string' && platforms[v]); 32 | } 33 | let parameter_overrides: DataObject = Object.create(null); 34 | export function platformParameter(platform: PlatformID, parameter: T): PlatformDef[T]; 35 | export function platformParameter(platform: PlatformID, parameter: string): unknown { 36 | let override = parameter_overrides[parameter]; 37 | if (override !== undefined) { 38 | return override; 39 | } 40 | let platdef = platforms[platform]; 41 | assert(platdef); 42 | return (platdef as DataObject)[parameter]; 43 | } 44 | 45 | export function platformOverrideParameter(parameter: T, value: PlatformDef[T]): void; 46 | export function platformOverrideParameter(parameter: string, value: unknown): void { 47 | parameter_overrides[parameter] = value; 48 | } 49 | 50 | platformRegister('web', { 51 | devmode: 'auto', 52 | reload: true, 53 | reload_updates: true, 54 | }); 55 | -------------------------------------------------------------------------------- /src/glov/common/texpack_common.js: -------------------------------------------------------------------------------- 1 | // bitmask 2 | exports.FORMAT_PACK = 1<<0; 3 | exports.FORMAT_PNG = 1<<1; 4 | exports.TEXPACK_MAGIC = 0x8F49A352; 5 | -------------------------------------------------------------------------------- /src/glov/common/tiny-events.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | /* eslint prefer-rest-params:off, no-underscore-dangle:off */ 4 | 5 | const assert = require('assert'); 6 | 7 | function EventEmitter() { 8 | this._listeners = {}; 9 | } 10 | 11 | module.exports = EventEmitter; 12 | // Also "export" `EventEmitter` so you can do `import { EventEmitter } from 'tiny-events.js'` but 13 | // not pollute the prototype (would show up for all classes that `extend EventEmitter`). 14 | Object.defineProperty(module.exports, 'EventEmitter', { 15 | value: EventEmitter, 16 | enumerable: false, 17 | }); 18 | 19 | function addListener(ee, type, fn, once) { 20 | assert(typeof fn === 'function'); 21 | let arr = ee._listeners[type]; 22 | if (!arr) { 23 | arr = ee._listeners[type] = []; 24 | } 25 | arr.push({ 26 | once, 27 | fn, 28 | }); 29 | } 30 | 31 | EventEmitter.prototype.hasListener = function (type, fn) { 32 | let arr = this._listeners[type]; 33 | if (!arr) { 34 | return false; 35 | } 36 | for (let ii = 0; ii < arr.length; ++ii) { 37 | if (arr[ii].fn === fn) { 38 | return true; 39 | } 40 | } 41 | return false; 42 | }; 43 | 44 | EventEmitter.prototype.on = function (type, fn) { 45 | addListener(this, type, fn, 0); 46 | return this; 47 | }; 48 | 49 | EventEmitter.prototype.once = function (type, fn) { 50 | addListener(this, type, fn, 1); 51 | return this; 52 | }; 53 | 54 | EventEmitter.prototype.removeListener = function (type, fn) { 55 | let arr = this._listeners[type]; 56 | assert(arr); 57 | for (let ii = 0; ii < arr.length; ++ii) { 58 | if (arr[ii].fn === fn) { 59 | arr.splice(ii, 1); 60 | return this; 61 | } 62 | } 63 | assert(false); // expected to find the listener! 64 | return this; 65 | }; 66 | 67 | function filterNotOnce(elem) { 68 | return !elem.once; 69 | } 70 | 71 | EventEmitter.prototype.emit = function (type, ...args) { 72 | let arr = this._listeners[type]; 73 | if (!arr) { 74 | return false; 75 | } 76 | 77 | let any = false; 78 | let any_once = false; 79 | for (let ii = 0; ii < arr.length; ++ii) { 80 | let elem = arr[ii]; 81 | any = true; 82 | elem.fn(...args); 83 | if (elem.once) { 84 | any_once = true; 85 | } 86 | } 87 | if (any_once) { 88 | this._listeners[type] = arr.filter(filterNotOnce); 89 | } 90 | 91 | return any; 92 | }; 93 | 94 | // Aliases 95 | // EventEmitter.prototype.addListener = EventEmitter.prototype.on; 96 | -------------------------------------------------------------------------------- /src/glov/common/verify.ts: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | // Like assert(0), but return the value, so the throw can be disabled if the 5 | // calling code handles failure. Can replace `verify(foo)` with `(foo)` at 6 | // build time in production builds. 7 | 8 | let should_throw = true; 9 | 10 | function verify(exp: T | undefined | null | false, msg?: string): T { 11 | if (!exp && should_throw) { 12 | throw new Error(`Assertion failed${msg ? `: ${msg}` : ''}`); 13 | } 14 | return exp as T; 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-namespace 18 | namespace verify { 19 | export const ok = verify; 20 | 21 | export function equal(a: T, b: T): boolean { 22 | if (a === b) { 23 | return true; 24 | } 25 | if (should_throw) { 26 | throw new Error(`Assertion failed: "${a}"==="${b}"`); 27 | } 28 | return false; 29 | } 30 | 31 | export function dothrow(doit: boolean): void { 32 | should_throw = doit; 33 | } 34 | 35 | export function shouldThrow(): boolean { 36 | return should_throw; 37 | } 38 | } 39 | 40 | export = verify; 41 | -------------------------------------------------------------------------------- /src/glov/common/words/exceptions.txt: -------------------------------------------------------------------------------- 1 | spices 2 | spicy 3 | spiky 4 | Anaïs 5 | Anaís 6 | -------------------------------------------------------------------------------- /src/glov/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glov", 3 | "version": "0.0.1", 4 | "description": "Placeholder package file for resolving local file path module" 5 | } 6 | -------------------------------------------------------------------------------- /src/glov/server/channel_data_differ.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { max } = Math; 3 | const { typeof2 } = require('glov/common/differ'); 4 | const { clone } = require('glov/common/util.js'); 5 | 6 | function walk(differ, worker, path_pre, data1, data2) { 7 | let type = typeof2(data1); 8 | if (type !== typeof2(data2)) { 9 | // Types changed, probably one is now undefined or null 10 | worker.setChannelDataBatched(path_pre, data2); 11 | return; 12 | } 13 | 14 | if (type === 'object') { 15 | let seen = Object.create(null); 16 | for (let key in data1) { 17 | seen[key] = true; 18 | // deletes, modifications 19 | walk(differ, worker, `${path_pre}.${key}`, data1[key], data2[key]); 20 | } 21 | for (let key in data2) { 22 | if (!seen[key]) { 23 | // additions 24 | walk(differ, worker, `${path_pre}.${key}`, data1[key], data2[key]); 25 | } 26 | } 27 | } else if (type === 'array') { 28 | let maxlen = max(data1.length, data2.length); 29 | for (let ii = 0; ii < maxlen; ++ii) { 30 | walk(differ, worker, `${path_pre}.${ii}`, data1[ii], data2[ii]); 31 | } 32 | if (data2.length < data1.length) { 33 | worker.setChannelDataBatched(`${path_pre}.length`, data2.length); 34 | } 35 | } else { 36 | // string, number, boolean 37 | if (data1 !== data2) { 38 | worker.setChannelDataBatched(path_pre, data2); 39 | } 40 | } 41 | } 42 | 43 | class ChannelDataDiffer { 44 | constructor(channel_worker) { 45 | this.worker = channel_worker; 46 | this.started = false; 47 | this.data_pre = null; 48 | } 49 | 50 | start() { 51 | // assert(!this.started); 52 | this.started = true; 53 | 54 | let { worker } = this; 55 | this.data_pre = clone(worker.data.public); 56 | } 57 | 58 | end() { 59 | assert(this.started); 60 | 61 | let { worker } = this; 62 | 63 | // worker.setChannelData('public', worker.data.public); 64 | assert(!worker.batched_sets); 65 | walk(this, worker, 'public', this.data_pre, worker.data.public); 66 | 67 | if (worker.batched_sets) { // any were emitted 68 | worker.setChannelDataBatchedFlush(); 69 | } 70 | 71 | this.reset(); 72 | } 73 | 74 | reset() { 75 | this.started = false; 76 | this.data_pre = null; 77 | } 78 | } 79 | 80 | export function channelDataDifferCreate(channel_worker) { 81 | return new ChannelDataDiffer(channel_worker); 82 | } 83 | -------------------------------------------------------------------------------- /src/glov/server/channel_server_worker.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { ChannelWorker } from './channel_worker.js'; 3 | import { serverGlobalsHandleChannelData, serverGlobalsInit } from './server_globals'; 4 | 5 | export class ChannelServerWorker extends ChannelWorker { 6 | constructor(channel_server, channel_id, channel_data) { 7 | super(channel_server, channel_id, channel_data); 8 | serverGlobalsInit(this); 9 | channel_server.whenReady(this.subscribeOther.bind(this, 'global.global', ['*'])); 10 | } 11 | 12 | // data is a { key, value } pair of what has changed 13 | onApplyChannelData(source, data) { 14 | if (source.type === 'global') { 15 | serverGlobalsHandleChannelData(data.key, data.value); 16 | } 17 | } 18 | 19 | // data is the channel's entire (public) data sent in response to a subscribe 20 | onChannelData(source, data) { 21 | if (source.type === 'global') { 22 | serverGlobalsHandleChannelData('', data); 23 | } 24 | } 25 | 26 | } 27 | // Returns a function that forwards to a method of the same name on the ChannelServer 28 | function channelServerBroadcast(name) { 29 | return (ChannelServerWorker.prototype[name] = function (src, data, resp_func) { 30 | assert(!resp_func.expecting_response); // this is a broadcast 31 | this.channel_server[name](data); 32 | }); 33 | } 34 | function channelServerHandler(name) { 35 | return (ChannelServerWorker.prototype[name] = function (src, data, resp_func) { 36 | this.channel_server[name](data, resp_func); 37 | }); 38 | } 39 | 40 | ChannelServerWorker.prototype.no_datastore = true; // No datastore instances created here as no persistence is needed 41 | 42 | export function channelServerWorkerInit(channel_server) { 43 | channel_server.registerChannelWorker('channel_server', ChannelServerWorker, { 44 | autocreate: false, 45 | subid_regex: /^[a-zA-Z0-9-]+$/, 46 | handlers: { 47 | worker_create: channelServerHandler('handleWorkerCreate'), 48 | master_startup: channelServerBroadcast('handleMasterStartup'), 49 | master_stats: channelServerBroadcast('handleMasterStats'), 50 | restarting: channelServerBroadcast('handleRestarting'), 51 | chat_broadcast: channelServerBroadcast('handleChatBroadcast'), 52 | ping: channelServerBroadcast('handlePing'), 53 | eat_cpu: channelServerHandler('handleEatCPU'), 54 | }, 55 | filters: { 56 | // note: these do *not* override the one on ChannelWorker.prototype, both 57 | // would be called via `filters` (if maintain_client_list were set) 58 | channel_data: ChannelServerWorker.prototype.onChannelData, 59 | apply_channel_data: ChannelServerWorker.prototype.onApplyChannelData, 60 | }, 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /src/glov/server/class_proxy.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | /* 3 | * Creates an object which can be used as a proxy for a class until the class 4 | * constructor and prototype are set up. All static members added and all 5 | * things added to `prototype` will be added to the eventual class, and all 6 | * other static property reads are assumed to be calling static functions, 7 | * which will be deferred and called when the class is realized. 8 | */ 9 | function classProxyCreate() { 10 | let prototype = {}; 11 | let static_data = {}; 12 | let queued_funcs = []; 13 | let expected_calls = 0; 14 | 15 | function finalize(target_ctor) { 16 | assert(!expected_calls); // Otherwise, something referenced was a function not called (or called twice?) 17 | for (let key in static_data) { 18 | assert(!target_ctor[key], `Duplicate class field ${key} defined in two files`); 19 | target_ctor[key] = static_data[key]; 20 | } 21 | for (let key in prototype) { 22 | assert(!target_ctor.prototype[key], `Duplicate class function ${key} defined in two files`); 23 | target_ctor.prototype[key] = prototype[key]; 24 | } 25 | for (let ii = 0; ii < queued_funcs.length; ++ii) { 26 | let pair = queued_funcs[ii]; 27 | target_ctor[pair.func_name].apply(target_ctor, pair.args); 28 | } 29 | } 30 | 31 | return new Proxy({}, { 32 | get: function (target, prop) { 33 | if (prop === 'prototype') { 34 | return prototype; 35 | } else if (prop === 'finalize') { 36 | return finalize; 37 | } 38 | // Otherwise, assume function, delay calling until later 39 | ++expected_calls; 40 | return function (...args) { 41 | --expected_calls; 42 | queued_funcs.push({ func_name: prop, args }); 43 | }; 44 | }, 45 | set: function (target, prop, value) { 46 | assert(prop !== 'prototype'); 47 | // Setting static data, also fine 48 | static_data[prop] = value; 49 | return true; 50 | }, 51 | }); 52 | } 53 | module.exports = classProxyCreate; 54 | -------------------------------------------------------------------------------- /src/glov/server/data_store_image.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | const mkdirp = require('mkdirp'); 5 | const path = require('path'); 6 | 7 | const valid_path_regex = /[a-zA-Z0-9.-_]+/; 8 | class DataStoreImage { 9 | constructor(store_path, subdir) { 10 | this.path = path.join(store_path, subdir).replace(/\\/g, '/'); 11 | this.subdir = subdir; 12 | mkdirp.sync(this.path); 13 | } 14 | 15 | // cb(err, url) 16 | set(key, buffer, mime_type, cb) { 17 | assert(buffer instanceof Uint8Array); // Probably Uint8Array or Buffer 18 | assert(key.match(valid_path_regex)); 19 | let disk_path = path.join(this.path, key); 20 | let serve_path = `${this.subdir}/${key}`; 21 | fs.writeFile(disk_path, buffer, function (err) { 22 | cb(err, serve_path); 23 | }); 24 | } 25 | 26 | delete(key, cb) { 27 | let disk_path = path.join(this.path, key); 28 | fs.unlink(disk_path, cb); 29 | } 30 | } 31 | 32 | export function dataStoreImageCreate(serve_root, subdir) { 33 | console.info('[DATASTORE] Local Image FileStore in use'); 34 | return new DataStoreImage(serve_root, subdir); 35 | } 36 | -------------------------------------------------------------------------------- /src/glov/server/data_stores_init.js: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist'; 2 | const argv = minimist(process.argv.slice(2)); 3 | import { dataStoreCreate } from './data_store'; 4 | import { dataStoreImageCreate } from './data_store_image'; 5 | import { dataStoreLimitedCreate } from './data_store_limited'; 6 | import { dataStoreMirrorCreate } from './data_store_mirror'; 7 | import { dataStoreShieldCreate } from './data_store_shield'; 8 | import { serverConfig } from './server_config'; 9 | 10 | export function dataStoresInit(data_stores) { 11 | let server_config = serverConfig(); 12 | 13 | // Meta and bulk stores 14 | if (!data_stores.meta) { 15 | data_stores.meta = dataStoreCreate('data_store'); 16 | } else if (server_config.do_mirror) { 17 | if (data_stores.meta) { 18 | if (server_config.local_authoritative === false) { 19 | console.log('[DATASTORE] Mirroring meta store (cloud authoritative)'); 20 | data_stores.meta = dataStoreMirrorCreate({ 21 | read_check: true, 22 | readwrite: data_stores.meta, 23 | write: dataStoreCreate('data_store'), 24 | }); 25 | } else { 26 | console.log('[DATASTORE] Mirroring meta store (local authoritative)'); 27 | data_stores.meta = dataStoreMirrorCreate({ 28 | read_check: true, 29 | readwrite: dataStoreCreate('data_store'), 30 | write: data_stores.meta, 31 | }); 32 | } 33 | } 34 | } 35 | if (!data_stores.bulk) { 36 | data_stores.bulk = dataStoreCreate('data_store/bulk'); 37 | if (argv.dev && argv['net-delay'] !== false) { 38 | data_stores.bulk = dataStoreLimitedCreate(data_stores.bulk, 1000, 1000, 250); 39 | } 40 | } else if (server_config.do_mirror) { 41 | if (data_stores.bulk) { 42 | if (server_config.local_authoritative === false) { 43 | console.log('[DATASTORE] Mirroring bulk store (cloud authoritative)'); 44 | data_stores.bulk = dataStoreMirrorCreate({ 45 | read_check: true, 46 | readwrite: data_stores.bulk, 47 | write: dataStoreCreate('data_store/bulk'), 48 | }); 49 | } else { 50 | console.log('[DATASTORE] Mirroring bulk store (local authoritative)'); 51 | data_stores.bulk = dataStoreMirrorCreate({ 52 | read_check: true, 53 | readwrite: dataStoreCreate('data_store/bulk'), 54 | write: data_stores.bulk, 55 | }); 56 | } 57 | } 58 | } 59 | if (server_config.do_shield) { 60 | console.log('[DATASTORE] Applying shield layer to bulk and meta stores'); 61 | data_stores.meta = dataStoreShieldCreate(data_stores.meta, { label: 'meta' }); 62 | data_stores.bulk = dataStoreShieldCreate(data_stores.bulk, { label: 'bulk' }); 63 | } 64 | 65 | // Image data store is a different API, not supporting mirror/shield for now 66 | if (data_stores.image === undefined) { 67 | data_stores.image = dataStoreImageCreate('data_store/public', 'upload'); 68 | } 69 | return data_stores; 70 | } 71 | -------------------------------------------------------------------------------- /src/glov/server/error_reports.js: -------------------------------------------------------------------------------- 1 | const metrics = require('./metrics.js'); 2 | const { ipFromRequest } = require('./request_utils.js'); 3 | 4 | let app_build_timestamp; 5 | export function errorReportsSetAppBuildTimestamp(version) { 6 | app_build_timestamp = version; 7 | } 8 | 9 | export function errorReportsInit(app) { 10 | app.post('/api/errorReport', function (req, res, next) { 11 | let ip = ipFromRequest(req); 12 | req.query.ip = ip; 13 | req.query.ua = req.headers['user-agent']; 14 | console.info('errorReport', req.query); 15 | res.end('OK'); 16 | if (app_build_timestamp && req.query.build !== app_build_timestamp) { 17 | metrics.add('client.error_report_old', 1); 18 | } else { 19 | metrics.add('client.error_report', 1); 20 | } 21 | }); 22 | app.post('/api/errorLog', function (req, res, next) { 23 | let ip = ipFromRequest(req); 24 | req.query.ip = ip; 25 | req.query.ua = req.headers['user-agent']; 26 | console.info('errorLog', req.query); 27 | res.end('OK'); 28 | metrics.add('client.error_report_nonfatal', 1); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/glov/server/exchange_gmx_common.ts: -------------------------------------------------------------------------------- 1 | // client -> server 2 | export const GMX_CMD_REGISTER = 1; 3 | export const GMX_CMD_UNREGISTER = 2; 4 | export const GMX_CMD_SUBSCRIBE = 3; 5 | export const GMX_CMD_PUBLISH = 5; 6 | // server -> client 7 | export const GMX_CMD_ACK = 10; 8 | 9 | export const GMX_HEADER = 0xCF; // 207 10 | 11 | export const GMX_OK = 0; 12 | export const GMX_ERR_ALREADY_EXISTS = 1; 13 | export const GMX_ERR_NOT_FOUND = 2; 14 | 15 | import assert from 'assert'; 16 | import { packetReadIntFromBuffer } from 'glov/common/packet'; 17 | 18 | export function createGMXDataHandler(parent: { 19 | emitBuf: (cmd: number, buf: Buffer, offs: number, len: number) => void; 20 | }): (buf: Buffer) => void { 21 | let buf: Buffer | null = null; 22 | function handleNewData(): boolean { 23 | assert(buf); 24 | if (buf.length < 3) { 25 | // need more 26 | // console.log('GMXDH: need more(1)'); 27 | return false; 28 | } 29 | let offs = 0; 30 | let header = buf[offs++]; 31 | assert.equal(header, GMX_HEADER); 32 | let read_ret = packetReadIntFromBuffer(buf, offs, buf.length); 33 | if (!read_ret) { 34 | // need more 35 | // console.log('GMXDH: need more(2)'); 36 | return false; 37 | } 38 | let payload_size = read_ret.v; 39 | assert(payload_size > 1); 40 | offs = read_ret.offs; 41 | if (offs + payload_size > buf.length) { 42 | // Need more 43 | // console.log(`GMXDH: need more(3:${offs},${payload_size},${buf.length})`); 44 | return false; 45 | } 46 | let cmd = buf[offs++]; 47 | let msg_data_buf = buf; 48 | let msg_data_offs = offs; 49 | let payload_end = offs + payload_size - 1; 50 | let more = buf.length > payload_end; 51 | if (more) { 52 | buf = buf.subarray(payload_end); 53 | } else { 54 | buf = null; 55 | } 56 | parent.emitBuf(cmd, msg_data_buf, msg_data_offs, payload_end); 57 | return more; 58 | } 59 | 60 | return (data: Buffer) => { 61 | // console.log(`GMXDH:on data len=${data.length}, buf=${buf?buf.length:null}`); 62 | if (buf) { 63 | buf = Buffer.concat([buf, data]); 64 | } else { 65 | buf = data; 66 | } 67 | while (handleNewData()) { 68 | // repeat until consumed 69 | } 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/glov/server/exchange_hashed.ts: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2023 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | 5 | import assert from 'assert'; 6 | import { mashString } from 'glov/common/rand_alea'; 7 | 8 | import type { Mexchange } from './exchange'; 9 | 10 | const { floor } = Math; 11 | 12 | type MexchangeFunction = (id: string, ...args: unknown[]) => void; 13 | type MexchangeFKey = keyof Mexchange; 14 | const function_names: readonly MexchangeFKey[] = 15 | ['register', 'replaceMessageHandler', 'subscribe', 'unregister', 'publish'] as const; 16 | 17 | export function exchangeHashedCreate(exchange_list: Mexchange[]): Mexchange { 18 | assert(exchange_list); 19 | assert(exchange_list.length); 20 | // Hash comparison and performance: https://gist.github.com/Jimbly/328387ec1623909af935e133850e9ed6 21 | let mult = 1 / 0xFFFFFFFF * exchange_list.length; 22 | function hasher(str: string): number { 23 | assert(str && typeof str === 'string'); 24 | let ret = mashString(str); 25 | return floor(ret * mult); 26 | } 27 | let ret: Partial = {}; 28 | function_names.forEach((api: keyof Mexchange) => { 29 | (ret as Record)[api] = function (id, ...args) { 30 | let hash = hasher(id); 31 | return (exchange_list[hash][api] as MexchangeFunction)(id, ...args); 32 | }; 33 | }); 34 | return ret as Mexchange; 35 | } 36 | -------------------------------------------------------------------------------- /src/glov/server/exchange_local_bypass.ts: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2023 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | import assert from 'assert'; 5 | import { 6 | Packet, 7 | isPacket, 8 | packetFromBuffer, 9 | } from 'glov/common/packet'; 10 | 11 | import type { 12 | Mexchange, 13 | MexchangeCompletionCB, 14 | MexchangeHandler, 15 | } from './exchange'; 16 | import type { TSMap } from 'glov/common/types'; 17 | 18 | class ExchangeLocalBypass implements Mexchange { 19 | queues: TSMap = {}; 20 | actual_exchange: Mexchange; 21 | 22 | constructor(actual_exchange: Mexchange) { 23 | this.queues = {}; 24 | this.actual_exchange = actual_exchange; 25 | } 26 | 27 | // register as an authoritative single handler 28 | // cb(message) 29 | // register_cb(err) if already exists 30 | register(id: string, cb: MexchangeHandler, register_cb: MexchangeCompletionCB): void { 31 | assert(id); 32 | assert(cb); 33 | if (this.queues[id]) { 34 | return void process.nextTick(function () { 35 | register_cb('ERR_ALREADY_EXISTS'); 36 | }); 37 | } 38 | this.queues[id] = cb; 39 | this.actual_exchange.register(id, cb, register_cb); 40 | } 41 | 42 | replaceMessageHandler(id: string, old_cb: MexchangeHandler, cb: MexchangeHandler): void { 43 | assert(id); 44 | assert(cb); 45 | assert(this.queues[id]); 46 | assert.equal(this.queues[id], old_cb); 47 | this.queues[id] = cb; 48 | this.actual_exchange.replaceMessageHandler(id, old_cb, cb); 49 | } 50 | 51 | // subscribe to a broadcast-to-all channel 52 | // can *not* do a local bypass, the broadcasts must go to the actual exchange 53 | subscribe(id: string, cb: MexchangeHandler, register_cb: MexchangeCompletionCB): void { 54 | this.actual_exchange.subscribe(id, cb, register_cb); 55 | } 56 | 57 | unregister(id: string, cb?: MexchangeCompletionCB): void { 58 | assert(this.queues[id]); 59 | delete this.queues[id]; 60 | this.actual_exchange.unregister(id, cb); 61 | } 62 | 63 | // pak and it's buffers are valid until cb() is called 64 | // cb(err) 65 | publish(dest: string, pak: Packet, cb: MexchangeCompletionCB): void { 66 | assert(isPacket(pak)); 67 | 68 | if (!this.queues[dest] || pak.no_local_bypass) { 69 | // not in our process 70 | return void this.actual_exchange.publish(dest, pak, cb); 71 | } 72 | 73 | let buf = pak.getBuffer(); // Actually probably a Uint8Array 74 | let buf_len = pak.getBufferLen(); 75 | assert(buf_len); 76 | process.nextTick(() => { 77 | let queue_cb = this.queues[dest]; 78 | if (!queue_cb) { 79 | // Unregistered just this tick? Fall back to actual exchange 80 | return void this.actual_exchange.publish(dest, pak, cb); 81 | } 82 | let clone = packetFromBuffer(buf, buf_len, true); 83 | queue_cb(clone); 84 | cb(null); 85 | }); 86 | } 87 | } 88 | 89 | export function exchangeLocalBypassCreate(actual_exchange: Mexchange): Mexchange { 90 | assert(actual_exchange); 91 | return new ExchangeLocalBypass(actual_exchange); 92 | } 93 | -------------------------------------------------------------------------------- /src/glov/server/external_users_validation.ts: -------------------------------------------------------------------------------- 1 | import { ERR_INVALID_PROVIDER } from 'glov/common/external_users_common'; 2 | import { ErrorCallback } from 'glov/common/types'; 3 | 4 | export interface ValidLoginData { 5 | provider: string; 6 | external_id: string; 7 | extra?: { 8 | identifier: string; 9 | platform: string; 10 | verified: boolean; 11 | }; 12 | } 13 | 14 | export interface ExternalUsersValidator { 15 | getProvider(): string; 16 | validateLogin(validation_data: string, cb: ErrorCallback): void; 17 | } 18 | 19 | const setup_validators: Partial> = {}; 20 | 21 | export function externalUsersValidateLogin( 22 | provider: string, 23 | validation_data: string, 24 | cb: ErrorCallback, 25 | ): void { 26 | let validator = setup_validators[provider]; 27 | if (validator) { 28 | validator.validateLogin(validation_data, cb); 29 | } else { 30 | cb(ERR_INVALID_PROVIDER); 31 | } 32 | } 33 | 34 | export function externalUsersValidationSetup(validators: ExternalUsersValidator[]): void { 35 | validators.forEach((validator) => { 36 | setup_validators[validator.getProvider()] = validator; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/glov/server/global_worker.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { CmdDef, HandlerSource, isClientHandlerSource } from 'glov/common/types'; 3 | import { ChannelServer } from './channel_server'; 4 | import { ChannelWorker, HandleNewClientOpts } from './channel_worker'; 5 | 6 | // General purpose worker(s) for handling global state 7 | 8 | export class GlobalWorker extends ChannelWorker { 9 | // constructor(channel_server, channel_id, channel_data) { 10 | // super(channel_server, channel_id, channel_data); 11 | // } 12 | 13 | handleNewClient(src: HandlerSource, opts: HandleNewClientOpts): string | null { 14 | // Do not allow any subscriptions by anyone other than sysadmins to any global 15 | // channels by default. 16 | // sysadmins probably subscribe only to get command completion 17 | // regular users should get global data in another way (it's already being broadcast 18 | // to each ChannelServerWorker) 19 | if (!isClientHandlerSource(src)) { 20 | return null; 21 | } 22 | if (!src.sysadmin && !src.csr) { 23 | return 'ERR_ACCESS_DENIED'; 24 | } 25 | return null; 26 | } 27 | } 28 | GlobalWorker.prototype.auto_destroy = true; 29 | 30 | let global_worker_cmds: CmdDef[] = []; 31 | let inited = false; 32 | 33 | export function globalWorkerAddCmd(cmd_def: CmdDef): void { 34 | assert(!inited); 35 | assert(!global_worker_cmds.find((e) => e.cmd === cmd_def.cmd)); 36 | assert(cmd_def.access_run && (cmd_def.access_run.includes('sysadmin') || cmd_def.access_run.includes('csr'))); 37 | global_worker_cmds.push(cmd_def); 38 | } 39 | 40 | export function globalWorkerInit(channel_server: ChannelServer): void { 41 | assert(!inited); 42 | inited = true; 43 | channel_server.registerChannelWorker('global', GlobalWorker, { 44 | autocreate: true, 45 | subid_regex: /^(global)$/, 46 | cmds: global_worker_cmds, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/glov/server/key_metrics.ts: -------------------------------------------------------------------------------- 1 | import { empty } from 'glov/common/util'; 2 | import { metricsAdd } from './metrics'; 3 | import { serverConfig } from './server_config'; 4 | import { UserTimeAccumulator } from './usertime'; 5 | 6 | import type { TSMap } from 'glov/common/types'; 7 | 8 | 9 | const TICK_TIME = 10000; // Moderate frequency reporting to metrics 10 | const LOG_TIME = 60000; // Much less frequent (long retention) logging (if enabled in server config) 11 | 12 | let usertime: UserTimeAccumulator; 13 | let accum: TSMap = {}; 14 | let last_log_time: number; 15 | let do_logging: boolean; 16 | 17 | export function keyMetricsAdd(metric: string, value: number): void { 18 | metricsAdd(metric, value); 19 | if (do_logging) { 20 | accum[metric] = (accum[metric] || 0) + value; 21 | } 22 | } 23 | 24 | export function keyMetricsAddTagged(metric: string, tags: string | string[], value: number): void { 25 | keyMetricsAdd(metric, value); 26 | if (typeof tags === 'string') { 27 | tags = tags ? tags.split(',') : []; 28 | } 29 | for (let ii = 0; ii < tags.length; ++ii) { 30 | keyMetricsAdd(`${metric}.${tags[ii]}`, value); 31 | } 32 | } 33 | 34 | export function usertimeStart(tags: string): void { 35 | usertime.start(tags); 36 | } 37 | 38 | export function usertimeEnd(tags: string): void { 39 | usertime.end(tags); 40 | } 41 | 42 | let accumulators: UserTimeAccumulator[] = []; 43 | export function keyMetricsAccumulatorCreate(metric_name: string): UserTimeAccumulator { 44 | let accumulator = new UserTimeAccumulator(metric_name, keyMetricsAdd); 45 | accumulators.push(accumulator); 46 | return accumulator; 47 | } 48 | 49 | function keyMetricsTickInternal(): void { 50 | for (let ii = 0; ii < accumulators.length; ++ii) { 51 | accumulators[ii].tick(); 52 | } 53 | 54 | if (do_logging) { 55 | let now = Date.now(); 56 | let time_since_log = now - last_log_time; 57 | if (time_since_log >= LOG_TIME) { 58 | last_log_time = now; 59 | if (!empty(accum)) { 60 | console.log('key_metrics', accum); 61 | } 62 | accum = {}; 63 | } 64 | } 65 | } 66 | 67 | export function keyMetricsTick(): void { 68 | keyMetricsTickInternal(); 69 | setTimeout(keyMetricsTick, TICK_TIME); 70 | } 71 | 72 | export function keyMetricsFlush(): void { 73 | keyMetricsTickInternal(); 74 | } 75 | 76 | export function keyMetricsStartup(): void { 77 | last_log_time = Date.now(); 78 | usertime = keyMetricsAccumulatorCreate('usertime'); 79 | setTimeout(keyMetricsTick, TICK_TIME); 80 | do_logging = Boolean(serverConfig().log?.load_log); 81 | } 82 | -------------------------------------------------------------------------------- /src/glov/server/load_bias_map.js: -------------------------------------------------------------------------------- 1 | exports.loadBiasMap = function loadBiasMap(value, value_safe, value_loaded, value_worst, bias_loaded, bias_max) { 2 | let high_is_bad = value_loaded > value_safe; 3 | if (high_is_bad) { 4 | if (value <= value_safe) { 5 | return 0; 6 | } 7 | if (value <= value_loaded) { 8 | return (value - value_safe) / (value_loaded - value_safe) * bias_loaded; 9 | } 10 | if (value <= value_worst) { 11 | return bias_loaded + (value - value_loaded) / (value_worst - value_loaded) * (bias_max - bias_loaded); 12 | } 13 | return bias_max; 14 | } else { 15 | if (value >= value_safe) { 16 | return 0; 17 | } 18 | if (value >= value_loaded) { 19 | return (value_safe - value) / (value_safe - value_loaded) * bias_loaded; 20 | } 21 | if (value >= value_worst) { 22 | return bias_loaded + (value_loaded - value) / (value_loaded - value_worst) * (bias_max - bias_loaded); 23 | } 24 | return bias_max; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/glov/server/metrics.js: -------------------------------------------------------------------------------- 1 | import * as execute_with_retry from 'glov/common/execute_with_retry'; 2 | 3 | const assert = require('assert'); 4 | 5 | let metric; 6 | let add_metrics = {}; 7 | let set_metrics = {}; 8 | 9 | // Add to a metric for event counts (i.e. something that we want to view the sum of over a time range) 10 | export function metricsAdd(metric_name, value) { 11 | assert(!set_metrics[metric_name]); 12 | add_metrics[metric_name] = 1; 13 | if (metric) { 14 | metric.add(metric_name, value); 15 | } 16 | } 17 | 18 | // Set a measurement metric (i.e. something reported on a fixed period that we may want to view the min/max/average of) 19 | // The most recent value will be reported when flushed 20 | export function metricsSet(metric_name, value) { 21 | assert(!add_metrics[metric_name]); 22 | if (set_metrics[metric_name] !== value || true) { 23 | set_metrics[metric_name] = value; 24 | if (metric) { 25 | metric.set(metric_name, value); 26 | } 27 | } 28 | } 29 | 30 | // Set a valued event metric for which we want detailed statistics (e.g. bytes sent per request), *not* sampled 31 | // at a regular interval 32 | // The metric provider may need to track sum/min/max/avg in-process between flushes 33 | // This could maybe be combined with `metric.add(metric_name, 1)` (but only care about sum in that case)? 34 | export function metricsStats(metric_name, value) { 35 | if (metric) { 36 | metric.stats(metric_name, value); 37 | } 38 | } 39 | 40 | // metric_impl must have .add and .set 41 | export function metricsInit(metric_impl) { 42 | metric = metric_impl; 43 | execute_with_retry.setMetricsAdd(metricsAdd); 44 | } 45 | 46 | // Legacy API 47 | export const add = metricsAdd; 48 | export const set = metricsSet; 49 | export const stats = metricsStats; 50 | export const init = metricsInit; 51 | -------------------------------------------------------------------------------- /src/glov/server/must_import.js: -------------------------------------------------------------------------------- 1 | /* 2 | The problem: Classes are not hoisted, and therefore cannot be exported safely. 3 | The wrong solution: hoist classes (causes problems if the class extends any other class 4 | - that which is exported might be the wrong thing) 5 | The hacky solution: know our dependency tree and require importing things in the right 6 | order, like it's 1999 again. 7 | */ 8 | 9 | const assert = require('assert'); 10 | let has_been_imported = {}; 11 | 12 | module.exports = function (mod_name, before_name) { 13 | assert(has_been_imported[mod_name], `Must import ${mod_name} before something that imports ${before_name.match(/[^/\\]+$/)[0]}`); 14 | }; 15 | 16 | module.exports.imported = function (mod_name) { 17 | has_been_imported[mod_name] = true; 18 | }; 19 | -------------------------------------------------------------------------------- /src/glov/server/packet_log.js: -------------------------------------------------------------------------------- 1 | const { perfCounterAdd } = require('glov/common/perfcounters.js'); 2 | const { min } = Math; 3 | 4 | const PKT_LOG_SIZE = 16; 5 | const PKT_LOG_BUF_SIZE = 32; 6 | 7 | export function packetLogInit(receiver) { 8 | receiver.pkt_log_idx = 0; 9 | receiver.pkt_log = new Array(PKT_LOG_SIZE); 10 | } 11 | 12 | export function packetLog(source, pak, buf_offs, msg) { 13 | let receiver = this; // eslint-disable-line @typescript-eslint/no-invalid-this 14 | let ple = receiver.pkt_log[receiver.pkt_log_idx]; 15 | if (!ple) { 16 | ple = receiver.pkt_log[receiver.pkt_log_idx] = { data: Buffer.alloc(PKT_LOG_BUF_SIZE) }; 17 | } 18 | // Copy first PKT_LOG_BUF_SIZE bytes for logging 19 | let buf = pak.getBuffer(); 20 | let buf_len = pak.getBufferLen(); 21 | let total_data_len = buf_len - buf_offs; 22 | let data_len = min(PKT_LOG_BUF_SIZE, total_data_len); 23 | ple.ts = Date.now(); 24 | ple.source = source; 25 | Buffer.prototype.copy.call(buf, ple.data, 0, buf_offs, buf_offs + data_len); 26 | ple.data_len = data_len; 27 | receiver.pkt_log_idx = (receiver.pkt_log_idx + 1) % PKT_LOG_SIZE; 28 | 29 | perfCounterAdd(`${receiver.perf_prefix}${msg}`); 30 | 31 | receiver.pkg_log_last_size = total_data_len; 32 | } 33 | -------------------------------------------------------------------------------- /src/glov/server/random_names.js: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2019 Jimb Esser (https://github.com/Jimbly/) 2 | // Released under MIT License: https://opensource.org/licenses/MIT 3 | 4 | const { floor, random } = Math; 5 | 6 | const adj = [ 7 | 'Adamant','Adroit','Amatory','Animistic','Antic','Arcadian','Baleful','Bearded', 8 | 'Bellicose','Bilious','Boorish','Calamitous','Caustic','Cerulean','Comely', 9 | 'Concomitant','Contumacious','Corpulent','Crapulous','Cromulent','Defamatory','Didactic', 10 | 'Dilatory','Dowdy','Efficacious','Effulgent','Egregious','Endemic','Equanimous', 11 | 'Execrable','Fastidious','Feckless','Fecund','Friable','Fulsome','Garrulous', 12 | 'Guileless','Gustatory','Heuristic','Histrionic','Hubristic','Incendiary', 13 | 'Insidious','Insolent','Intransigent','Inveterate','Invidious','Irksome', 14 | 'Jejune','Jocular','Judicious','Lachrymose','Limpid','Loquacious','Luminous', 15 | 'Mannered','Mendacious','Meretricious','Minatory','Mordant','Munificent', 16 | 'Nefarious','Noxious','Obtuse','Parsimonious','Pendulous','Pernicious', 17 | 'Pervasive','Petulant','Platitudinous','Precipitate','Propitious','Puckish', 18 | 'Querulous','Quiescent','Rebarbative','Recalcitrant','Redolent','Rhadamanthine', 19 | 'Risible','Ruminative','Sagacious','Salubrious','Sartorial','Sclerotic', 20 | 'Serpentine','Slumberous','Spasmodic','Strident','Taciturn','Tenacious','Tremulous', 21 | 'Trenchant','Turbulent','Turgid','Ubiquitous','Uxorious','Verdant','Voluble', 22 | 'Voracious','Wheedling','Withering','Zealous', 23 | ]; 24 | const nadj = adj.length; 25 | const noun = [ 26 | 'Alligator','Bear','Dragon','Heron','Chihuahua','Collie','Cougar','Dog','Eagle', 27 | 'Egret','Elephant','Falcon','Gallinule','Goldendoodle','Goldfinch', 28 | 'Guinea Pig','Hamster','Horned Owl','Hornet','Ibis','Kitten','Kookaburra', 29 | 'Leopard','Limpkin','Lion','Longwing','Macaw','Meerkat','Monkey','Owl', 30 | 'Bunting','Panda','Panther','Peafowl','Penguin', 31 | 'Puppy','Rabbit','Raccoon','Schipperke','Seal','Softshell', 32 | 'Squirrel','Starling','Stork','Sunbittern','Swallowtail','Tiger','Tortoise', 33 | 'Wolf','Zebra' 34 | ]; 35 | let nnoun = noun.length; 36 | 37 | export function get() { 38 | return `${adj[floor(random() * nadj)]} ${noun[floor(random() * nnoun)]}`; 39 | } 40 | -------------------------------------------------------------------------------- /src/glov/server/server_filewatch.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { serverFSdeleteCached } from './serverfs'; 3 | 4 | type FilewatchCB = (filename: string) => void; 5 | 6 | let always: FilewatchCB[] = []; 7 | let by_ext: Partial> = {}; 8 | let by_match: [RegExp | string, FilewatchCB][] = []; 9 | 10 | // cb(filename) 11 | export function serverFilewatchOn(ext_or_search: RegExp | string, cb: FilewatchCB): void { 12 | if (!ext_or_search) { 13 | always.push(cb); // also guaranteed to run first 14 | } else if (typeof ext_or_search === 'string' && ext_or_search[0] === '.') { 15 | assert(!by_ext[ext_or_search]); 16 | by_ext[ext_or_search] = cb; 17 | } else { 18 | by_match.push([ext_or_search, cb]); 19 | } 20 | } 21 | 22 | function serverOnFileChange(filename: string): void { 23 | console.log(`Server FileWatch change: ${filename}`); 24 | for (let ii = 0; ii < always.length; ++ii) { 25 | always[ii](filename); 26 | } 27 | let ext_idx = filename.lastIndexOf('.'); 28 | if (ext_idx !== -1) { 29 | let cb = by_ext[filename.slice(ext_idx)]; 30 | if (cb) { 31 | cb(filename); 32 | } 33 | } 34 | for (let ii = 0; ii < by_match.length; ++ii) { 35 | if (filename.match(by_match[ii][0])) { 36 | by_match[ii][1](filename); 37 | } 38 | } 39 | } 40 | 41 | export function serverFilewatchTriggerChange(filename: string): void { 42 | serverOnFileChange(filename); 43 | } 44 | 45 | serverFilewatchOn('', serverFSdeleteCached); 46 | -------------------------------------------------------------------------------- /src/glov/server/server_globals.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { dotPropDelete, dotPropGet, dotPropSet } from 'glov/common/dot-prop'; 3 | import { CmdDef } from 'glov/common/types'; 4 | import { ChannelServerWorker } from './channel_server_worker'; 5 | import { globalWorkerAddCmd } from './global_worker'; 6 | 7 | 8 | let global_data: Partial>; 9 | 10 | let csworker: ChannelServerWorker; 11 | export function serverGlobalsInit(csworker_in: ChannelServerWorker): void { 12 | assert(!csworker); 13 | csworker = csworker_in; 14 | } 15 | 16 | export type ServerGlobalOnDataCB = (csworker: ChannelServerWorker, data: T | undefined) => void; 17 | 18 | let on_data_cbs: { 19 | prefix: string; 20 | cb: ServerGlobalOnDataCB; 21 | }[] = []; 22 | 23 | function prefixMatches(prefix: string, key: string): boolean { 24 | // true if watching `foo.bar` and we get an update on `foo` or the other way around 25 | return key.startsWith(prefix) || prefix.startsWith(key); 26 | } 27 | 28 | export function serverGlobalsHandleChannelData(key: string, value: unknown): void { 29 | if (!key) { 30 | global_data = value as Partial>; 31 | } else { 32 | if (value === undefined) { 33 | dotPropDelete(global_data, key); 34 | } else { 35 | dotPropSet(global_data, key, value); 36 | } 37 | } 38 | for (let ii = 0; ii < on_data_cbs.length; ++ii) { 39 | let { prefix, cb } = on_data_cbs[ii]; 40 | if (prefixMatches(prefix, key)) { 41 | cb(csworker, dotPropGet(global_data, prefix)); 42 | } 43 | } 44 | } 45 | 46 | export function serverGlobalsReady(): boolean { 47 | return Boolean(global_data); 48 | } 49 | 50 | export function serverGlobalsGet(key: string): T | undefined; 51 | export function serverGlobalsGet(key: string, def: T): T; 52 | export function serverGlobalsGet(key: string, def?: T): T | undefined { 53 | return dotPropGet(global_data, key, def); 54 | } 55 | 56 | type ServerGlobalsDef = { 57 | // Note: callbacks here are ran in the context of the _ChannelServerWorker_ 58 | on_data?: ServerGlobalOnDataCB; 59 | // Note: commands here are ran in the context of the _GlobalWorker_ 60 | cmds?: CmdDef[]; 61 | }; 62 | 63 | export function serverGlobalsRegister(prefix: string, param: ServerGlobalsDef): void { 64 | if (param.on_data) { 65 | on_data_cbs.push({ 66 | prefix, 67 | cb: param.on_data as ServerGlobalOnDataCB, 68 | }); 69 | } 70 | if (param.cmds) { 71 | for (let ii = 0; ii < param.cmds.length; ++ii) { 72 | globalWorkerAddCmd(param.cmds[ii]); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/glov/server/server_util.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | const { floor } = Math; 4 | 5 | // Assures the specified length and does not start with a 0. 6 | export function randNumericId(len: number): string { 7 | // About 4.5x slower than calling Math.random() for each letter, but still relatively fast 8 | let buf = crypto.randomBytes(len); 9 | for (let ii = 0; ii < len; ++ii) { 10 | buf[ii] = (ii ? 48 : 49) + floor(buf[ii]/256 * (ii ? 10 : 9)); 11 | } 12 | return buf.toString(); 13 | } 14 | -------------------------------------------------------------------------------- /src/glov/server/serverfs.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { existsSync, readFileSync, readdirSync } from 'fs'; 3 | import path from 'path'; 4 | import { serverFilewatchOn } from './server_filewatch'; 5 | 6 | import type { FSAPI } from 'glov/common/fsapi'; 7 | import type { DataObject } from 'glov/common/types'; 8 | 9 | const FS_BASEPATH1 = '../../server/'; 10 | const FS_BASEPATH2 = '../../client/'; 11 | 12 | export function serverFSGetFileNames(directory: string): string[] { 13 | let path1 = path.join(__dirname, FS_BASEPATH1, directory); 14 | let path2 = path.join(__dirname, FS_BASEPATH2, directory); 15 | let ret; 16 | if (existsSync(path1)) { 17 | ret = readdirSync(path1); 18 | if (existsSync(path2)) { 19 | ret = ret.concat(readdirSync(path2)); 20 | } 21 | } else { 22 | // will throw exception here if neither path exists 23 | ret = readdirSync(path2); 24 | } 25 | ret = ret.filter((filename) => (!filename.endsWith('.br') && !filename.endsWith('.gz'))); 26 | ret = ret.map((filename) => `${directory}/${filename}`); 27 | return ret; 28 | } 29 | 30 | type ServerFSEntry = DataObject | Buffer; 31 | let serverfs_cache: Partial> = {}; 32 | 33 | export function serverFSGetFile(filename: string, encoding?: string): T { 34 | let cached = serverfs_cache[filename]; 35 | if (cached) { 36 | return cached as T; 37 | } 38 | let path1 = path.join(__dirname, FS_BASEPATH1, filename); 39 | let path2 = path.join(__dirname, FS_BASEPATH2, filename); 40 | let data; 41 | if (existsSync(path1)) { 42 | data = readFileSync(path1); 43 | } else { 44 | // Will throw exception here if neither exist 45 | data = readFileSync(path2); 46 | } 47 | assert(data, `Error loading file: ${filename}`); 48 | let ret; 49 | if (encoding === 'jsobj') { 50 | ret = JSON.parse(data.toString()); 51 | } else { 52 | ret = data; 53 | } 54 | serverfs_cache[filename] = ret; 55 | return ret; 56 | } 57 | 58 | export function serverFSdeleteCached(filename: string): void { 59 | delete serverfs_cache[filename]; 60 | } 61 | 62 | export function serverFSAPI(): FSAPI { 63 | return { 64 | getFileNames: serverFSGetFileNames, 65 | getFile: serverFSGetFile, 66 | filewatchOn: serverFilewatchOn, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/glov/server/test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | function notNodeModules(filename: string): boolean { 4 | return !filename.includes('node_modules'); 5 | } 6 | 7 | let project_root = `${path.resolve(__dirname, '../..')}/`.replace(/\\/g, '/'); 8 | function relpath(filename: string): string { 9 | filename = filename.replace(/\\/g, '/'); 10 | if (filename.startsWith(project_root)) { 11 | return filename.slice(project_root.length); 12 | } 13 | return filename; 14 | } 15 | 16 | process.on('exit', function () { 17 | let deps = Object.keys(require.cache).filter(notNodeModules).map(relpath); 18 | if (process.send) { 19 | process.send!({ type: 'deps', deps }); 20 | } else { 21 | console.log('Test deps = ', deps); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/glov/server/usertime.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import type { TSMap } from 'glov/common/types'; 4 | 5 | const { floor } = Math; 6 | 7 | export type MetricReporter = (metric: string, value: number) => void; 8 | 9 | // Tracks time attributed to users each with a set of tags, keeping track of 10 | // total time and time per tag. 11 | export class UserTimeAccumulator { 12 | private accum: TSMap = {}; 13 | private active: TSMap = {}; 14 | private last_tick_time: number; 15 | constructor(private metric: string, private reporter: MetricReporter) { 16 | this.last_tick_time = Date.now(); 17 | } 18 | 19 | start(tags: string): void { 20 | let list = tags ? tags.split(',') : []; 21 | let dt = Date.now() - this.last_tick_time; 22 | const { active, accum } = this; 23 | if (dt) { 24 | accum.total = (accum.total || 0) - dt; 25 | for (let ii = 0; ii < list.length; ++ii) { 26 | let tag = list[ii]; 27 | accum[tag] = (accum[tag] || 0) - dt; 28 | } 29 | } 30 | active.total = (active.total || 0) + 1; 31 | for (let ii = 0; ii < list.length; ++ii) { 32 | let tag = list[ii]; 33 | active[tag] = (active[tag] || 0) + 1; 34 | } 35 | } 36 | 37 | end(tags: string): void { 38 | let list = tags ? tags.split(',') : []; 39 | let dt = Date.now() - this.last_tick_time; 40 | const { active, accum } = this; 41 | if (dt) { 42 | accum.total = (accum.total || 0) + dt; 43 | for (let ii = 0; ii < list.length; ++ii) { 44 | let tag = list[ii]; 45 | accum[tag] = (accum[tag] || 0) + dt; 46 | } 47 | } 48 | assert(active.total); 49 | active.total--; 50 | for (let ii = 0; ii < list.length; ++ii) { 51 | let tag = list[ii]; 52 | assert(active[tag]); 53 | active[tag]!--; 54 | } 55 | } 56 | 57 | tick(): void { 58 | let now = Date.now(); 59 | let dt = now - this.last_tick_time; 60 | this.last_tick_time = now; 61 | let log: null | string[] = null as null | string[]; // Set to [] for debugging 62 | const { active, accum, metric, reporter } = this; 63 | for (let tag in active) { 64 | let count = active[tag]!; 65 | if (!count) { 66 | delete active[tag]; 67 | } 68 | let extra = accum[tag] || 0; 69 | let total_time = (count * dt + extra) / 1000; 70 | let total_seconds = floor(total_time); 71 | if (total_seconds) { 72 | log?.push(`${tag}=${total_seconds}`); 73 | if (tag === 'total') { 74 | reporter(metric, total_seconds); 75 | } else { 76 | reporter(`${metric}.${tag}`, total_seconds); 77 | } 78 | } 79 | let remainder = total_time - total_seconds; 80 | if (count) { 81 | // still have users being counted, keep the millisecond remainder for next tick 82 | accum[tag] = remainder; 83 | } else { 84 | // no users connected, drop the extra 85 | delete accum[tag]; 86 | } 87 | } 88 | if (log?.length) { 89 | console.debug(log.join(' ')); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/glov/tests/client/test-glov-client.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/order:off */ 2 | import 'glov/client/test'; // Must be first 3 | 4 | import assert from 'assert'; 5 | import 'glov/client/textures'; 6 | 7 | assert(true); 8 | -------------------------------------------------------------------------------- /src/glov/tests/common/dummyfs.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import type { FSAPI, FilewatchCB } from 'glov/common/fsapi'; 4 | import type { DataObject } from 'glov/common/types'; 5 | 6 | export class DummyFS implements FSAPI { 7 | files: Partial>; 8 | constructor(files: Partial>) { 9 | this.files = files; 10 | } 11 | 12 | getFileNames(directory: string): string[] { 13 | let ret = []; 14 | for (let key in this.files) { 15 | if (key.startsWith(directory)) { 16 | ret.push(key); 17 | } 18 | } 19 | return ret; 20 | } 21 | getFile(filename: string, encoding: 'jsobj'): T; 22 | getFile(filename: string, encoding: 'buffer'): Buffer; 23 | getFile(filename: string, encoding: 'jsobj' | 'buffer'): T { 24 | assert(encoding === 'jsobj'); 25 | let ret = this.files[filename]; 26 | assert(ret); 27 | return ret as DataObject as T; 28 | } 29 | 30 | by_ext: Partial> = {}; 31 | by_match: [RegExp | string, FilewatchCB][] = []; 32 | filewatchOn(ext_or_search: RegExp | string, cb: FilewatchCB): void { 33 | if (typeof ext_or_search === 'string' && ext_or_search[0] === '.') { 34 | assert(!this.by_ext[ext_or_search]); 35 | this.by_ext[ext_or_search] = cb; 36 | } else { 37 | this.by_match.push([ext_or_search, cb]); 38 | } 39 | } 40 | 41 | applyNewFile(filename: string, data: DataType): void { 42 | this.files[filename] = data; 43 | let ext_idx = filename.lastIndexOf('.'); 44 | if (ext_idx !== -1) { 45 | let ext = filename.slice(ext_idx); 46 | this.by_ext[ext]?.(filename); 47 | } 48 | for (let ii = 0; ii < this.by_match.length; ++ii) { 49 | if (filename.match(this.by_match[ii][0])) { 50 | this.by_match[ii][1](filename); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/glov/tests/common/test-traitstate.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { TypeDef, traitFactoryCreate } from 'glov/common/trait_factory'; 3 | import { clone } from 'glov/common/util'; 4 | import 'glov/server/test'; 5 | import { DummyFS } from './dummyfs'; 6 | 7 | type Stats = { 8 | stat1: number; 9 | }; 10 | 11 | type ClassData = { 12 | stats: Stats; 13 | }; 14 | 15 | class BaseClass { 16 | declare type_id: string; 17 | data: ClassData; 18 | constructor(data: ClassData) { 19 | this.data = data; 20 | } 21 | } 22 | 23 | let factory = traitFactoryCreate(); 24 | factory.registerTrait('stats', { 25 | default_opts: { 26 | stat1: 1, 27 | }, 28 | alloc_state: function (opts: Stats, obj: BaseClass) { 29 | // TODO: use a callback that doesn't actually need to allocate any state on the entity? 30 | if (!obj.data.stats) { 31 | obj.data.stats = clone(opts); 32 | } 33 | return undefined; 34 | }, 35 | }); 36 | 37 | 38 | let fs = new DummyFS({ 39 | 'foo/bar.def': { 40 | traits: [{ 41 | id: 'stats', 42 | stat1: 3, 43 | }], 44 | }, 45 | }); 46 | 47 | let reload_called = ''; 48 | function onReload(type_id: string): void { 49 | reload_called = type_id; 50 | } 51 | 52 | factory.initialize({ 53 | name: 'Test', 54 | fs, 55 | directory: 'foo', 56 | ext: '.def', 57 | Ctor: BaseClass, 58 | reload_cb: onReload, 59 | }); 60 | 61 | let barnew = factory.allocate('bar', {} as ClassData); 62 | let barold = factory.allocate('bar', { stats: { stat1: 5 } }); 63 | 64 | assert.equal(barnew.data.stats.stat1, 3); 65 | assert.equal(barold.data.stats.stat1, 5); 66 | 67 | // Reload 68 | assert(!reload_called); 69 | // trigger reload 70 | fs.applyNewFile('foo/bar.def', { 71 | traits: [{ 72 | id: 'stats', 73 | stat1: 7, 74 | }], 75 | }); 76 | assert.equal(reload_called, 'bar'); 77 | 78 | // existing should not have changed 79 | assert.equal(barnew.data.stats.stat1, 3); 80 | assert.equal(barold.data.stats.stat1, 5); 81 | 82 | // allocate again, should get the new values 83 | barnew = factory.allocate('bar', {} as ClassData); 84 | barold = factory.allocate('bar', { stats: { stat1: 5 } }); 85 | assert.equal(barnew.data.stats.stat1, 7); 86 | assert.equal(barold.data.stats.stat1, 5); 87 | -------------------------------------------------------------------------------- /src/glov/tests/common/test-util.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { asyncSeries } from 'glov-async'; 3 | import { DataObject } from 'glov/common/types'; 4 | import { 5 | dateToFileTimestamp, 6 | empty, 7 | lerpAngle, 8 | nearSameAngle, 9 | once, 10 | randomNot, 11 | trimEnd, 12 | } from 'glov/common/util'; 13 | import 'glov/server/test'; 14 | 15 | const { PI } = Math; 16 | 17 | asyncSeries([ 18 | function testOnce(next) { 19 | let called = false; 20 | function foo(): void { 21 | assert(!called); 22 | called = true; 23 | } 24 | let bar = once(foo); 25 | bar(); 26 | bar(); 27 | assert(called); 28 | next(); 29 | }, 30 | function testMisc(next) { 31 | assert(empty({})); 32 | assert(!empty({ foo: 'bar' })); 33 | assert(empty([] as unknown as DataObject)); 34 | assert(!empty([1] as unknown as DataObject)); 35 | next(); 36 | }, 37 | function testEmpty(next) { 38 | class Foo { 39 | bar: string; 40 | constructor() { 41 | this.bar = 'baz'; 42 | } 43 | } 44 | assert(!empty(new Foo() as unknown as DataObject)); 45 | class Foo2 { 46 | declare bar: string; 47 | } 48 | assert(empty(new Foo2() as unknown as DataObject)); 49 | class Foo3 { 50 | bar!: string; 51 | } 52 | assert(!empty(new Foo3() as unknown as DataObject)); 53 | class Foo4 { 54 | bar?: string; 55 | } 56 | assert(!empty(new Foo4() as unknown as DataObject)); 57 | next(); 58 | }, 59 | function testLerpAngle(next) { 60 | const E = 0.00001; 61 | const ANGLES = [0, 0.1, PI/3, PI/2, PI, PI * 3/2, PI * 2]; 62 | for (let ii = 0; ii < ANGLES.length; ++ii) { 63 | let a0 = ANGLES[ii]; 64 | for (let jj = ii; jj < ANGLES.length; ++jj) { 65 | let a1 = ANGLES[jj]; 66 | assert(nearSameAngle(lerpAngle(0, a0, a1), a0, E)); 67 | assert(nearSameAngle(lerpAngle(0, a1, a0), a1, E)); 68 | assert(nearSameAngle(lerpAngle(1, a0, a1), a1, E)); 69 | assert(nearSameAngle(lerpAngle(1, a1, a0), a0, E)); 70 | } 71 | } 72 | assert(nearSameAngle(lerpAngle(0.5, 0, 0.2), 0.1, E)); 73 | assert(nearSameAngle(lerpAngle(0.5, 0, PI*2-0.2), PI*2-0.1, E)); 74 | next(); 75 | }, 76 | function testDateToFileTimestamp(next) { 77 | let d = new Date(9999, 11, 31, 23, 59, 59); 78 | assert(dateToFileTimestamp(d) === '9999-12-31 23_59_59'); 79 | d = new Date(1900, 0, 1, 0, 0, 0); 80 | assert(dateToFileTimestamp(d) === '1900-01-01 00_00_00'); 81 | next(); 82 | }, 83 | function testRandomNot(next) { 84 | let v = 2; 85 | for (let ii = 0; ii < 10; ++ii) { 86 | let v2 = randomNot(v, 2, 4); 87 | assert(v2 !== v); 88 | assert(v2 >= 2); 89 | assert(v2 < 4); 90 | v = v2; 91 | } 92 | next(); 93 | }, 94 | function testTrimEnd(next) { 95 | assert.equal(trimEnd('asdf '), trimEnd('asdf')); 96 | assert.equal(trimEnd('asdf \n '), trimEnd('asdf')); 97 | assert.equal(trimEnd(' asdf \n '), trimEnd(' asdf')); 98 | assert.equal(trimEnd(' \n asdf \n '), trimEnd(' \n asdf')); 99 | next(); 100 | }, 101 | ], function (err) { 102 | if (err) { 103 | throw err; 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /src/glov/tests/server/test-load_bias_map.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { loadBiasMap } from 'glov/server/load_bias_map.js'; 3 | import 'glov/server/test'; 4 | 5 | // Example: CPU usage bias > 75% 6 | assert.equal(loadBiasMap(0, 70, 75, 100, 10, 20), 0); 7 | assert.equal(loadBiasMap(70, 70, 75, 100, 10, 20), 0); 8 | assert.equal(loadBiasMap(72.5, 70, 75, 100, 10, 20), 5); 9 | assert.equal(loadBiasMap(75, 70, 75, 100, 10, 20), 10); 10 | assert.equal(loadBiasMap(75+25/2, 70, 75, 100, 10, 20), 15); 11 | assert.equal(loadBiasMap(100, 70, 75, 100, 10, 20), 20); 12 | assert.equal(loadBiasMap(110, 70, 75, 100, 10, 20), 20); 13 | 14 | 15 | // Example: Free memory bias < 20% 16 | assert.equal(loadBiasMap(100, 25, 20, 0, 10, 20), 0); 17 | assert.equal(loadBiasMap(25, 25, 20, 0, 10, 20), 0); 18 | assert.equal(loadBiasMap(22.5, 25, 20, 0, 10, 20), 5); 19 | assert.equal(loadBiasMap(20, 25, 20, 0, 10, 20), 10); 20 | assert.equal(loadBiasMap(10, 25, 20, 0, 10, 20), 15); 21 | assert.equal(loadBiasMap(0, 25, 20, 0, 10, 20), 20); 22 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const http = require('http'); 3 | const https = require('https'); 4 | const path = require('path'); 5 | const express = require('express'); 6 | const express_static_gzip = require('express-static-gzip'); 7 | const { setupRequestHeaders } = require('glov/server/request_utils.js'); 8 | const glov_server = require('glov/server/server.js'); 9 | const argv = require('minimist')(process.argv.slice(2)); 10 | const test_worker = require('./test_worker.js'); 11 | 12 | let app = express(); 13 | let server = http.createServer(app); 14 | 15 | let server_https; 16 | if (argv.dev) { 17 | if (fs.existsSync('debugkeys/localhost.crt')) { 18 | let https_options = { 19 | cert: fs.readFileSync('debugkeys/localhost.crt'), 20 | key: fs.readFileSync('debugkeys/localhost.key'), 21 | }; 22 | server_https = https.createServer(https_options, app); 23 | } 24 | } 25 | setupRequestHeaders(app, { 26 | dev: argv.dev, 27 | allow_map: true, 28 | }); 29 | 30 | app.use(express_static_gzip(path.join(__dirname, '../client/'), { 31 | enableBrotli: true, 32 | orderPreference: ['br'], 33 | })); 34 | 35 | app.use(express_static_gzip('data_store/public', { 36 | enableBrotli: true, 37 | orderPreference: ['br'], 38 | })); 39 | 40 | glov_server.startup({ 41 | app, 42 | server, 43 | server_https, 44 | }); 45 | 46 | test_worker.init(glov_server.channel_server); 47 | 48 | let port = argv.port || process.env.port || 3000; 49 | 50 | server.listen(port, () => { 51 | console.info(`Running server at http://localhost:${port}`); 52 | }); 53 | if (server_https) { 54 | let secure_port = argv.sport || process.env.sport || (port + 100); 55 | server_https.listen(secure_port, () => { 56 | console.info(`Running server at https://localhost:${secure_port}`); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/server/test_worker.js: -------------------------------------------------------------------------------- 1 | const { ChannelWorker } = require('glov/server/channel_worker.js'); 2 | const { handleChat, handleChatGet } = require('glov/server/chattable_worker.js'); 3 | 4 | class TestWorker extends ChannelWorker { 5 | // constructor(channel_server, channel_id, channel_data) { 6 | // super(channel_server, channel_id, channel_data); 7 | // } 8 | handleBinGet(src, pak, resp_func) { 9 | let resp = resp_func.pak(); 10 | resp.writeBuffer(this.test_bin || new Uint8Array(0)); 11 | resp.send(); 12 | } 13 | handleBinSet(src, pak, resp_func) { 14 | let buf = pak.readBuffer(false); 15 | if (buf.length > 100) { 16 | return void resp_func('Too big'); 17 | } 18 | this.test_bin = buf; 19 | resp_func(); 20 | } 21 | } 22 | TestWorker.prototype.maintain_client_list = true; 23 | TestWorker.prototype.emit_join_leave_events = true; 24 | TestWorker.prototype.require_login = false; 25 | TestWorker.prototype.auto_destroy = true; 26 | 27 | export function init(channel_server) { 28 | channel_server.registerChannelWorker('test', TestWorker, { 29 | autocreate: true, 30 | subid_regex: /^.+$/, 31 | client_handlers: { 32 | bin_get: TestWorker.prototype.handleBinGet, 33 | bin_set: TestWorker.prototype.handleBinSet, 34 | chat: handleChat, 35 | chat_get: handleChatGet, 36 | }, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Target version of ECMAScript. 4 | "target": "ES6", 5 | // Module system 6 | "module": "CommonJS", 7 | // Search under node_modules for non-relative imports. 8 | "moduleResolution": "node", 9 | // Allow JavaScript files to be imported from TypeScript files. 10 | "allowJs": true, 11 | // Do not report errors in JavaScript files. 12 | "checkJs": false, 13 | // Don't emit; allow Babel to transform files. 14 | "noEmit": true, 15 | // Enable strictest settings like strictNullChecks & noImplicitAny. 16 | "strict": true, 17 | // Disallow features that require cross-file information for emit. 18 | "isolatedModules": true, 19 | // Import non-ES modules as default imports. 20 | "esModuleInterop": true, 21 | // Allows importing modules with a `.json` extension. 22 | "resolveJsonModule": true, 23 | // Base directory to resolve non-absolute module names 24 | "baseUrl": ".", 25 | // Possibly useful, but doing resolution in `typescript.js` instead to avoid cache misses 26 | // "paths": { 27 | // "glov": ["./glov"], 28 | // }, 29 | // Switch to the ECMA runtime behavior of class fields (coupled with allowDeclareFields in @babel/preset-typescript 30 | "useDefineForClassFields": true, 31 | }, 32 | "include": ["**/*.ts"] 33 | } 34 | --------------------------------------------------------------------------------