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