├── .editorconfig ├── .gitignore ├── PSDs ├── Icon.png ├── Icon.psd ├── entity.psd ├── menu.psd └── scrollbar.psd ├── README.md ├── build ├── alphafix.js ├── app-bundle.js ├── asset-hasher-rewrite.js ├── asset-hasher.js ├── autoatlas_build.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 ├── sourcemap-remap.js ├── spritesheet.js ├── test-runner.js ├── texpack.js ├── texproc.js ├── typescript.js ├── uglify.js ├── uglifyrc.js ├── warn-match.js ├── webfs_build.js ├── yamlproc.js └── zip.js ├── eslint.config.mjs ├── glovjs.sublime-project ├── package-lock.json ├── package.json └── src ├── client ├── account_ui.js ├── app.js ├── app_deps.js ├── atlases │ ├── default │ │ ├── button.9.png │ │ ├── button_disabled.9.png │ │ ├── button_down.9.png │ │ ├── button_rollover.9.png │ │ ├── buttons.psd │ │ ├── checked.9.png │ │ ├── collapsagories.9.png │ │ ├── collapsagories_rollover.9.png │ │ ├── collapsagories_shadow_down.9.png │ │ ├── menu.psd │ │ ├── menu_down.9.png │ │ ├── menu_entry.9.png │ │ ├── menu_header.9.png │ │ ├── menu_selected.9.png │ │ ├── panel.9.png │ │ ├── progress_bar.9.png │ │ ├── progress_bar.psd │ │ ├── progress_bar_trough.9.png │ │ ├── scrollbar.psd │ │ ├── scrollbar_bottom.png │ │ ├── scrollbar_handle.9.png │ │ ├── scrollbar_handle_grabber.png │ │ ├── scrollbar_top.png │ │ ├── scrollbar_trough.9.png │ │ ├── slider.9.png │ │ ├── slider.psd │ │ ├── slider_handle.png │ │ ├── slider_handle.psd │ │ ├── slider_notch.png │ │ └── unchecked.9.png │ ├── pixely │ │ ├── button.9.png │ │ ├── button_disabled.9.png │ │ ├── button_down.9.png │ │ ├── button_rollover.9.png │ │ ├── checked.9.png │ │ ├── collapsagories.9.png │ │ ├── collapsagories_rollover.9.png │ │ ├── collapsagories_shadow_down.9.png │ │ ├── menu_down.9.png │ │ ├── menu_entry.9.png │ │ ├── menu_header.9.png │ │ ├── menu_selected.9.png │ │ ├── panel.9.png │ │ ├── progress_bar.9.png │ │ ├── progress_bar_trough.9.png │ │ ├── scrollbar_bottom.png │ │ ├── scrollbar_handle.9.png │ │ ├── scrollbar_handle_grabber.png │ │ ├── scrollbar_top.png │ │ ├── scrollbar_trough.9.png │ │ ├── slider.9.png │ │ ├── slider_handle.png │ │ ├── slider_notch.png │ │ └── unchecked.9.png │ └── stone │ │ ├── button.9.png │ │ ├── button_disabled.9.png │ │ ├── button_down.9.png │ │ ├── button_rollover.9.png │ │ └── stone_buttons.psd ├── enttest.ts ├── favicon.ico ├── favicon.png ├── img │ ├── entity.png │ ├── font │ │ ├── 04b03_8x1.json │ │ ├── 04b03_8x1.png │ │ ├── 04b03_8x2.json │ │ ├── 04b03_8x2.png │ │ ├── gentium32.json │ │ ├── gentium32.png │ │ ├── palanquin32.json │ │ ├── palanquin32.png │ │ ├── vga_16x1.json │ │ ├── vga_16x1.png │ │ ├── vga_16x2.json │ │ └── vga_16x2.png │ ├── particles │ │ ├── circle64.png │ │ └── circle8.png │ ├── test.png │ ├── test_sprite.png │ ├── tinted_0.png │ └── tinted_1.png ├── index.html ├── main.css ├── main.ts ├── multiplayer.ts ├── particle_data.js ├── shaders │ └── test.fp ├── sounds │ ├── button_click.mp3 │ ├── msg_err.mp3 │ ├── msg_in.mp3 │ ├── msg_out.mp3 │ ├── msg_out_err.mp3 │ ├── music_test.mp3 │ ├── rollover.mp3 │ ├── test.mp3 │ ├── user_join.mp3 │ └── user_leave.mp3 ├── spine │ ├── copyright.txt │ ├── dino.atlas │ ├── dino.png │ └── dino.skel ├── test_3d.js ├── test_fullscreen.html ├── worker.js └── worker_deps.js ├── common └── entity_test_common.ts ├── glov ├── client │ ├── aabbtree.js │ ├── abtest.ts │ ├── animation.ts │ ├── auto_reset.ts │ ├── autoatlas.ts │ ├── bootstrap.js │ ├── browser.js │ ├── build_ui.ts │ ├── camera2d.js │ ├── chat_ui.ts │ ├── client_config.ts │ ├── cmd_auto_complete.ts │ ├── cmds.js │ ├── collapsagories.ts │ ├── color_picker.ts │ ├── draw_list.js │ ├── dyn_geom.js │ ├── edit_box.d.ts │ ├── edit_box.js │ ├── effects.js │ ├── engine.js │ ├── engine_cmds.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 │ ├── isbot.ts │ ├── link.js │ ├── local_storage.ts │ ├── localization.ts │ ├── locate_asset.ts │ ├── markdown.ts │ ├── markdown_parse.ts │ ├── markdown_renderables.ts │ ├── mat2d.js │ ├── mat43.js │ ├── mat4ScaleRotateTranslate.js │ ├── models.js │ ├── models │ │ ├── box_textured_embed.glb │ │ └── box_textured_embed.gltf │ ├── net.d.ts │ ├── net.js │ ├── net_position_manager.js │ ├── normalize_mousewheel.ts │ ├── particles.js │ ├── perf.js │ ├── perf_net.ts │ ├── pico8.js │ ├── platformer.js │ ├── pointer_lock.js │ ├── polyfill.js │ ├── profiler.js │ ├── profiler_ui.js │ ├── quat.js │ ├── report.ts │ ├── require.js │ ├── round_robinable.ts │ ├── score.ts │ ├── score_provider_autoweb.ts │ ├── score_ui.ts │ ├── scroll_area.ts │ ├── selection_box.d.ts │ ├── selection_box.js │ ├── settings.d.ts │ ├── settings.js │ ├── settings_types.d.ts │ ├── 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-markdown │ │ ├── index.ts │ │ └── troublesome-types.ts │ ├── 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.ts │ ├── 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.ts │ ├── worker_perf.js │ ├── worker_thread.ts │ └── wsclient.js ├── common │ ├── ack.js │ ├── ansi.ts │ ├── base32.js │ ├── base64.js │ ├── chunked_send.js │ ├── cmd_parse.ts │ ├── crc32.d.ts │ ├── 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 │ ├── net_common.ts │ ├── packet.d.ts │ ├── packet.js │ ├── perfcounters.js │ ├── platform.ts │ ├── rand_alea.js │ ├── rand_fast.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.ts │ ├── channel_worker.ts │ ├── chattable_worker.ts │ ├── class_proxy.js │ ├── client_comm.js │ ├── client_worker.ts │ ├── 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.ts │ ├── 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_font.ts │ ├── server_globals.ts │ ├── server_util.ts │ ├── serverfs.ts │ ├── shader_stats.js │ ├── test.ts │ ├── usertime.ts │ ├── version_management.ts │ └── wsserver.js └── tests │ ├── client │ ├── test-autocomplete.ts │ ├── test-glov-client.ts │ ├── test-markdown.ts │ └── test-round-robinable.ts │ ├── common │ ├── dummyfs.ts │ ├── test-common-types.ts │ ├── test-differ.ts │ ├── test-traitcompound.ts │ ├── test-traitfactory.ts │ ├── test-traitstate.ts │ ├── test-util-promisesprotection.js │ └── test-util.ts │ └── server │ └── test-load_bias_map.ts ├── server ├── enttest_worker.ts ├── index.js ├── loadtest.ts └── multiplayer_worker.ts ├── tests ├── client │ └── test-demo-client.ts ├── common │ ├── demo_helper.ts │ ├── demo_ignored.ts │ └── test-demo-common.ts └── server │ └── test-demo-server.ts └── 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 | user/ 3 | artifacts/ 4 | node_modules/ 5 | logs/ 6 | ynpm-debug.log 7 | npm-debug.log 8 | build.dev/ 9 | build.prod/ 10 | build.test/ 11 | build.electron/ 12 | .gbstate/ 13 | *.sublime-workspace 14 | Bfxr 15 | *.00? 16 | config/*.json 17 | .DS_Store 18 | wsdebug.json 19 | -------------------------------------------------------------------------------- /PSDs/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/PSDs/Icon.png -------------------------------------------------------------------------------- /PSDs/Icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/PSDs/Icon.psd -------------------------------------------------------------------------------- /PSDs/entity.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/PSDs/entity.psd -------------------------------------------------------------------------------- /PSDs/menu.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/PSDs/menu.psd -------------------------------------------------------------------------------- /PSDs/scrollbar.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/PSDs/scrollbar.psd -------------------------------------------------------------------------------- /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.bundles.push({ 3 | entrypoint: 'worker', 4 | deps: 'worker_deps', 5 | is_worker: true, 6 | do_version: null, 7 | }); 8 | config.extra_index.push({ 9 | name: 'multiplayer', 10 | defines: { 11 | PLATFORM: 'web', 12 | ENV: 'multiplayer', 13 | }, 14 | zip: false, 15 | }, { 16 | name: 'entity', 17 | defines: { 18 | PLATFORM: 'web', 19 | ENV: 'entity', 20 | }, 21 | zip: false, 22 | }, { 23 | // example .zip for itch.io publishing 24 | name: 'itch', 25 | defines: { 26 | ...config.default_defines, 27 | PLATFORM: 'web', 28 | }, 29 | zip: true, 30 | }); 31 | 32 | // Spine support 33 | // Note: Runtime requires a Spine license to use in any product. 34 | config.client_fsdata.push( 35 | 'client/spine/**.atlas', 36 | 'client/spine/**.skel', 37 | 'client/spine/**.json', 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /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 | if (options.filter_obj) { 17 | obj = options.filter_obj(obj, job, file); 18 | } 19 | let text = JSON.stringify(obj, null, options.beautify ? 2 : null); 20 | if (options.filter_text) { 21 | text = options.filter_text(text, job, file, obj); 22 | } 23 | job.out({ 24 | relative: file.relative.replace(/\.json5$/, '.json'), 25 | contents: Buffer.from(text), 26 | }); 27 | done(); 28 | } 29 | 30 | return { 31 | type: gb.SINGLE, 32 | func: parseJSON5, 33 | version: [ 34 | parseJSON5, 35 | options.filter_obj, 36 | options.filter_text, 37 | ], 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /build/sourcemap-remap.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const gb = require('glov-build'); 3 | 4 | const SOURCEMAP_STRING = '//# sourceMappingURL='; 5 | const SOURCEMAP_BUFFER = Buffer.from(SOURCEMAP_STRING); 6 | 7 | module.exports = function sourcemapRemap(cb) { 8 | return { 9 | type: gb.SINGLE, 10 | func: function (job, done) { 11 | let file = job.getFile(); 12 | let { relative, contents } = file; 13 | 14 | function proc(next) { 15 | if (!relative.endsWith('.js')) { 16 | return void next(); 17 | } 18 | let idx = contents.lastIndexOf(SOURCEMAP_BUFFER); 19 | if (idx === -1) { 20 | return void next(); 21 | } 22 | idx += SOURCEMAP_BUFFER.length; 23 | let str_len = contents.length - idx; 24 | assert(str_len < 1000); // something gone wrong? Should be exactly one of these at the end of the buffer 25 | let rest = contents.toString('utf8', idx); 26 | let m = rest.match(/^([^\s]+\.map)/); 27 | assert(m); 28 | let filename = m[1]; 29 | rest = rest.slice(filename.length); 30 | cb(job, filename, function (err, new_filename) { 31 | if (err) { 32 | return void next(err); 33 | } 34 | assert(new_filename); 35 | rest = `${new_filename}${rest}`; 36 | let newbuf = Buffer.alloc(idx + Buffer.byteLength(rest, 'utf8')); 37 | contents.copy(newbuf); 38 | newbuf.write(rest, idx, 'utf8'); 39 | contents = newbuf; 40 | next(); 41 | }); 42 | } 43 | proc(function (err) { 44 | job.out({ 45 | relative, 46 | contents, 47 | }); 48 | done(err); 49 | }); 50 | }, 51 | version: [cb], 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /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 n/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/zip.js: -------------------------------------------------------------------------------- 1 | const gb = require('glov-build'); 2 | const streamToArray = require('stream-to-array'); 3 | const { ZipFile } = require('yazl'); 4 | 5 | module.exports = function (params) { 6 | let { name } = params; 7 | 8 | function zip(job, done) { 9 | let zipfile = new ZipFile(); 10 | let files = job.getFiles(); 11 | for (let ii = 0; ii < files.length; ++ii) { 12 | let { relative, contents } = files[ii]; 13 | zipfile.addBuffer(contents, relative, { 14 | forceDosTimestamp: true, 15 | }); 16 | } 17 | streamToArray(zipfile.outputStream, function (err, parts) { 18 | if (err) { 19 | return void done(err); 20 | } 21 | job.out({ 22 | relative: name, 23 | contents: Buffer.concat(parts), 24 | }); 25 | done(); 26 | }); 27 | zipfile.end(); 28 | } 29 | return { 30 | type: gb.ALL, 31 | func: zip, 32 | version: [ 33 | zip, 34 | params, 35 | ], 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /glovjs.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [{ 3 | "folder_exclude_patterns": [ 4 | "node_modules", 5 | ".gbcache", 6 | ".gbstate", 7 | "build.dev", 8 | "build.prod", 9 | "build.test", 10 | "build.electron", 11 | "logs", 12 | "data_store", 13 | ], 14 | "file_exclude_patterns": ["*.00?", "*.node"], 15 | "binary_file_patterns": [ 16 | "*.glb", 17 | "*.gltf", 18 | "*.gz", 19 | "*.mp3", 20 | "*.ogg", 21 | "*.wav", 22 | "package-lock.json", 23 | ], 24 | "path": ".", 25 | }] 26 | } 27 | -------------------------------------------------------------------------------- /src/client/app.js: -------------------------------------------------------------------------------- 1 | /* eslint n/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('glov/client/worker_comm.js').startup(); // First, so it gets loading quickly (if workers required) 17 | if (window.conf_env === 'multiplayer') { 18 | require('./multiplayer.js').main(); 19 | } else if (window.conf_env === 'entity') { 20 | require('./enttest.js').main(); 21 | } else { 22 | require('./main.js').main(); 23 | } 24 | window.time_load_init = Date.now(); 25 | } 26 | 27 | window.addEventListener('DOMContentLoaded', onLoad); 28 | 29 | window.onload = onLoad; 30 | -------------------------------------------------------------------------------- /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 | deps['@jimbly/howler/src/plugins/howler.spatial.js'] = require('@jimbly/howler/src/plugins/howler.spatial.js'); 19 | // Spine support: 20 | deps['spine-core/AnimationState'] = require('@jimbly/spine-core/dist/AnimationState'); 21 | deps['spine-core/AnimationStateData'] = require('@jimbly/spine-core/dist/AnimationStateData'); 22 | deps['spine-core/AtlasAttachmentLoader'] = require('@jimbly/spine-core/dist/AtlasAttachmentLoader'); 23 | deps['spine-core/ClippingAttachment'] = require('@jimbly/spine-core/dist/attachments/ClippingAttachment'); 24 | deps['spine-core/MeshAttachment'] = require('@jimbly/spine-core/dist/attachments/MeshAttachment'); 25 | deps['spine-core/RegionAttachment'] = require('@jimbly/spine-core/dist/attachments/RegionAttachment'); 26 | deps['spine-core/SlotData'] = require('@jimbly/spine-core/dist/SlotData'); 27 | deps['spine-core/Skeleton'] = require('@jimbly/spine-core/dist/Skeleton'); 28 | deps['spine-core/SkeletonBinary'] = require('@jimbly/spine-core/dist/SkeletonBinary'); 29 | // deps['spine-core/SkeletonJson'] = require('@jimbly/spine-core/dist/SkeletonJson'); 30 | deps['spine-core/TextureAtlas'] = require('@jimbly/spine-core/dist/TextureAtlas'); 31 | deps['spine-core/Utils'] = require('@jimbly/spine-core/dist/Utils'); 32 | -------------------------------------------------------------------------------- /src/client/atlases/default/button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/button.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/button_disabled.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/button_disabled.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/button_down.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/button_down.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/button_rollover.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/button_rollover.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/buttons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/buttons.psd -------------------------------------------------------------------------------- /src/client/atlases/default/checked.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/checked.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/collapsagories.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/collapsagories.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/collapsagories_rollover.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/collapsagories_rollover.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/collapsagories_shadow_down.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/collapsagories_shadow_down.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/menu.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/menu.psd -------------------------------------------------------------------------------- /src/client/atlases/default/menu_down.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/menu_down.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/menu_entry.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/menu_entry.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/menu_header.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/menu_header.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/menu_selected.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/menu_selected.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/panel.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/panel.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/progress_bar.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/progress_bar.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/progress_bar.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/progress_bar.psd -------------------------------------------------------------------------------- /src/client/atlases/default/progress_bar_trough.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/progress_bar_trough.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/scrollbar.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/scrollbar.psd -------------------------------------------------------------------------------- /src/client/atlases/default/scrollbar_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/scrollbar_bottom.png -------------------------------------------------------------------------------- /src/client/atlases/default/scrollbar_handle.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/scrollbar_handle.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/scrollbar_handle_grabber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/scrollbar_handle_grabber.png -------------------------------------------------------------------------------- /src/client/atlases/default/scrollbar_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/scrollbar_top.png -------------------------------------------------------------------------------- /src/client/atlases/default/scrollbar_trough.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/scrollbar_trough.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/slider.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/slider.9.png -------------------------------------------------------------------------------- /src/client/atlases/default/slider.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/slider.psd -------------------------------------------------------------------------------- /src/client/atlases/default/slider_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/slider_handle.png -------------------------------------------------------------------------------- /src/client/atlases/default/slider_handle.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/slider_handle.psd -------------------------------------------------------------------------------- /src/client/atlases/default/slider_notch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/slider_notch.png -------------------------------------------------------------------------------- /src/client/atlases/default/unchecked.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/default/unchecked.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/button.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/button_disabled.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/button_disabled.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/button_down.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/button_down.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/button_rollover.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/button_rollover.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/checked.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/checked.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/collapsagories.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/collapsagories.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/collapsagories_rollover.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/collapsagories_rollover.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/collapsagories_shadow_down.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/collapsagories_shadow_down.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/menu_down.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/menu_down.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/menu_entry.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/menu_entry.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/menu_header.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/menu_header.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/menu_selected.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/menu_selected.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/panel.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/panel.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/progress_bar.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/progress_bar.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/progress_bar_trough.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/progress_bar_trough.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/scrollbar_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/scrollbar_bottom.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/scrollbar_handle.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/scrollbar_handle.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/scrollbar_handle_grabber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/scrollbar_handle_grabber.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/scrollbar_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/scrollbar_top.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/scrollbar_trough.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/scrollbar_trough.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/slider.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/slider.9.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/slider_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/slider_handle.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/slider_notch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/slider_notch.png -------------------------------------------------------------------------------- /src/client/atlases/pixely/unchecked.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/pixely/unchecked.9.png -------------------------------------------------------------------------------- /src/client/atlases/stone/button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/stone/button.9.png -------------------------------------------------------------------------------- /src/client/atlases/stone/button_disabled.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/stone/button_disabled.9.png -------------------------------------------------------------------------------- /src/client/atlases/stone/button_down.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/stone/button_down.9.png -------------------------------------------------------------------------------- /src/client/atlases/stone/button_rollover.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/stone/button_rollover.9.png -------------------------------------------------------------------------------- /src/client/atlases/stone/stone_buttons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/atlases/stone/stone_buttons.psd -------------------------------------------------------------------------------- /src/client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/favicon.ico -------------------------------------------------------------------------------- /src/client/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/favicon.png -------------------------------------------------------------------------------- /src/client/img/entity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/entity.png -------------------------------------------------------------------------------- /src/client/img/font/04b03_8x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/font/04b03_8x1.png -------------------------------------------------------------------------------- /src/client/img/font/04b03_8x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/font/04b03_8x2.png -------------------------------------------------------------------------------- /src/client/img/font/gentium32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/font/gentium32.png -------------------------------------------------------------------------------- /src/client/img/font/palanquin32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/font/palanquin32.png -------------------------------------------------------------------------------- /src/client/img/font/vga_16x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/font/vga_16x1.png -------------------------------------------------------------------------------- /src/client/img/font/vga_16x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/font/vga_16x2.png -------------------------------------------------------------------------------- /src/client/img/particles/circle64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/particles/circle64.png -------------------------------------------------------------------------------- /src/client/img/particles/circle8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/particles/circle8.png -------------------------------------------------------------------------------- /src/client/img/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/test.png -------------------------------------------------------------------------------- /src/client/img/test_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/test_sprite.png -------------------------------------------------------------------------------- /src/client/img/tinted_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/tinted_0.png -------------------------------------------------------------------------------- /src/client/img/tinted_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/img/tinted_1.png -------------------------------------------------------------------------------- /src/client/particle_data.js: -------------------------------------------------------------------------------- 1 | export let defs = {}; 2 | 3 | defs.explosion = { 4 | particles: { 5 | part0: { 6 | blend: 'alpha', 7 | texture: 'particles/circle64', 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.4, v: [1,1,0.5,0.5] }, 14 | { t: 1.0, v: [1,0,0,0] }, 15 | ], 16 | size: [[48,8], [48,8]], // 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: [2500,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: [[-16,32], [-16,32], 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,1000], 40 | emit_initial: 10, 41 | max_parts: Infinity, 42 | }, 43 | }, 44 | system_lifespan: 2500, 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/button_click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/button_click.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_err.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/msg_err.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_in.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/msg_in.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_out.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/msg_out.mp3 -------------------------------------------------------------------------------- /src/client/sounds/msg_out_err.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/msg_out_err.mp3 -------------------------------------------------------------------------------- /src/client/sounds/music_test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/music_test.mp3 -------------------------------------------------------------------------------- /src/client/sounds/rollover.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/rollover.mp3 -------------------------------------------------------------------------------- /src/client/sounds/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/test.mp3 -------------------------------------------------------------------------------- /src/client/sounds/user_join.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/user_join.mp3 -------------------------------------------------------------------------------- /src/client/sounds/user_leave.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/sounds/user_leave.mp3 -------------------------------------------------------------------------------- /src/client/spine/copyright.txt: -------------------------------------------------------------------------------- 1 | Dino from Splody 2 | Copyright (c) 2015 Dashing Strike LLC 3 | Used with permission -------------------------------------------------------------------------------- /src/client/spine/dino.atlas: -------------------------------------------------------------------------------- 1 | dino.png 2 | size:512,128 3 | filter:Linear,Linear 4 | E_Arm_Front 5 | bounds:91,4,26,28 6 | offsets:5,5,36,38 7 | rotate:90 8 | E_Body 9 | bounds:91,34,89,76 10 | offsets:5,5,99,86 11 | rotate:90 12 | E_Foot_Front 13 | bounds:270,5,45,30 14 | offsets:5,5,55,40 15 | rotate:90 16 | E_Head 17 | bounds:4,26,97,83 18 | offsets:57,5,159,93 19 | rotate:90 20 | E_Jaw 21 | bounds:271,60,63,61 22 | offsets:5,5,73,71 23 | rotate:90 24 | E_Leg_1 25 | bounds:171,5,39,47 26 | offsets:5,5,49,57 27 | rotate:90 28 | E_Leg_2 29 | bounds:222,13,31,44 30 | offsets:5,5,41,54 31 | rotate:90 32 | E_Tail 33 | bounds:171,48,75,49 34 | offsets:5,5,85,59 35 | rotate:90 36 | Shadow 37 | bounds:224,54,69,43 38 | offsets:5,5,79,53 39 | rotate:90 40 | -------------------------------------------------------------------------------- /src/client/spine/dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/spine/dino.png -------------------------------------------------------------------------------- /src/client/spine/dino.skel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jimbly/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/client/spine/dino.skel -------------------------------------------------------------------------------- /src/client/test_3d.js: -------------------------------------------------------------------------------- 1 | import * as mat4LookAt from 'gl-mat4/lookAt'; 2 | import * as engine from 'glov/client/engine.js'; 3 | import * as mat4ScaleRotateTranslate from 'glov/client/mat4ScaleRotateTranslate.js'; 4 | import * as models from 'glov/client/models.js'; 5 | import { qRotateZ, quat } from 'glov/client/quat.js'; 6 | import * as textures from 'glov/client/textures.js'; 7 | import { mat4, zaxis, zero_vec } from 'glov/common/vmath.js'; 8 | 9 | let mat_view = mat4(); 10 | let mat_obj = mat4(); 11 | let rot = quat(); 12 | 13 | export function test3D() { 14 | if (!models.models.box) { 15 | models.startup(); 16 | } 17 | engine.start3DRendering(); 18 | mat4LookAt(mat_view, [5,4,3], zero_vec, zaxis); 19 | engine.setGlobalMatrices(mat_view); 20 | qRotateZ(rot, rot, engine.frame_dt * 0.001); 21 | textures.bind(0, textures.textures.error); 22 | mat4ScaleRotateTranslate(mat_obj, 1, rot, [1,1,0.03]); 23 | models.models.box.draw({ mat: mat_obj }); 24 | mat4ScaleRotateTranslate(mat_obj, 1, rot, [0,0,0]); 25 | models.models.box.draw({ mat: mat_obj }); 26 | mat4ScaleRotateTranslate(mat_obj, 1, rot, [1,0,0.01]); 27 | models.models.box.draw({ mat: mat_obj }); 28 | mat4ScaleRotateTranslate(mat_obj, 1, rot, [0,1,0.02]); 29 | models.models.box.draw({ mat: mat_obj }); 30 | } 31 | -------------------------------------------------------------------------------- /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/common/entity_test_common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EntityBaseCommon, 3 | EntityBaseDataCommon, 4 | } from 'glov/common/entity_base_common'; 5 | 6 | export const VA_SIZE = 500; 7 | export const VIEW_DIST = 200; 8 | 9 | export enum EntityType { 10 | Player = 1, 11 | Bot = 2, 12 | } 13 | 14 | export type EntityTestDataCommon = { 15 | type: EntityType; 16 | pos: [number, number, number]; 17 | speed?: number; 18 | test_anim_state?: string; 19 | display_name?: string; 20 | seq_ai_move?: string; 21 | } & EntityBaseDataCommon; 22 | 23 | // export function entSamePos(ent: EntityBaseCommon, pos: [number, number]): boolean { 24 | // let ent_pos = ent.getData('pos') as number[]; 25 | // for (let ii = 0; ii < 2; ++ii) { 26 | // if (abs(ent_pos[ii] - pos[ii]) > EPSILON) { 27 | // return false; 28 | // } 29 | // } 30 | // return true; 31 | // } 32 | 33 | // eslint-disable-next-line @stylistic/max-len 34 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type 35 | export function entityTestCommonClass>(base: T) { 36 | // Note: `base` is either `EntityBaseServer` or `EntityBaseClient` 37 | return class EntityTestCommon extends base { 38 | declare data: EntityTestDataCommon; 39 | // // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | // constructor(...args: any[]) { 41 | // super(...args); 42 | // No data init here, client data comes from server. 43 | // } 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /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 | class AnimationSequencer { 25 | time = 0; 26 | fns: { 27 | done: boolean; 28 | fn: AnimationFunc; 29 | start: number; 30 | end: number; 31 | duration: number; 32 | }[]; 33 | constructor() { 34 | this.fns = []; 35 | } 36 | 37 | // Calls fn(progress) with progress >0 and <= 1; guaranteed call with === 1 38 | add(start: number, duration: number, fn: AnimationFunc): number { 39 | let end = start + duration; 40 | this.fns.push({ 41 | done: false, 42 | fn, 43 | start, 44 | end, 45 | duration, 46 | }); 47 | return end; 48 | } 49 | 50 | update(dt: number): boolean { 51 | this.time += dt; 52 | let any_left = false; 53 | for (let ii = 0; ii < this.fns.length; ++ii) { 54 | let e = this.fns[ii]; 55 | if (e.start < this.time && this.time < e.end) { 56 | any_left = true; 57 | e.fn((this.time - e.start) / e.duration); 58 | } else if (this.time >= e.end && !e.done) { 59 | e.fn(1); 60 | e.done = true; 61 | } else if (e.start >= this.time) { 62 | any_left = true; 63 | } 64 | } 65 | return any_left; 66 | } 67 | } 68 | export type { AnimationSequencer }; 69 | 70 | export function animationSequencerCreate(): AnimationSequencer { 71 | return new AnimationSequencer(); 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 type { TSMap } from 'glov/common/types'; 5 | import * as engine from './engine'; 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 | 20 | export function autoResetForce(key: string): void { 21 | delete auto_reset_data[key]; 22 | } 23 | -------------------------------------------------------------------------------- /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_ipad = Boolean(ua.match(/iPad/)); 5 | export let is_webkit = ua.match(/WebKit/i); 6 | export let is_ios_safari = is_ios && is_webkit && !ua.match(/CriOS/i); 7 | export let is_ios_chrome = is_ios && is_webkit && ua.match(/CriOS/i); 8 | let m = ua.match(/OS (\d+)(_\d+)?(_\d+)?\s/); 9 | // Note: also set on Chrome on iOS 10 | export let safari_version_major = (is_ios && m) ? Number(m[1] || '0') : 0; 11 | export let safari_version_minor = (is_ios && m) ? Number(m[2] && m[2].slice(1) || '0') : 0; 12 | export let is_mac_osx_safari = window.safari !== undefined; 13 | export let is_windows_phone = ua.match(/windows phone/i); 14 | export let is_android = !is_windows_phone && ua.match(/android/i); 15 | export let is_firefox = ua.match(/Firefox/i); 16 | export let is_itch_app = String(window.location.protocol).indexOf('itch') !== -1; // Note: itch.io APP, not web site 17 | 18 | export let is_discrete_gpu = false; 19 | 20 | function init() { 21 | try { 22 | let canvas = document.createElement('canvas'); 23 | canvas.width = 4; 24 | canvas.height = 4; 25 | let gltest = canvas.getContext('webgl'); 26 | if (gltest) { 27 | let debug_info = gltest.getExtension('WEBGL_debug_renderer_info'); 28 | if (debug_info) { 29 | let renderer_unmasked = gltest.getParameter(debug_info.UNMASKED_RENDERER_WEBGL); 30 | is_discrete_gpu = Boolean(renderer_unmasked && ( 31 | renderer_unmasked.match(/nvidia|radeon/i) || 32 | renderer_unmasked.match(/apple gpu/i) && is_mac_osx && !is_ios 33 | )); 34 | } 35 | } 36 | } catch (e) { 37 | // ignored 38 | } 39 | } 40 | init(); 41 | -------------------------------------------------------------------------------- /src/glov/client/client_config.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { 3 | PlatformDef, 4 | PlatformID, 5 | platformIsValid, 6 | platformOverrideParameter, 7 | platformParameter, 8 | } from 'glov/common/platform'; 9 | import type { TSMap } from 'glov/common/types'; 10 | 11 | // Platform 12 | assert(platformIsValid(window.conf_platform)); 13 | export const PLATFORM = window.conf_platform as PlatformID; 14 | 15 | let override_platform = PLATFORM; 16 | export function platformOverrideID(id: PlatformID): void { 17 | override_platform = id; 18 | } 19 | export function platformGetID(): PlatformID { 20 | return override_platform; 21 | } 22 | 23 | export function platformParameterGet(parameter: T): PlatformDef[T] { 24 | return platformParameter(platformGetID(), parameter); 25 | } 26 | 27 | const platform_devmode = platformParameter(PLATFORM, 'devmode'); 28 | export const MODE_DEVELOPMENT = platform_devmode === 'on' || platform_devmode === 'auto' && 29 | Boolean(String(document.location).match(/^https?:\/\/localhost/)); 30 | export const MODE_PRODUCTION = !MODE_DEVELOPMENT; 31 | 32 | // Abilities 33 | export function getAbilityReload(): boolean { 34 | return platformParameterGet('reload'); 35 | } 36 | export function setAbilityReload(value: boolean): void { 37 | platformOverrideParameter('reload', platformParameterGet('reload') && value); 38 | } 39 | 40 | export function getAbilityReloadUpdates(): boolean { 41 | return platformParameterGet('reload_updates'); 42 | } 43 | export function setAbilityReloadUpdates(value: boolean): void { 44 | platformOverrideParameter('reload_updates', platformParameterGet('reload_updates') && value); 45 | } 46 | 47 | export function getAbilityExit(): boolean { 48 | return platformParameterGet('exit'); 49 | } 50 | 51 | let ability_chat = true; 52 | export function getAbilityChat(): boolean { 53 | return ability_chat; 54 | } 55 | export function setAbilityChat(value: boolean): void { 56 | ability_chat = value; 57 | } 58 | 59 | let last_status: string | null = null; 60 | let last_others: string; 61 | let sent_to_platform = false; 62 | export function platformSetRichPresence(status: string | null, others: TSMap | null): void { 63 | let set_fn = platformParameterGet('setRichPresence'); 64 | if (set_fn && !sent_to_platform) { 65 | last_status = null; 66 | } 67 | let others_string = JSON.stringify(others); 68 | if (status === last_status && others_string === last_others) { 69 | return; 70 | } 71 | last_status = status; 72 | last_others = others_string; 73 | // Don't require at startup, has huge dependencies that will mess up boostrapping 74 | const { errorReportSetDetails } = require('./error_report'); // eslint-disable-line n/global-require 75 | errorReportSetDetails('rich_status', status); 76 | errorReportSetDetails('rich_others', others ? others_string : null); 77 | if (set_fn) { 78 | set_fn(status, others); 79 | sent_to_platform = true; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /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 { EventCallback } from 'glov/client/ui'; 2 | import type { TextVisualLimit } from 'glov/common/types'; 3 | import type { ROVec4 } from 'glov/common/vmath'; 4 | import type { FontStyle } from './font'; 5 | import type { FocusableElement } from './scroll_area'; 6 | 7 | export type EditBoxResult = null | 'submit' | 'cancel'; 8 | 9 | export interface EditBoxOptsAll { 10 | key: string; 11 | x: number; 12 | y: number; 13 | z: number; 14 | w: number; 15 | type: 'text' | 'number' | 'password' | 'email'; 16 | font_height: number; 17 | text: string | number; 18 | placeholder: string; 19 | max_len: number; 20 | max_visual_size: TextVisualLimit; 21 | zindex: null | number; 22 | uppercase: boolean; 23 | initial_focus: boolean; 24 | resize: boolean; // only applies to multi-line edit boxes 25 | // internal state: onetime_focus: boolean; 26 | focus_steal: boolean; 27 | auto_unfocus: boolean; 28 | initial_select: boolean; 29 | select_on_focus: boolean; 30 | spellcheck: boolean; 31 | esc_clears: boolean; 32 | esc_unfocuses: boolean; 33 | multiline: number; 34 | enforce_multiline: boolean; 35 | autocomplete: boolean; 36 | center: boolean; 37 | suppress_up_down: boolean; 38 | // custom_nav: Partial>; 39 | canvas_render: null | { 40 | // if set, will do custom canvas rendering instead of DOM rendering 41 | // requires a fixed-width font and near-perfectly aligned font rendering (tweak setDOMFontPixelScale) 42 | char_width: number; 43 | char_height: number; 44 | color_selection: ROVec4; 45 | color_caret: ROVec4; 46 | style_text: FontStyle; 47 | }; 48 | pointer_lock: boolean; 49 | spatial_focus: boolean; // used by spot logic 50 | } 51 | 52 | export type EditBoxOpts = Partial; 53 | 54 | export interface EditBox extends Readonly, FocusableElement { 55 | run(params?: EditBoxOpts): EditBoxResult; 56 | getText(): string; 57 | setText(new_text: string | number): void; 58 | isFocused(): boolean; 59 | hadOverflow(): boolean; 60 | getSelection(): [[number, number], [number, number]]; // [column, row], [column, row] 61 | 62 | readonly SUBMIT: 'submit'; 63 | readonly CANCEL: 'cancel'; 64 | } 65 | 66 | export function editBoxCreate(params?: EditBoxOpts): EditBox; 67 | 68 | // Pure immediate-mode API 69 | export function editBox(params: EditBoxOpts, current: T): { 70 | result: EditBoxResult; 71 | text: T; 72 | edit_box: EditBox; 73 | }; 74 | 75 | export function showOnscreenKeyboard(): EventCallback | undefined; 76 | 77 | export function editBoxAnyActive(): boolean; 78 | -------------------------------------------------------------------------------- /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 | import assert from 'assert'; 2 | import { locateAssetDisableHashing } from './locate_asset'; 3 | 4 | let by_ext = {}; 5 | let by_match = []; 6 | 7 | // cb(filename) 8 | export function filewatchOn(ext_or_search, cb) { 9 | if (ext_or_search[0] === '.') { 10 | assert(!by_ext[ext_or_search]); 11 | by_ext[ext_or_search] = cb; 12 | } else { 13 | by_match.push([ext_or_search, cb]); 14 | } 15 | } 16 | 17 | let message_cb; 18 | // cb(message) 19 | export function filewatchMessageHandler(cb) { 20 | message_cb = cb; 21 | } 22 | 23 | function onFileChange(filename) { 24 | console.log(`FileWatch change: ${filename}`); 25 | locateAssetDisableHashing(); 26 | let ext_idx = filename.lastIndexOf('.'); 27 | let did_reload = false; 28 | if (ext_idx !== -1) { 29 | let ext = filename.slice(ext_idx); 30 | if (by_ext[ext]) { 31 | if (by_ext[ext](filename) !== false) { 32 | did_reload = true; 33 | } 34 | } 35 | } 36 | for (let ii = 0; ii < by_match.length; ++ii) { 37 | if (filename.match(by_match[ii][0])) { 38 | if (by_match[ii][1](filename) !== false) { 39 | did_reload = true; 40 | } 41 | } 42 | } 43 | if (message_cb && did_reload) { 44 | message_cb(`Reloading: ${filename}`); 45 | } 46 | } 47 | 48 | export function filewatchTriggerChange(filename) { 49 | onFileChange(filename); 50 | } 51 | 52 | export function filewatchStartup(client) { 53 | client.onMsg('filewatch', onFileChange); 54 | } 55 | -------------------------------------------------------------------------------- /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 | declare module 'glov/client/global' { 2 | global { 3 | interface Window { 4 | // GLOV injected variables 5 | conf_platform?: string; 6 | conf_env?: string; 7 | 8 | // GLOV bootstrap 9 | debugmsg: (msg: string, clear?: boolean) => void; 10 | Z: Record; 11 | } 12 | 13 | const BUILD_TIMESTAMP: string; 14 | const __funcname: string; // eslint-disable-line no-underscore-dangle 15 | 16 | // GLOV ui.js 17 | const Z: Record; 18 | // GL context 19 | let gl: WebGLRenderingContext | WebGL2RenderingContext; 20 | // GLOV profiler 21 | function profilerStart(name: string): void; 22 | function profilerStop(name?: string): void; 23 | function profilerStopStart(name: string): void; 24 | function profilerStartFunc(): void; 25 | function profilerStopFunc(): void; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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 || 0; 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: ButtonIndex = 0; 4 | export const BUTTON_MIDDLE: ButtonIndex = 1; 5 | export const BUTTON_RIGHT: ButtonIndex = 2; 6 | export const BUTTON_ANY: ButtonIndex = -2; 7 | export const BUTTON_POINTERLOCK: ButtonIndex = -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/glovjs/847e6b308b8988af6d23a98ef4dbcfe17534b069/src/glov/client/models/box_textured_embed.glb -------------------------------------------------------------------------------- /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 { settingsRegister } 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 | settingsRegister({ 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 | settingsRegister({ 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/report.ts: -------------------------------------------------------------------------------- 1 | import type { CmdRespFunc } from 'glov/common/cmd_parse'; 2 | import { executeWithRetry } from 'glov/common/execute_with_retry'; 3 | import type { DataObject, ErrorCallback, TSMap } from 'glov/common/types'; 4 | import { cmd_parse } from './cmds'; 5 | import { 6 | errorReportSetDetails, 7 | reportingAPIPath, 8 | session_uid, 9 | } from './error_report'; 10 | import { fetch } from './fetch'; 11 | import { getStoragePrefix } from './local_storage'; 12 | import { scoreWithUserID } from './score'; 13 | 14 | let project_id: string = getStoragePrefix(); 15 | export function reportSetProject(project: string): void { 16 | project_id = project; 17 | errorReportSetDetails('project', project_id); 18 | } 19 | 20 | let last_report_id = 0; 21 | const RATE_LIMIT = 30*1000; 22 | let last_report_time: TSMap = {}; 23 | let queued_report: TSMap = {}; 24 | export function reportSend(type: string, payload: DataObject): void { 25 | if (queued_report[type]) { 26 | queued_report[type] = payload; 27 | return; 28 | } 29 | let now = Date.now(); 30 | let last_time = last_report_time[type] || 0; 31 | if (now - last_time < RATE_LIMIT) { 32 | setTimeout(function () { 33 | let payload2 = queued_report[type]; 34 | if (payload2) { 35 | delete queued_report[type]; 36 | reportSend(type, payload2); 37 | } 38 | }, RATE_LIMIT); 39 | queued_report[type] = payload; 40 | return; 41 | } 42 | last_report_time[type] = now; 43 | scoreWithUserID(function (user_id: string): void { 44 | let url = reportingAPIPath(); 45 | let report_uid = `${session_uid}-${++last_report_id}`; 46 | url += `report?project=${project_id}&type=${type}&uid=${user_id}` + 47 | `&rid=${report_uid}&p=${escape(JSON.stringify(payload))}`; 48 | function sendReport(next: ErrorCallback): void { 49 | fetch({ 50 | url, 51 | method: 'POST', 52 | timeout: 20000, 53 | }, next); 54 | } 55 | function done(err?: string | null): void { 56 | // ignore error, will happen when playing offline, etc 57 | if (err) { 58 | console.error(err); 59 | } 60 | } 61 | executeWithRetry( 62 | sendReport, { 63 | max_retries: 60, 64 | inc_backoff_duration: 250, 65 | max_backoff: 30000, 66 | log_prefix: 'reportSend', 67 | }, 68 | done, 69 | ); 70 | }); 71 | } 72 | 73 | export function reportFlush(type: string): void { 74 | let payload = queued_report[type]; 75 | if (!payload) { 76 | return; 77 | } 78 | delete queued_report[type]; 79 | last_report_time[type] = 0; 80 | reportSend(type, payload); 81 | } 82 | 83 | cmd_parse.register({ 84 | cmd: 'test_report', 85 | help: '(Debug) - Send a test report', 86 | access_show: ['sysadmin'], 87 | func: function (str: string, resp_func: CmdRespFunc) { 88 | reportSend('test', { str }); 89 | resp_func(); 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /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/settings.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | An example of how to add and declare a setting: 3 | 4 | import * as settings from 'glov/client/settings'; 5 | import { settingsRegister } from 'glov/client/settings'; 6 | 7 | settingsRegister({ 8 | my_setting: { 9 | default_value: -1, 10 | type: cmd_parse.TYPE_INT, 11 | range: [-1,1], 12 | } 13 | }); 14 | declare module 'glov/client/settings' { 15 | let my_setting: -1 | 0 | 1; 16 | } 17 | 18 | console.log(settings.my_setting); 19 | */ 20 | 21 | import type { CmdValueDef } from 'glov/common/cmd_parse'; 22 | import type { TSMap } from 'glov/common/types'; 23 | 24 | export function settingsGet(key: string): string | number; 25 | export function settingsSet(key: string, value: string | number): void; 26 | 27 | export function settingsSetAsync(key: string, value: string | number): void; 28 | export function settingsRunTimeDefault(key: string, new_default: string | number): void; 29 | export function settingsPush(pairs: Partial>): void; 30 | export function settingsPop(): void; 31 | export function settingsRegister(defs: TSMap): void; 32 | 33 | export function settingIsModified(key: string): boolean; 34 | -------------------------------------------------------------------------------- /src/glov/client/settings_types.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KeysMatching, 3 | NumberBoolean, 4 | } from 'glov/common/types'; 5 | import * as settings from './settings'; 6 | 7 | export type SettingsNumericalKeys = KeysMatching; 8 | export type SettingsStringKeys = KeysMatching; 9 | // export type SettingsValueKeys = KeysMatching; 10 | 11 | declare module 'glov/client/settings' { 12 | // Engine settings declared in settings.js: 13 | let max_fps: number; 14 | let use_animation_frame: number; 15 | let render_scale: number; 16 | let render_scale_mode: 0 | 1 | 2; 17 | let render_scale_all: number; 18 | let render_scale_clear: NumberBoolean; 19 | let fov: number; 20 | let double_click_time: number; 21 | 22 | // Extend settings.d.ts with typed versions of get/set 23 | function settingsGet(key: SettingsNumericalKeys): number; 24 | function settingsGet(key: SettingsStringKeys): string; 25 | 26 | function settingsSet(key: SettingsNumericalKeys, value: number): void; 27 | function settingsSet(key: SettingsStringKeys, value: string): void; 28 | } 29 | -------------------------------------------------------------------------------- /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-markdown/troublesome-types.ts: -------------------------------------------------------------------------------- 1 | export type Capture = 2 | | (Array & { 3 | index: number; 4 | }) 5 | | (Array & { 6 | index?: number; 7 | }); 8 | 9 | export type State = { 10 | key?: string | number | undefined; 11 | inline?: boolean | null | undefined; 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | [key: string]: any; 14 | }; 15 | 16 | export type MatchFunction = { 17 | regex?: RegExp; 18 | } & (( 19 | source: string, 20 | state: State, 21 | prevCapture: string, 22 | ) => Capture | null | undefined); 23 | -------------------------------------------------------------------------------- /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: { atlas: 'stone' }, 6 | button_rollover: { atlas: 'stone' }, 7 | button_down: { atlas: 'stone' }, 8 | button_disabled: { atlas: 'stone' }, 9 | }, 10 | pixely: { 11 | color_set_shades: [0.8, 1, 1], 12 | slider_params: [1, 1, 0.3], 13 | 14 | button: { atlas: 'pixely' }, 15 | button_rollover: { atlas: 'pixely' }, 16 | button_down: { atlas: 'pixely' }, 17 | button_disabled: { atlas: 'pixely' }, 18 | panel: { atlas: 'pixely' }, 19 | menu_entry: { atlas: 'pixely' }, 20 | menu_selected: { atlas: 'pixely' }, 21 | menu_down: { atlas: 'pixely' }, 22 | menu_header: { atlas: 'pixely' }, 23 | slider: { atlas: 'pixely' }, 24 | // slider_notch: { atlas: 'pixely' }, 25 | slider_handle: { atlas: 'pixely' }, 26 | 27 | checked: { atlas: 'pixely' }, 28 | unchecked: { atlas: 'pixely' }, 29 | 30 | scrollbar_bottom: { atlas: 'pixely' }, 31 | scrollbar_trough: { atlas: 'pixely' }, 32 | scrollbar_top: { atlas: 'pixely' }, 33 | scrollbar_handle_grabber: { atlas: 'pixely' }, 34 | scrollbar_handle: { atlas: 'pixely' }, 35 | progress_bar: { atlas: 'pixely' }, 36 | progress_bar_trough: { atlas: 'pixely' }, 37 | 38 | collapsagories: { atlas: 'pixely' }, 39 | collapsagories_rollover: { atlas: 'pixely' }, 40 | collapsagories_shadow_down: { atlas: 'pixely' }, 41 | }, 42 | }; 43 | 44 | export function spriteSetGet(key) { 45 | assert(sprite_sets[key]); 46 | return sprite_sets[key]; 47 | } 48 | -------------------------------------------------------------------------------- /src/glov/client/spritesheet.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { TSMap } from 'glov/common/types'; 3 | import { 4 | vec2, 5 | Vec4, 6 | vec4, 7 | } from 'glov/common/vmath'; 8 | import { engineStartupFunc } from './engine'; 9 | import { 10 | Sprite, 11 | spriteCreate, 12 | SpriteUIData, 13 | Texture, 14 | TextureOptions, 15 | } from './sprites'; 16 | import { textureLoad } from './textures'; 17 | 18 | export type SpriteSheetBuildTime = { 19 | name: string; 20 | tiles: TSMap; 21 | uidata: { 22 | rects: Vec4[]; 23 | aspect?: number[]; 24 | }; 25 | layers?: number; 26 | // [frame:string]: number; // FRAME_* 27 | }; 28 | 29 | export type SpriteSheet = { 30 | sprite: Sprite; 31 | sprite_centered: Sprite; 32 | sprite_centered_x: Sprite; 33 | tiles: Partial>; 34 | }; 35 | 36 | 37 | const uvs = vec4(0, 0, 1, 1); 38 | const origin_centered = vec2(0.5, 0.5); 39 | const origin_centered_x = vec2(0.5, 0); 40 | let load_opts: TSMap = {}; 41 | let hit_startup = false; 42 | export function spritesheetTextureOpts(name: string, opts: TextureOptions): void { 43 | assert(!hit_startup); 44 | load_opts[name] = opts; 45 | } 46 | 47 | // Called only from auto-generated .js files 48 | export function spritesheetRegister(runtime_data_in: SpriteSheetBuildTime): void { 49 | let runtime_data = runtime_data_in as (SpriteSheetBuildTime & SpriteSheet & TSMap); 50 | // Create with dummy data, will load later 51 | let texs: Texture[] = []; 52 | let sprite = runtime_data.sprite = spriteCreate({ texs, uvs }); 53 | runtime_data[`sprite_${runtime_data.name}`] = sprite; 54 | let sprite_centered = runtime_data.sprite_centered = spriteCreate({ texs, uvs, origin: origin_centered }); 55 | runtime_data[`sprite_${runtime_data.name}_centered`] = sprite_centered; 56 | let sprite_centered_x = runtime_data.sprite_centered_x = spriteCreate({ texs, uvs, origin: origin_centered_x }); 57 | runtime_data[`sprite_${runtime_data.name}_centered_x`] = sprite_centered_x; 58 | sprite.uidata = sprite_centered.uidata = sprite_centered_x.uidata = runtime_data.uidata as SpriteUIData; 59 | engineStartupFunc(function () { 60 | hit_startup = true; 61 | let opts = load_opts[runtime_data.name] || {}; 62 | if (runtime_data.layers) { 63 | for (let idx = 0; idx < runtime_data.layers; ++idx) { 64 | let tex = textureLoad({ 65 | ...opts, 66 | url: `img/${runtime_data.name}_${idx}.png`, 67 | }); 68 | texs.push(tex); 69 | } 70 | } else { 71 | let tex = textureLoad({ 72 | ...opts, 73 | url: `img/${runtime_data.name}.png`, 74 | }); 75 | texs.push(tex); 76 | } 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /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 | host = 'localhostmock'; 16 | } 17 | 18 | class MockDocument { 19 | getElementById(id: string): MockElementDebug { 20 | assert.equal(id, 'debug'); 21 | if (!debug) { 22 | debug = new MockElementDebug(); 23 | } 24 | return debug; 25 | } 26 | addEventListener(event: string, callback: () => void): void { 27 | // ignore 28 | } 29 | location = new MockLocation(); 30 | } 31 | 32 | class MockNavigator { 33 | userAgent = 'Node.js/test'; 34 | language = 'en-US'; 35 | languages = ['en-US']; 36 | platform = process.platform; 37 | } 38 | let glob = global as DataObject; 39 | 40 | assert(!glob.addEventListener); 41 | glob.addEventListener = function () { 42 | // ignore 43 | }; 44 | glob.conf_platform = 'web'; 45 | if (!glob.navigator) { 46 | glob.navigator = new MockNavigator(); 47 | } // else uses Node 22's built-in one 48 | glob.BUILD_TIMESTAMP = String(Date.now()); 49 | 50 | assert(!glob.document); 51 | let document = new MockDocument(); 52 | glob.document = document; 53 | glob.location = document.location; 54 | glob.window = glob; 55 | -------------------------------------------------------------------------------- /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 { locateAsset } from 'glov/client/locate_asset'; 6 | import { getURLBase } from 'glov/client/urlhash'; 7 | import { webFSGetFile } from 'glov/client/webfs'; 8 | import { mashString } from 'glov/common/rand_alea'; 9 | import { randFastCreate } from 'glov/common/rand_fast'; 10 | import { 11 | profanityCommonStartup, 12 | profanityFilterCommon, 13 | profanitySetReplacementChars, 14 | } from 'glov/common/words/profanity_common'; 15 | 16 | let non_profanity; 17 | 18 | export function profanityStartup() { 19 | non_profanity = webFSGetFile('words/replacements.txt', 'text').split('\n').filter((a) => a); 20 | profanityCommonStartup(webFSGetFile('words/filter.gkg', 'text'), 21 | webFSGetFile('words/exceptions.txt', 'text')); 22 | 23 | } 24 | 25 | export function profanityStartupLate() { 26 | // Async load of (potentially large) unicode replacement data, after all other loading is finished 27 | let scriptTag = document.createElement('script'); 28 | scriptTag.src = `${getURLBase()}${locateAsset('replacement_chars.min.js')}`; 29 | scriptTag.onload = function () { 30 | if (window.unicode_replacement_chars) { 31 | profanitySetReplacementChars(window.unicode_replacement_chars); 32 | fontSetReplacementChars(window.unicode_replacement_chars); 33 | } 34 | }; 35 | document.getElementsByTagName('head')[0].appendChild(scriptTag); 36 | } 37 | 38 | let rand = randFastCreate(); 39 | 40 | let last_word; 41 | function randWord() { 42 | if (last_word === -1 || non_profanity.length === 1) { 43 | last_word = rand.range(non_profanity.length); 44 | } else { 45 | let choice = rand.range(non_profanity.length - 1); 46 | last_word = choice < last_word ? choice : choice + 1; 47 | } 48 | return non_profanity[last_word]; 49 | } 50 | 51 | export function profanityFilter(user_str) { 52 | last_word = -1; 53 | rand.seed = mashString(user_str); 54 | return profanityFilterCommon(user_str, randWord); 55 | } 56 | -------------------------------------------------------------------------------- /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 | const { floor, random } = Math; 2 | 3 | // From Crockford's Base32, no confusing letters/numbers 4 | let to_base_32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; 5 | let char_table = to_base_32.split(''); 6 | 7 | // Tables including lower case and confusing letters (L, l, i, I, o, O) 8 | 9 | // let to_binary = [ 10 | // -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, 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 | // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1, -1,-1,-1,-1, 14 | // -1,10,11,12, 13,14,15,16, 17, 1,18,19, 1,20,21, 0, 15 | // 22,23,24,25, 26,-1,27,28, 29,30,31,-1, -1,-1,-1,-1, 16 | // -1,10,11,12, 13,14,15,16, 17, 1,18,19, 1,20,21, 0, 17 | // 22,23,24,25, 26,-1,27,28, 29,30,31,-1, -1,-1,-1,-1 18 | // ]; 19 | let to_cannon_table = [ 20 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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', '1', '2', '3', '4', '5', '6', '7', '8', '9', 0, 0, 0, 0, 0, 0, 24 | 0, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', '1', 'J', 'K', '1', 'M', 'N', '0', 25 | 'P', 'Q', 'R', 'S', 'T', 0, 'V', 'W', 'X', 'Y', 'Z', 0, 0, 0, 0, 0, 26 | 0, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', '1', 'J', 'K', '1', 'M', 'N', '0', 27 | 'P', 'Q', 'R', 'S', 'T', 0, 'V', 'W', 'X', 'Y', 'Z', 28 | ]; 29 | // Also strip out ignorable characters 30 | (function () { 31 | let strip = ' -–.,\t\n\r'; 32 | for (let ii = 0; ii < strip.length; ++ii) { 33 | to_cannon_table[strip.charCodeAt(ii)] = ''; 34 | } 35 | }()); 36 | // let to_cannon = { 37 | // '0':'0', '1':'1', '2':'2', '3':'3', '4':'4', '5':'5', '6':'6', '7':'7', '8':'8', '9':'9', 38 | // A:'A', B:'B', C:'C', D:'D', E:'E', F:'F', G:'G', H:'H', I:'1', 39 | // J:'J', K:'K', L:'1', M:'M', N:'N', O:'0', P:'P', Q:'Q', R:'R', 40 | // S:'S', T:'T', V:'V', W:'W', X:'X', Y:'Y', Z:'Z', 41 | // a:'A', b:'B', c:'C', d:'D', e:'E', f:'F', g:'G', h:'H', i:'1', 42 | // j:'J', k:'K', l:'1', m:'M', n:'N', o:'0', p:'P', q:'Q', r:'R', 43 | // s:'S', t:'T', v:'V', w:'W', x:'X', y:'Y', z:'Z', 44 | // }; 45 | 46 | export function cannonize(str) { 47 | let ret = []; 48 | for (let ii = 0; ii < str.length; ++ii) { 49 | let new_char = to_cannon_table[str.charCodeAt(ii)]; 50 | if (new_char === '') { 51 | // skipable char 52 | continue; 53 | } else if (!new_char) { 54 | // invalid char 55 | return null; 56 | } 57 | ret.push(new_char); 58 | } 59 | return ret.join(''); 60 | } 61 | 62 | export function gen(length) { 63 | let ret = []; 64 | for (let ii = 0; ii < length; ++ii) { 65 | ret.push(char_table[floor(random() * 32)]); 66 | } 67 | return ret.join(''); 68 | } 69 | 70 | export function addDashes(str) { 71 | let segs = floor(str.length / 4); 72 | let ret = []; 73 | for (let ii = 0; ii < segs; ++ii) { 74 | ret.push(str.slice(ii*4, ii*4+4)); 75 | } 76 | return ret.join('-'); 77 | } 78 | -------------------------------------------------------------------------------- /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.d.ts: -------------------------------------------------------------------------------- 1 | // Efficient CRCing of Buffers 2 | export default function crc32(buf: Buffer | Uint8Array | number[]): number; 3 | export function crc32(buf: Buffer | Uint8Array | number[]): number; 4 | 5 | // Note: not particularly efficient 6 | export function crc32string(str: string): number; 7 | -------------------------------------------------------------------------------- /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 | 44 | function stringUtf8Encode(str) { 45 | let c; 46 | let n; 47 | let utfbytes = []; 48 | // str = str.replace(/\r\n/g, '\n'); 49 | for (n = 0; n < str.length; ++n) { 50 | c = str.charCodeAt(n); 51 | if (c < 128) { 52 | utfbytes.push(c); 53 | } else if ((c > 127) && (c < 2048)) { 54 | utfbytes.push((c >> 6) | 192); 55 | utfbytes.push((c & 63) | 128); 56 | } else { 57 | utfbytes.push((c >> 12) | 224); 58 | utfbytes.push(((c >> 6) & 63) | 128); 59 | utfbytes.push((c & 63) | 128); 60 | } 61 | } 62 | return utfbytes; 63 | } 64 | 65 | // Note: not particularly efficient 66 | function crc32string(str) { 67 | let arr = stringUtf8Encode(str); 68 | return crc32(arr, arr.length); 69 | } 70 | module.exports.crc32string = crc32string; 71 | -------------------------------------------------------------------------------- /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 | // when invisible *and* idle; client -> server only, other clients should never see 8 | export const PRESENCE_OFFLINE_INACTIVE = 3; 9 | export function presenceActive(presence_value: number): boolean { 10 | return !(presence_value === PRESENCE_INACTIVE || presence_value === PRESENCE_OFFLINE_INACTIVE); 11 | } 12 | export function presenceVisible(presence_value: number): boolean { 13 | return !(presence_value === PRESENCE_OFFLINE || presence_value === PRESENCE_OFFLINE_INACTIVE); 14 | } 15 | 16 | export type NumberEnum = Record & Partial>; 17 | export type StringEnum = Record; 18 | 19 | export function getStringEnumValues(e: StringEnum): V[] { 20 | return Object.values(e); 21 | } 22 | export function isValidNumberEnumKey(e: NumberEnum, k: string): k is K { 23 | return typeof e[k] === 'number'; 24 | } 25 | export function isValidStringEnumKey(e: StringEnum, k: string): k is K { 26 | return k in e; 27 | } 28 | export function isValidStringEnumValue( 29 | e: StringEnum, 30 | v: string | undefined | null, 31 | ): v is V { 32 | for (let key in e) { 33 | if (e[key] === v) { 34 | return true; 35 | } 36 | } 37 | return false; 38 | } 39 | 40 | export const CHAT_FLAG_EMOTE = 1; 41 | export const CHAT_FLAG_USERCHAT = 2; // Only used on client, not communicated 42 | export const CHAT_USER_FLAGS = CHAT_FLAG_EMOTE|CHAT_FLAG_USERCHAT; 43 | 44 | export const CHAT_FLAG_DO_ECHO = 1<<31; // Only used on server, not communicated; do ERR_ECHO checks even if system msg 45 | -------------------------------------------------------------------------------- /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 FIFO { 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 }; 80 | 81 | export function fifoCreate(): FIFO { 82 | return new FIFO(); 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/fromQuat' { 16 | import type { Mat4, Vec4 } from 'glov/common/vmath'; 17 | export default function fromQuat(out: Mat4, a: Readonly): Mat4; 18 | } 19 | declare module 'gl-mat4/identity' { 20 | import type { Mat4 } from 'glov/common/vmath'; 21 | export default function identity(out: Mat4): Mat4; 22 | } 23 | declare module 'gl-mat4/invert' { 24 | import type { Mat4 } from 'glov/common/vmath'; 25 | export default function invert(out: Mat4, a: Readonly): Mat4; 26 | } 27 | declare module 'gl-mat4/lookAt' { 28 | import type { Mat4, ROVec3 } from 'glov/common/vmath'; 29 | export default function lookAt(out: Mat4, eye: ROVec3, center: ROVec3, up: ROVec3): Mat4; 30 | } 31 | declare module 'gl-mat4/multiply' { 32 | import type { Mat4 } from 'glov/common/vmath'; 33 | export default function multiply(out: Mat4, a: Readonly, b: Readonly): Mat4; 34 | } 35 | declare module 'gl-mat4/perspective' { 36 | import type { Mat4 } from 'glov/common/vmath'; 37 | export default function perspective(out: Mat4, fov_y: number, aspect: number, znear: number, zfar: number): Mat4; 38 | } 39 | declare module 'gl-mat4/scale' { 40 | import type { Mat4, ROVec3 } from 'glov/common/vmath'; 41 | export default function scale(out: Mat4, a: Readonly, v: ROVec3): Mat4; 42 | } 43 | declare module 'gl-mat4/translate' { 44 | import type { Mat4, ROVec3 } from 'glov/common/vmath'; 45 | export default function translate(out: Mat4, a: Readonly, v: ROVec3): Mat4; 46 | } 47 | declare module 'gl-mat4/transpose' { 48 | import type { Mat4 } from 'glov/common/vmath'; 49 | export default function transpose(out: Mat4, a: Readonly): Mat4; 50 | } 51 | -------------------------------------------------------------------------------- /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/no-empty-object-type 10 | type Constructor = new (...args: any[]) => T; 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/glov/common/net_common.ts: -------------------------------------------------------------------------------- 1 | import type { TextVisualLimit } from './types'; 2 | 3 | export const DISPLAY_NAME_MAX_LENGTH = 30; 4 | export const DISPLAY_NAME_MAX_VISUAL_SIZE: TextVisualLimit = { 5 | font_height: 24, 6 | width: 240, 7 | }; 8 | -------------------------------------------------------------------------------- /src/glov/common/packet.d.ts: -------------------------------------------------------------------------------- 1 | import type { NetResponseCallbackCalledBySystem } from './types'; 2 | 3 | export const PACKET_DEBUG = 1; 4 | export const MAX_JS_INT64 = 18446744073709550000; 5 | export const MAX_JS_INT53 = 9007199254740992; 6 | 7 | type PacketFlags = 0 | typeof PACKET_DEBUG; 8 | 9 | export function packetDefaultFlags(): PacketFlags; 10 | export function packetEnableDebug(enable: true): void; 11 | 12 | export interface Packet { 13 | readU8(): number; 14 | writeU8(value: number): void; 15 | readU32(): number; 16 | writeU32(value: number): void; 17 | readInt(): number; 18 | writeInt(value: number): void; 19 | readFloat(): number; 20 | writeFloat(value: number): void; 21 | readString(): string; 22 | writeString(value: string): void; 23 | readAnsiString(): string; 24 | writeAnsiString(value: string): void; 25 | readJSON(): T; 26 | readJSON(): unknown; 27 | writeJSON(value: T): void; 28 | writeJSON(value: unknown): void; 29 | readBool(): boolean; 30 | writeBool(value: boolean): void; 31 | readBuffer(do_copy: boolean): Uint8Array; 32 | writeBuffer(value: Uint8Array): void; 33 | appendBuffer(value: Uint8Array): void; 34 | 35 | append(other: Packet): void; 36 | appendRemaining(other: Packet): void; 37 | send(resp_func?: NetResponseCallbackCalledBySystem): void; 38 | ended(): boolean; 39 | updateFlags(flags: number): void; 40 | readFlags(): void; 41 | writeFlags(): void; 42 | getFlags(): number; 43 | getBuffer(): Uint8Array; 44 | getBufferLen(): number; 45 | getInternalFlags(): number; 46 | getOffset(): number; 47 | getRefCount(): number; 48 | makeReadable(): void; 49 | pool(): void; 50 | ref(): void; 51 | seek(offs: number): void; 52 | totalSize(): number; 53 | 54 | no_local_bypass?: true; // Internal-ish: poked by channel_server.js 55 | } 56 | 57 | export function packetCreate(flags?: PacketFlags, init_size?: number): Packet; 58 | export function packetFromBuffer(buf: Uint8Array | Buffer, buf_len: number, need_copy?: boolean): Packet; 59 | export function packetFromJSON(js_obj: unknown): Packet; 60 | export function isPacket(thing: unknown): thing is Packet; 61 | export function packetSizeInt(v: number): number; 62 | export function packetSizeAnsiString(v: string): number; 63 | export type PacketSpeculativeReadRet = { v: number; offs: number }; 64 | export function packetReadIntFromBuffer(buf: Buffer, offs: number, buf_len: number): PacketSpeculativeReadRet | null; 65 | -------------------------------------------------------------------------------- /src/glov/common/perfcounters.js: -------------------------------------------------------------------------------- 1 | const NUM_BUCKETS = 5; 2 | 3 | let counters = { time_start: Date.now() }; 4 | let hist = [counters]; 5 | let bucket_time = 10000; 6 | let countdown = bucket_time; 7 | 8 | export function perfCounterSetBucketTime(time) { 9 | countdown = bucket_time = time; 10 | } 11 | 12 | export function perfCounterAdd(key) { 13 | counters[key] = (counters[key] || 0) + 1; 14 | } 15 | 16 | export function perfCounterAddValue(key, value) { 17 | counters[key] = (counters[key] || 0) + value; 18 | } 19 | 20 | export function perfCounterTick(dt, log) { 21 | countdown -= dt; 22 | if (countdown <= 0) { 23 | countdown = bucket_time; 24 | if (hist.length === NUM_BUCKETS) { 25 | hist.splice(0, 1); 26 | } 27 | let now = Date.now(); 28 | counters.time_end = now; 29 | if (log) { 30 | log(counters); 31 | } 32 | counters = {}; 33 | counters.time_start = now; 34 | hist.push(counters); 35 | } 36 | } 37 | 38 | export function perfCounterHistory() { 39 | return hist; 40 | } 41 | -------------------------------------------------------------------------------- /src/glov/common/platform.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import type { TSMap } from './types'; 3 | 4 | export type PlatformID = string; 5 | 6 | export interface PlatformDef { 7 | // devmode: if `auto`, will enable MODE_DEVELOPMENT if the host starts with `localhost` 8 | devmode: 'auto' | 'on' | 'off'; 9 | // reload: whether or not we can call document.reload() to reload the page 10 | reload: boolean; 11 | // reload_updates: whether or not calling document.reload() will cause us to get an updated version of the client 12 | reload_updates: boolean; 13 | // random_creation_name: new users get a randomly generated name by default 14 | random_creation_name: boolean; 15 | // exit: can exit the app / need an exit button 16 | exit: boolean; 17 | // linkHandler: for platforms where web links will not work 18 | linkHandler?(url: string): void; 19 | // setRichPresence: for platforms that support it 20 | // note: due to startup ordering, probably needs to be set via platformOverrideParameter() 21 | setRichPresence?(status: string | null, others: TSMap | null): void; 22 | } 23 | /* 24 | Extend this like so: 25 | 26 | declare module 'glov/common/platform' { 27 | interface PlatformDef { 28 | some_feature: boolean; 29 | } 30 | } 31 | 32 | And make sure to define all parameters for the `web` platform with: 33 | platformRegister('web', { ... }); 34 | 35 | */ 36 | 37 | let platforms: Partial> = Object.create(null); 38 | 39 | let too_late_to_register = false; 40 | 41 | export function platformRegister(id: PlatformID, def: PlatformDef): void { 42 | assert(!too_late_to_register); 43 | assert(!platforms[id] || id === 'web'); 44 | platforms[id] = def; 45 | } 46 | 47 | export function platformGetValidIDs(): PlatformID[] { 48 | return Object.keys(platforms); 49 | } 50 | export function platformIsValid(v: string | undefined | null): boolean { 51 | too_late_to_register = true; // all registering must be done before the first querying 52 | return Boolean(typeof v === 'string' && platforms[v]); 53 | } 54 | let parameter_overrides: Partial = Object.create(null); 55 | export function platformParameter(platform: PlatformID, parameter: T): PlatformDef[T]; 56 | export function platformParameter(platform: PlatformID, parameter: keyof PlatformDef): unknown { 57 | let override = parameter_overrides[parameter]; 58 | if (override !== undefined) { 59 | return override; 60 | } 61 | let platdef = platforms[platform]; 62 | assert(platdef); 63 | return platdef[parameter]; 64 | } 65 | 66 | export function platformOverrideParameter(parameter: T, value: PlatformDef[T]): void { 67 | parameter_overrides[parameter] = value; 68 | } 69 | 70 | platformRegister('web', { 71 | devmode: 'auto', 72 | reload: true, 73 | reload_updates: true, 74 | random_creation_name: false, 75 | exit: false, 76 | } as PlatformDef); 77 | -------------------------------------------------------------------------------- /src/glov/common/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/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 | export function unreachable(x: never): false { 40 | verify(false); 41 | return false; 42 | } 43 | } 44 | 45 | export = verify; 46 | -------------------------------------------------------------------------------- /src/glov/common/words/exceptions.txt: -------------------------------------------------------------------------------- 1 | spices 2 | spikes 3 | spicy 4 | spiky 5 | Anaïs 6 | Anaís 7 | -------------------------------------------------------------------------------- /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/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 | import assert from 'assert'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import mkdirp from 'mkdirp'; 5 | 6 | const valid_path_regex = /[a-zA-Z0-9.-_]+/; 7 | class DataStoreImage { 8 | constructor(store_path, subdir) { 9 | this.path = path.join(store_path, subdir).replace(/\\/g, '/'); 10 | this.subdir = subdir; 11 | mkdirp.sync(this.path); 12 | } 13 | 14 | // cb(err, url) 15 | set(key, buffer, mime_type, cb) { 16 | assert(buffer instanceof Uint8Array); // Probably Uint8Array or Buffer 17 | assert(key.match(valid_path_regex)); 18 | let disk_path = path.join(this.path, key); 19 | let serve_path = `${this.subdir}/${key}`; 20 | fs.writeFile(disk_path, buffer, function (err) { 21 | cb(err, serve_path); 22 | }); 23 | } 24 | 25 | delete(key, cb) { 26 | let disk_path = path.join(this.path, key); 27 | fs.unlink(disk_path, cb); 28 | } 29 | } 30 | 31 | export function dataStoreImageCreate(serve_root, subdir) { 32 | console.info('[DATASTORE] Local Image FileStore in use'); 33 | return new DataStoreImage(serve_root, subdir); 34 | } 35 | -------------------------------------------------------------------------------- /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 { metricsAdd } = 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 | metricsAdd('client.error_report_old', 1, 'high'); 18 | } else { 19 | metricsAdd('client.error_report', 1, 'high'); 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 | metricsAdd('client.error_report_nonfatal', 1, 'high'); 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 / 0x100000000 * 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/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 | email?: string; 8 | display_name?: string; 9 | extra?: { 10 | identifier: string; 11 | platform: string; 12 | verified: boolean; 13 | }; 14 | } 15 | 16 | export interface ExternalUsersValidator { 17 | getProvider(): string; 18 | validateLogin(validation_data: string, cb: ErrorCallback): void; 19 | } 20 | 21 | const setup_validators: Partial> = {}; 22 | 23 | export function externalUsersValidateLogin( 24 | provider: string, 25 | validation_data: string, 26 | cb: ErrorCallback, 27 | ): void { 28 | let validator = setup_validators[provider]; 29 | if (validator) { 30 | validator.validateLogin(validation_data, cb); 31 | } else { 32 | cb(ERR_INVALID_PROVIDER); 33 | } 34 | } 35 | 36 | export function externalUsersValidationSetup(validators: ExternalUsersValidator[]): void { 37 | validators.forEach((validator) => { 38 | setup_validators[validator.getProvider()] = validator; 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/glov/server/global_worker.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import type { CmdDef } from 'glov/common/cmd_parse'; 3 | import { 4 | HandlerSource, 5 | isClientHandlerSource, 6 | TSMap, 7 | } from 'glov/common/types'; 8 | import type { ChannelServer } from './channel_server'; 9 | import { 10 | ChannelWorker, 11 | ClientHandlerFunction, 12 | HandleNewClientOpts, 13 | } from './channel_worker'; 14 | 15 | // General purpose worker(s) for handling global state 16 | 17 | export class GlobalWorker extends ChannelWorker { 18 | // constructor(channel_server, channel_id, channel_data) { 19 | // super(channel_server, channel_id, channel_data); 20 | // } 21 | 22 | handleNewClient(src: HandlerSource, opts: HandleNewClientOpts): string | null { 23 | // Do not allow any subscriptions by anyone other than sysadmins to any global 24 | // channels by default. 25 | // sysadmins probably subscribe only to get command completion 26 | // regular users should get global data in another way (it's already being broadcast 27 | // to each ChannelServerWorker) 28 | if (!isClientHandlerSource(src)) { 29 | return null; 30 | } 31 | if (!src.sysadmin && !src.csr) { 32 | return 'ERR_ACCESS_DENIED'; 33 | } 34 | return null; 35 | } 36 | } 37 | GlobalWorker.prototype.require_subscribe = false; 38 | GlobalWorker.prototype.auto_destroy = true; 39 | 40 | let global_worker_cmds: CmdDef[] = []; 41 | let inited = false; 42 | 43 | export function globalWorkerAddCmd(cmd_def: CmdDef): void { 44 | assert(!inited); 45 | assert(!global_worker_cmds.find((e) => e.cmd === cmd_def.cmd)); 46 | assert(cmd_def.access_run && (cmd_def.access_run.includes('sysadmin') || cmd_def.access_run.includes('csr'))); 47 | global_worker_cmds.push(cmd_def); 48 | } 49 | 50 | let global_worker_client_handlers: TSMap = {}; 51 | export function globalWorkerRegisterClientHandler( 52 | message: string, 53 | handler: ClientHandlerFunction 54 | ): void { 55 | assert(!inited); 56 | assert(!global_worker_client_handlers[message]); 57 | global_worker_client_handlers[message] = handler as unknown as ClientHandlerFunction; 58 | } 59 | 60 | export function globalWorkerInit(channel_server: ChannelServer): void { 61 | assert(!inited); 62 | inited = true; 63 | channel_server.registerChannelWorker('global', GlobalWorker, { 64 | autocreate: true, 65 | subid_regex: /^(global)$/, 66 | cmds: global_worker_cmds, 67 | client_handlers: global_worker_client_handlers, 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/glov/server/key_metrics.ts: -------------------------------------------------------------------------------- 1 | import type { TSMap } from 'glov/common/types'; 2 | import { empty } from 'glov/common/util'; 3 | import { logCategoryEnabled } from './log'; 4 | import { MetricFreq, metricsAdd } from './metrics'; 5 | import { UserTimeAccumulator } from './usertime'; 6 | 7 | 8 | const TICK_TIME = 10000; // Moderate frequency reporting to metrics 9 | const LOG_TIME = 60000; // Much less frequent (long retention) logging (if enabled in server config) 10 | 11 | let usertime: UserTimeAccumulator; 12 | let accum: TSMap = {}; 13 | let last_log_time: number; 14 | 15 | export function keyMetricsAdd(metric: string, value: number, freq: MetricFreq): void { 16 | metricsAdd(metric, value, freq); 17 | if (logCategoryEnabled('load')) { 18 | accum[metric] = (accum[metric] || 0) + value; 19 | } 20 | } 21 | 22 | export function keyMetricsAddTagged(metric: string, tags: string | string[], value: number, freq: MetricFreq): void { 23 | keyMetricsAdd(metric, value, freq); 24 | if (typeof tags === 'string') { 25 | tags = tags ? tags.split(',') : []; 26 | } 27 | for (let ii = 0; ii < tags.length; ++ii) { 28 | keyMetricsAdd(`${metric}.${tags[ii]}`, value, freq); 29 | } 30 | } 31 | 32 | export function usertimeStart(tags: string): void { 33 | usertime.start(tags); 34 | } 35 | 36 | export function usertimeEnd(tags: string): void { 37 | usertime.end(tags); 38 | } 39 | 40 | let accumulators: UserTimeAccumulator[] = []; 41 | export function keyMetricsAccumulatorCreate(metric_name: string): UserTimeAccumulator { 42 | let accumulator = new UserTimeAccumulator(metric_name, keyMetricsAdd); 43 | accumulators.push(accumulator); 44 | return accumulator; 45 | } 46 | 47 | function keyMetricsTickInternal(): void { 48 | for (let ii = 0; ii < accumulators.length; ++ii) { 49 | accumulators[ii].tick(); 50 | } 51 | 52 | if (logCategoryEnabled('load')) { 53 | let now = Date.now(); 54 | let time_since_log = now - last_log_time; 55 | if (time_since_log >= LOG_TIME) { 56 | last_log_time = now; 57 | if (!empty(accum)) { 58 | console.log('key_metrics', accum); 59 | } 60 | accum = {}; 61 | } 62 | } 63 | } 64 | 65 | export function keyMetricsTick(): void { 66 | keyMetricsTickInternal(); 67 | setTimeout(keyMetricsTick, TICK_TIME); 68 | } 69 | 70 | export function keyMetricsFlush(): void { 71 | keyMetricsTickInternal(); 72 | } 73 | 74 | export function keyMetricsStartup(): void { 75 | last_log_time = Date.now(); 76 | usertime = keyMetricsAccumulatorCreate('usertime'); 77 | setTimeout(keyMetricsTick, TICK_TIME); 78 | } 79 | -------------------------------------------------------------------------------- /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.ts: -------------------------------------------------------------------------------- 1 | export const METRIC_FREQ_HIGH = 'high'; // at least once a minute - whatever the maximum frequency of the back-end is 2 | export const METRIC_FREQ_MED = 'med'; // once every 2 minutes - default 3 | export const METRIC_FREQ_LOW = 'low'; // flushed once every 10 minutes, used for large bulk counters, e.g. # of logs 4 | export type MetricFreq = typeof METRIC_FREQ_HIGH | typeof METRIC_FREQ_MED | typeof METRIC_FREQ_LOW; 5 | 6 | import * as execute_with_retry from 'glov/common/execute_with_retry'; 7 | import { TSMap } from 'glov/common/types'; 8 | 9 | const assert = require('assert'); 10 | 11 | export type MetricsImplementation = { 12 | add(metric_name: string, value: number, freq: MetricFreq): void; 13 | set(metric_name: string, value: number, freq: MetricFreq): void; 14 | stats(metric_name: string, value: number, freq: MetricFreq): void; 15 | }; 16 | 17 | let metric: MetricsImplementation | undefined; 18 | let add_metrics: TSMap = {}; 19 | let set_metrics: TSMap = {}; 20 | 21 | // Add to a metric for event counts (i.e. something that we want to view the sum of over a time range) 22 | export function metricsAdd(metric_name: string, value: number, freq: MetricFreq): void { 23 | assert(!set_metrics[metric_name]); 24 | add_metrics[metric_name] = 1; 25 | if (metric) { 26 | metric.add(metric_name, value, freq || METRIC_FREQ_MED); 27 | } 28 | } 29 | 30 | // Set a measurement metric (i.e. something reported on a fixed period that we may want to view the min/max/average of) 31 | // The most recent value will be reported when flushed 32 | export function metricsSet(metric_name: string, value: number, freq: MetricFreq): void { 33 | assert(!add_metrics[metric_name]); 34 | if (set_metrics[metric_name] !== value || true) { 35 | set_metrics[metric_name] = value; 36 | if (metric) { 37 | metric.set(metric_name, value, freq || METRIC_FREQ_MED); 38 | } 39 | } 40 | } 41 | 42 | // Set a valued event metric for which we want detailed statistics (e.g. bytes sent per request), *not* sampled 43 | // at a regular interval 44 | // The metric provider may need to track sum/min/max/avg in-process between flushes 45 | // This could maybe be combined with `metric.add(metric_name, 1)` (but only care about sum in that case)? 46 | export function metricsStats(metric_name: string, value: number, freq: MetricFreq): void { 47 | if (metric) { 48 | metric.stats(metric_name, value, freq || METRIC_FREQ_MED); 49 | } 50 | } 51 | 52 | // metric_impl must have .add and .set 53 | export function metricsInit(metric_impl: MetricsImplementation): void { 54 | metric = metric_impl; 55 | execute_with_retry.setMetricsAdd(metricsAdd); 56 | } 57 | 58 | // Legacy API 59 | exports.add = metricsAdd; 60 | exports.set = metricsSet; 61 | exports.stats = metricsStats; 62 | exports.init = metricsInit; 63 | -------------------------------------------------------------------------------- /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 type { CmdDef } from 'glov/common/cmd_parse'; 3 | import { dotPropDelete, dotPropGet, dotPropSet } from 'glov/common/dot-prop'; 4 | import type { TSMap } from 'glov/common/types'; 5 | import type { ChannelServerWorker } from './channel_server_worker'; 6 | import type { ClientHandlerFunction } from './channel_worker'; 7 | import { 8 | globalWorkerAddCmd, 9 | globalWorkerRegisterClientHandler, 10 | } from './global_worker'; 11 | 12 | 13 | let global_data: Partial>; 14 | 15 | let csworker: ChannelServerWorker; 16 | export function serverGlobalsInit(csworker_in: ChannelServerWorker): void { 17 | assert(!csworker); 18 | csworker = csworker_in; 19 | } 20 | 21 | export type ServerGlobalOnDataCB = (csworker: ChannelServerWorker, data: T | undefined) => void; 22 | 23 | let on_data_cbs: { 24 | prefix: string; 25 | cb: ServerGlobalOnDataCB; 26 | }[] = []; 27 | 28 | function prefixMatches(prefix: string, key: string): boolean { 29 | // true if watching `foo.bar` and we get an update on `foo` or the other way around 30 | return key.startsWith(prefix) || prefix.startsWith(key); 31 | } 32 | 33 | export function serverGlobalsHandleChannelData(key: string, value: unknown): void { 34 | if (!key) { 35 | global_data = value as Partial>; 36 | } else { 37 | if (value === undefined) { 38 | dotPropDelete(global_data, key); 39 | } else { 40 | dotPropSet(global_data, key, value); 41 | } 42 | } 43 | for (let ii = 0; ii < on_data_cbs.length; ++ii) { 44 | let { prefix, cb } = on_data_cbs[ii]; 45 | if (prefixMatches(prefix, key)) { 46 | cb(csworker, dotPropGet(global_data, prefix)); 47 | } 48 | } 49 | } 50 | 51 | export function serverGlobalsReady(): boolean { 52 | return Boolean(global_data); 53 | } 54 | 55 | export function serverGlobalsGet(key: string): T | undefined; 56 | export function serverGlobalsGet(key: string, def: T): T; 57 | export function serverGlobalsGet(key: string, def?: T): T | undefined { 58 | return dotPropGet(global_data, key, def); 59 | } 60 | 61 | type ServerGlobalsDef = { 62 | // Note: callbacks here are ran in the context of the _ChannelServerWorker_ 63 | on_data?: ServerGlobalOnDataCB; 64 | // Note: commands here are ran in the context of the _GlobalWorker_ 65 | cmds?: CmdDef[]; 66 | client_handlers?: TSMap; 67 | }; 68 | 69 | export function serverGlobalsRegister(prefix: string, param: ServerGlobalsDef): void { 70 | if (param.on_data) { 71 | on_data_cbs.push({ 72 | prefix, 73 | cb: param.on_data as ServerGlobalOnDataCB, 74 | }); 75 | } 76 | if (param.cmds) { 77 | for (let ii = 0; ii < param.cmds.length; ++ii) { 78 | globalWorkerAddCmd(param.cmds[ii]); 79 | } 80 | } 81 | if (param.client_handlers) { 82 | for (let key in param.client_handlers) { 83 | globalWorkerRegisterClientHandler(key, param.client_handlers[key]!); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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 | 15 | let alphanum = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 16 | export function randAlphaNumericId(len: number): string { 17 | let buf = crypto.randomBytes(len); 18 | let ret = new Array(len); 19 | for (let ii = 0; ii < len; ++ii) { 20 | ret[ii] = alphanum[(ii ? 0 : 1) + floor(buf[ii]/256 * (ii ? 36 : 35))]; 21 | } 22 | return ret.join(''); 23 | } 24 | -------------------------------------------------------------------------------- /src/glov/server/serverfs.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { existsSync, readdirSync, readFileSync } from 'fs'; 3 | import path from 'path'; 4 | import type { FSAPI } from 'glov/common/fsapi'; 5 | import type { DataObject } from 'glov/common/types'; 6 | import { serverFilewatchOn } from './server_filewatch'; 7 | 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/tests/client/test-autocomplete.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/order:off */ 2 | import 'glov/client/test'; // Must be first 3 | 4 | import assert from 'assert'; 5 | import { cmdAutoCompleteMatchForTesting } from 'glov/client/cmd_auto_complete'; 6 | 7 | function test(user: string, cmd: string, expected: number): void { 8 | let actual = cmdAutoCompleteMatchForTesting(user, cmd); 9 | assert.equal(actual, expected, 10 | `('${user}', '${cmd}') returned ${actual} (expected: ${expected})`); 11 | } 12 | // exact match with parameters 13 | test('mycmd foo', 'my_cmd', 10); 14 | test('mycmd foo', 'my_cmd_two', 0); 15 | // prefix match 16 | test('mycmd', 'my_cmd', 10); 17 | test('myc_md', 'my_cmd', 10); 18 | test('mycmd', 'my_cmd_two', 9); 19 | test('myc_md', 'my_cmd_two', 9); 20 | // equal with transpose 21 | test('mcymd', 'my_cmd', 8); 22 | test('mcy_md', 'my_cmd', 8); 23 | test('mcy_md', 'my_cmd_two', 7); 24 | test('mcymd', 'my_cmd_two', 7); 25 | // rough match 26 | // weight bump 2 for prefix match 27 | // weight bump 1 for no transpose 28 | const PREFIX_EXACT = 6; 29 | const PREFIX_TRANSPOSE = 5; 30 | const ROUGH_EXACT = 4; 31 | const ROUGH_TRANSPOSE = 3; 32 | test('abcd', 'abXY_cdXY', PREFIX_EXACT); 33 | test('abcd', 'abcXY_bcdXY', ROUGH_EXACT); // c/should be PREFIX_EXACT 34 | test('abcd', 'acXY_bdXY', PREFIX_TRANSPOSE); 35 | test('abcd', 'X_bacdX', ROUGH_TRANSPOSE); // c/should be PREFIX_TRANSPOSE 36 | test('abcd', 'X_abdcX', ROUGH_TRANSPOSE); // c/should be PREFIX_TRANSPOSE 37 | test('abcd', 'babdc', ROUGH_TRANSPOSE); 38 | test('abcd', 'babcd', ROUGH_EXACT); 39 | test('abcd', 'abXY_efXY_cdXY', PREFIX_EXACT); 40 | test('abcd', 'aXbXY_cXdXY', ROUGH_EXACT); 41 | test('abcd', 'abXY_bcdXY', ROUGH_EXACT); // c/should be PREFIX_EXACT 42 | test('abcd', 'abXY_efXY_bcdXY', ROUGH_EXACT); // c/should be PREFIX_TRANSPOSE 43 | test('abcd', 'abXY_cbdXY', ROUGH_EXACT); // c/should be PREFIX_TRANSPOSE 44 | test('abcd', 'abXY_bacdXY', ROUGH_EXACT); // c/should be PREFIX_TRANSPOSE 45 | test('abcd', 'abXY_bdacXY', ROUGH_TRANSPOSE); 46 | -------------------------------------------------------------------------------- /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/client/test-markdown.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { 3 | MDASTNode, 4 | mdParse, 5 | mdParseSetValidRenderables, 6 | } from 'glov/client/markdown_parse'; 7 | 8 | function treeToText(tree: MDASTNode[]): string { 9 | let ret = ''; 10 | for (let ii = 0; ii < tree.length; ++ii) { 11 | let node = tree[ii]; 12 | if (node.type === 'text') { 13 | ret += node.content; 14 | } else if (node.type === 'paragraph') { 15 | ret += `${treeToText(node.content)}\n`; 16 | } else if (node.type === 'em' || node.type === 'strong') { 17 | ret += `<${node.type}>${treeToText(node.content)}`; 18 | } else if (node.type === 'renderable') { 19 | ret += `<${node.content.type}=${node.content.key}>`; 20 | // } else { 21 | // ret += ``; 22 | } 23 | } 24 | return ret; 25 | } 26 | 27 | let tree: MDASTNode[] = mdParse('FOO_BAR Here is [img=foo] [gt=ACCESS_AREA text="Access Areas"]' + 28 | ' [p=1] [img=foo scale=3 nostretch] [world=1234/info] [emoji=smile] and an *em**b**tag*.'); 29 | // console.log(JSON.stringify(tree, undefined, 2)); 30 | assert(Array.isArray(tree)); 31 | assert.equal(tree.length, 1); 32 | 33 | tree = mdParse('FOO\nbar\n\nbaz'); 34 | // console.log(JSON.stringify(tree, undefined, 2)); 35 | assert(Array.isArray(tree)); 36 | assert.equal(tree.length, 2); 37 | 38 | tree = mdParse('a :f'); 39 | assert.equal(treeToText(tree), 'a :f\n'); 40 | 41 | tree = mdParse('a +. f'); 42 | assert.equal(treeToText(tree), 'a +. f\n'); 43 | 44 | tree = mdParse('[img=foo] *bar*'); 45 | assert.equal(treeToText(tree), '[img=foo] bar\n'); 46 | 47 | mdParseSetValidRenderables({ img: true }); 48 | tree = mdParse('[img=foo] *bar*'); 49 | assert.equal(treeToText(tree), ' bar\n'); 50 | -------------------------------------------------------------------------------- /src/glov/tests/common/dummyfs.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import type { FilewatchCB, FSAPI } 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-common-types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | KeysMatching, 3 | KeysMatchingLoose, 4 | } from '../../common/types'; 5 | 6 | type TypesEqual = 7 | (() => G extends T ? 1 : 2) extends 8 | (() => G extends U ? 1 : 2) ? Y : N; 9 | 10 | type StaticAssert = X; 11 | 12 | type Test = { 13 | str: string; 14 | stropt?: string; 15 | strconst: 'a' | 'b'; 16 | strconstopt?: 'a' | 'b'; 17 | either: string | number; 18 | eitheropt?: string | number; 19 | num: number; 20 | numopt?: number; 21 | numconst: 1 | 2; 22 | numconstopt?: 1 | 2; 23 | }; 24 | // exactType(KeysMatching; 25 | export type Foo2 = KeysMatching; 26 | export type Foo3 = KeysMatching; 27 | export type Foo7 = KeysMatching; 28 | export type Foo4 = KeysMatchingLoose; 29 | export type Foo5 = KeysMatchingLoose; 30 | export type Foo6 = KeysMatchingLoose; 31 | export type Foo8 = KeysMatchingLoose; 32 | 33 | export type Tests = StaticAssert, 'str'>> | 34 | StaticAssert, 'either'>> | 35 | StaticAssert, 'num'>> | 36 | StaticAssert, 'numopt'>>; 37 | export type Tests2 = StaticAssert, 'str' | 'strconst'>> | 38 | StaticAssert, 39 | 'str' | 'strconst' | 'either' | 'num' | 'numconst'>> | 40 | StaticAssert, 'num' | 'numconst'>> | 41 | StaticAssert, 'num' | 'numopt' | 'numconst' | 'numconstopt'>>; 42 | -------------------------------------------------------------------------------- /src/glov/tests/common/test-traitstate.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { traitFactoryCreate, TypeDef } 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/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 | import * as assert from 'assert'; 2 | import * as fs from 'fs'; 3 | import * as http from 'http'; 4 | import * as https from 'https'; 5 | import * as path from 'path'; 6 | import * as express from 'express'; 7 | import * as express_static_gzip from 'express-static-gzip'; 8 | import { permTokenWorkerInit } from 'glov/server/perm_token_worker'; 9 | import { 10 | requestLogEverything, 11 | setupRequestHeaders, 12 | } from 'glov/server/request_utils'; 13 | import * as glov_server from 'glov/server/server'; 14 | import minimist from 'minimist'; 15 | import { entTestWorkerInit } from './enttest_worker'; 16 | import { multiplayerWorkerInit } from './multiplayer_worker'; 17 | 18 | const argv = minimist(process.argv.slice(2)); 19 | 20 | let app = express(); 21 | let server = http.createServer(app); 22 | 23 | let server_https; 24 | if (argv.dev) { 25 | if (fs.existsSync('debugkeys/localhost.crt')) { 26 | let https_options = { 27 | cert: fs.readFileSync('debugkeys/localhost.crt'), 28 | key: fs.readFileSync('debugkeys/localhost.key'), 29 | }; 30 | server_https = https.createServer(https_options, app); 31 | } 32 | } 33 | if (argv.requestlog) { 34 | requestLogEverything(app); 35 | } 36 | setupRequestHeaders(app, { 37 | dev: argv.dev, 38 | allow_map: true, 39 | }); 40 | 41 | function isDiscord(headers) { 42 | return headers.referrer && headers.referrer.includes('discord') || 43 | headers['cf-worker'] && headers['cf-worker'].includes('discord'); 44 | } 45 | 46 | if (argv.dev) { 47 | // In dev, `index.html` is unhashed, but we need to serve the hashed version 48 | // to the Discord proxy, otherwise it'll never load the new files 49 | app.get('/', function (req, res, next) { 50 | if (isDiscord(req.headers)) { 51 | assert(req.url.startsWith('/')); 52 | req.url = `/index_hashed.html${req.url.slice(1)}`; 53 | } 54 | next(); 55 | }); 56 | } 57 | 58 | app.use(express_static_gzip(path.join(__dirname, '../client/'), { 59 | enableBrotli: true, 60 | orderPreference: ['br'], 61 | })); 62 | 63 | app.use(express_static_gzip('data_store/public', { 64 | enableBrotli: true, 65 | orderPreference: ['br'], 66 | })); 67 | 68 | glov_server.startup({ 69 | app, 70 | server, 71 | server_https, 72 | }); 73 | 74 | // Opt-in to the permissions token system (Note: make sure config/server.json:forward_depth is correct!) 75 | permTokenWorkerInit(glov_server.channel_server, app); 76 | 77 | multiplayerWorkerInit(glov_server.channel_server); 78 | entTestWorkerInit(glov_server.channel_server); 79 | 80 | let port = argv.port || process.env.port || 3000; 81 | 82 | server.listen(port, () => { 83 | console.info(`Running server at http://localhost:${port}`); 84 | }); 85 | if (server_https) { 86 | let secure_port = argv.sport || process.env.sport || (port + 100); 87 | server_https.listen(secure_port, () => { 88 | console.info(`Running server at https://localhost:${secure_port}`); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /src/server/multiplayer_worker.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from 'glov/common/packet'; 2 | import { HandlerSource, NetResponseCallback } from 'glov/common/types'; 3 | import { ChannelServer } from 'glov/server/channel_server'; 4 | import { ChannelWorker } from 'glov/server/channel_worker'; 5 | import { chattableWorkerInit } from 'glov/server/chattable_worker'; 6 | 7 | class MultiplayerWorker extends ChannelWorker { 8 | // constructor(channel_server, channel_id, channel_data) { 9 | // super(channel_server, channel_id, channel_data); 10 | // } 11 | test_bin?: Uint8Array; 12 | } 13 | MultiplayerWorker.prototype.maintain_client_list = true; 14 | MultiplayerWorker.prototype.emit_join_leave_events = true; 15 | MultiplayerWorker.prototype.require_login = false; 16 | MultiplayerWorker.prototype.auto_destroy = true; 17 | 18 | chattableWorkerInit(MultiplayerWorker); 19 | 20 | MultiplayerWorker.registerClientHandler('bin_get', function ( 21 | this: MultiplayerWorker, 22 | src: HandlerSource, 23 | pak: Packet, 24 | resp_func: NetResponseCallback 25 | ): void { 26 | let resp = resp_func.pak(); 27 | resp.writeBuffer(this.test_bin || new Uint8Array(0)); 28 | resp.send(); 29 | }); 30 | 31 | MultiplayerWorker.registerClientHandler('bin_set', function ( 32 | this: MultiplayerWorker, 33 | src: HandlerSource, 34 | pak: Packet, 35 | resp_func: NetResponseCallback 36 | ): void { 37 | let buf = pak.readBuffer(false); 38 | if (buf.length > 100) { 39 | return void resp_func('Too big'); 40 | } 41 | this.test_bin = buf; 42 | resp_func(); 43 | }); 44 | 45 | export function multiplayerWorkerInit(channel_server: ChannelServer): void { 46 | channel_server.registerChannelWorker('multiplayer', MultiplayerWorker, { 47 | autocreate: true, 48 | subid_regex: /^.+$/, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/tests/client/test-demo-client.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import 'glov/client/test'; 3 | import { demoHelper } from '../common/demo_helper'; 4 | 5 | assert.equal(demoHelper(), 'foo'); 6 | -------------------------------------------------------------------------------- /src/tests/common/demo_helper.ts: -------------------------------------------------------------------------------- 1 | export function demoHelper(): string { 2 | return 'foo'; 3 | } 4 | -------------------------------------------------------------------------------- /src/tests/common/demo_ignored.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | assert(false); 4 | -------------------------------------------------------------------------------- /src/tests/common/test-demo-common.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import 'glov/server/test'; 3 | 4 | assert(true); 5 | -------------------------------------------------------------------------------- /src/tests/server/test-demo-server.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import 'glov/server/test'; 3 | 4 | console.log('Running test1...'); 5 | assert(1); 6 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------