aboutsummaryrefslogtreecommitdiff
path: root/webAO
diff options
context:
space:
mode:
Diffstat (limited to 'webAO')
-rw-r--r--webAO/__tests__/iniParse.test.js47
-rw-r--r--webAO/client.html2
-rw-r--r--webAO/client.js318
-rw-r--r--webAO/constants/backgrounds.js (renamed from webAO/backgrounds.js)0
-rw-r--r--webAO/constants/characters.js (renamed from webAO/characters.js)0
-rw-r--r--webAO/constants/evidence.js (renamed from webAO/evidence.js)0
-rw-r--r--webAO/constants/music.js (renamed from webAO/music.js)0
-rw-r--r--webAO/encoding.js2
-rw-r--r--webAO/iniParse.js42
-rw-r--r--webAO/master.js51
-rw-r--r--webAO/services/request.js32
-rw-r--r--webAO/sounds.js132
-rw-r--r--webAO/utils/calculateGifLength.js27
-rw-r--r--webAO/utils/calculateWebpLength.js22
-rw-r--r--webAO/utils/calculatorHandler.js7
-rw-r--r--webAO/utils/getCookie.js26
-rw-r--r--webAO/utils/setCookie.js10
17 files changed, 343 insertions, 375 deletions
diff --git a/webAO/__tests__/iniParse.test.js b/webAO/__tests__/iniParse.test.js
new file mode 100644
index 0000000..deb08f3
--- /dev/null
+++ b/webAO/__tests__/iniParse.test.js
@@ -0,0 +1,47 @@
+import iniParse from '../iniParse';
+
+const iniExample = `
+[Options]
+name = Matt
+showname = Matty
+
+[Emotions]
+number = 9
+1 = Normal#-#normal#0#1
+`;
+describe('iniParse', () => {
+ test('should not lowercase value if key is showname', () => {
+ const parsedIni = iniParse(`
+ [test]
+ showname = MATT
+ `);
+ expect(parsedIni.test.showname).toBe('MATT');
+ });
+ test('should lowercase value if key is not showname', () => {
+ const parsedIni = iniParse(`
+ [test]
+ party = TIME
+ `);
+ expect(parsedIni.test.party).toBe('time');
+ });
+ test('should parse sections', () => {
+ const parsedIni = iniParse(iniExample);
+ expect(Object.keys(parsedIni).length).toBe(2);
+ });
+ test('should parse parameters', () => {
+ const parsedIni = iniParse(iniExample);
+ expect(Object.keys(parsedIni.options).length).toBe(2);
+ });
+ test('should remove empty lines', () => {
+ const parsedIni = iniParse(`
+ [test]
+
+
+ 1 = 1
+ 2 = 2
+
+
+ `);
+ expect(Object.keys(parsedIni.test).length).toBe(2);
+ });
+});
diff --git a/webAO/client.html b/webAO/client.html
index e03e1b6..13b2de4 100644
--- a/webAO/client.html
+++ b/webAO/client.html
@@ -81,7 +81,7 @@
<img id="client_court_prot" onerror="imgError(this);">
<img id="client_court_pro" onerror="imgError(this);">
</div>
- <img id="client_court" onerror="switchPanTilt(2);" onload="switchPanTilt(1);" onerror="imgError(this);">
+ <img id="client_court" onerror="switchPanTilt(2);" onload="switchPanTilt(1);">
<div id="client_def_pair_char" class="client_char" alt="Paired character">
<img id="client_def_pair_gif" onerror="charError(this);">
<img id="client_def_pair_png" onerror="charError(this);">
diff --git a/webAO/client.js b/webAO/client.js
index 7ff561b..b6a4c38 100644
--- a/webAO/client.js
+++ b/webAO/client.js
@@ -4,20 +4,25 @@
* credits to aleks for original idea and source
*/
-import Fingerprint2 from 'fingerprintjs2';
+import FingerprintJS from '@fingerprintjs/fingerprintjs';
import { EventEmitter } from 'events';
import {
- escapeChat, encodeChat, prepChat, safe_tags,
+ escapeChat, encodeChat, prepChat, safeTags,
} from './encoding.js';
// Load some defaults for the background and evidence dropdowns
-import vanilla_character_arr from './characters.js';
-import vanilla_music_arr from './music.js';
-import vanilla_background_arr from './backgrounds.js';
-import vanilla_evidence_arr from './evidence.js';
+import vanilla_character_arr from './constants/characters.js';
+import vanilla_music_arr from './constants/music.js';
+import vanilla_background_arr from './constants/backgrounds.js';
+import vanilla_evidence_arr from './constants/evidence.js';
import chatbox_arr from './styles/chatbox/chatboxes.js';
+import iniParse from './iniParse';
+import calculatorHandler from './utils/calculatorHandler.js';
+import getCookie from './utils/getCookie.js';
+import setCookie from './utils/setCookie.js';
+import request from './services/request.js';
const version = process.env.npm_package_version;
@@ -30,8 +35,7 @@ location.search.substr(1).split('&').forEach((item) => {
queryDict[item.split('=')[0]] = item.split('=')[1];
});
-const serverIP = queryDict.ip;
-let { mode } = queryDict;
+let { ip: serverIP, mode } = queryDict;
// Unless there is an asset URL specified, use the wasabi one
const DEFAULT_HOST = 'http://attorneyoffline.de/base/';
@@ -49,51 +53,35 @@ const transparentPNG = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCA
let oldLoading = false;
// presettings
-const selectedEffect = 0;
let selectedMenu = 1;
let selectedShout = 0;
let extrafeatures = [];
let hdid;
-const options = { fonts: { extendedJsFonts: true, userDefinedFonts: ['Ace Attorney', '8bitoperator', 'DINEngschrift'] }, excludes: { userAgent: true, enumerateDevices: true } };
function isLowMemory() {
if (/webOS|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Kindle|Silk|PlayStation|Nintendo|Opera Mini/i.test(navigator.userAgent)) {
oldLoading = true;
}
}
+const fpPromise = FingerprintJS.load();
+fpPromise
+ .then((fp) => fp.get())
+ .then((result) => {
+ hdid = result.visitorId;
+ client = new Client(serverIP);
+ viewport = new Viewport();
-if (window.requestIdleCallback) {
- requestIdleCallback(() => {
- Fingerprint2.get(options, (components) => {
- hdid = Fingerprint2.x64hash128(components.reduce((a, b) => `${a.value || a}, ${b.value}`), 31);
- client = new Client(serverIP);
- viewport = new Viewport();
-
- isLowMemory();
- client.loadResources();
- });
+ isLowMemory();
+ client.loadResources();
});
-} else {
- setTimeout(() => {
- Fingerprint2.get(options, (components) => {
- hdid = Fingerprint2.x64hash128(components.reduce((a, b) => `${a.value || a}, ${b.value}`), 31);
- client = new Client(serverIP);
- viewport = new Viewport();
-
- isLowMemory();
- client.loadResources();
- });
- }, 500);
-}
let lastICMessageTime = new Date(0);
class Client extends EventEmitter {
constructor(address) {
super();
- console.log(`mode: ${mode}`);
if (mode !== 'replay') {
this.serv = new WebSocket(`ws://${address}`);
// Assign the websocket events
@@ -621,7 +609,7 @@ class Client extends EventEmitter {
document.getElementById('client_inner_chat').innerHTML = '';
const char_id = Number(args[9]);
- const char_name = safe_tags(args[3]);
+ const char_name = safeTags(args[3]);
let msg_nameplate = args[3];
let msg_blips = 'male';
@@ -649,21 +637,21 @@ class Client extends EventEmitter {
if (char_muted === false) {
let chatmsg = {
- deskmod: safe_tags(args[1]).toLowerCase(),
- preanim: safe_tags(args[2]).toLowerCase(), // get preanim
+ deskmod: safeTags(args[1]).toLowerCase(),
+ preanim: safeTags(args[2]).toLowerCase(), // get preanim
nameplate: msg_nameplate,
chatbox: char_chatbox,
name: char_name,
- sprite: safe_tags(args[4]).toLowerCase(),
+ sprite: safeTags(args[4]).toLowerCase(),
content: prepChat(args[5]), // Escape HTML tags
side: args[6].toLowerCase(),
- sound: safe_tags(args[7]).toLowerCase(),
- blips: safe_tags(msg_blips),
+ sound: safeTags(args[7]).toLowerCase(),
+ blips: safeTags(msg_blips),
type: Number(args[8]),
charid: char_id,
snddelay: Number(args[10]),
objection: Number(args[11]),
- evidence: safe_tags(args[12]),
+ evidence: safeTags(args[12]),
flip: Number(args[13]),
flash: Number(args[14]),
color: Number(args[15]),
@@ -671,10 +659,10 @@ class Client extends EventEmitter {
if (extrafeatures.includes('cccc_ic_support')) {
const extra_cccc = {
- showname: safe_tags(args[16]),
+ showname: safeTags(args[16]),
other_charid: Number(args[17]),
- other_name: safe_tags(args[18]),
- other_emote: safe_tags(args[19]),
+ other_name: safeTags(args[18]),
+ other_emote: safeTags(args[19]),
self_offset: args[20].split('<and>'), // HACK: here as well, client is fucked and uses this instead of &
other_offset: args[21].split('<and>'),
other_flip: Number(args[22]),
@@ -686,9 +674,9 @@ class Client extends EventEmitter {
const extra_27 = {
looping_sfx: Number(args[24]),
screenshake: Number(args[25]),
- frame_screenshake: safe_tags(args[26]),
- frame_realization: safe_tags(args[27]),
- frame_sfx: safe_tags(args[28]),
+ frame_screenshake: safeTags(args[26]),
+ frame_realization: safeTags(args[27]),
+ frame_sfx: safeTags(args[28]),
};
chatmsg = Object.assign(extra_27, chatmsg);
@@ -843,7 +831,7 @@ class Client extends EventEmitter {
// If the ini doesn't exist on the server this will throw an error
try {
const cinidata = await request(`${AO_HOST}characters/${encodeURI(chargs[0].toLowerCase())}/char.ini`);
- cini = INI.parse(cinidata);
+ cini = iniParse(cinidata);
} catch (err) {
cini = {};
img.classList.add('noini');
@@ -852,9 +840,9 @@ class Client extends EventEmitter {
}
const mute_select = document.getElementById('mute_select');
- mute_select.add(new Option(safe_tags(chargs[0]), charid));
+ mute_select.add(new Option(safeTags(chargs[0]), charid));
const pair_select = document.getElementById('pair_select');
- pair_select.add(new Option(safe_tags(chargs[0]), charid));
+ pair_select.add(new Option(safeTags(chargs[0]), charid));
// sometimes ini files lack important settings
const default_options = {
@@ -874,13 +862,13 @@ class Client extends EventEmitter {
cini.emotions = Object.assign(default_emotions, cini.emotions);
this.chars[charid] = {
- name: safe_tags(chargs[0]),
- showname: safe_tags(cini.options.showname),
- desc: safe_tags(chargs[1]),
- blips: safe_tags(cini.options.blips).toLowerCase(),
- gender: safe_tags(cini.options.gender).toLowerCase(),
- side: safe_tags(cini.options.side).toLowerCase(),
- chat: (cini.options.chat === '') ? safe_tags(cini.options.chat).toLowerCase() : safe_tags(cini.options.category).toLowerCase(),
+ name: safeTags(chargs[0]),
+ showname: safeTags(cini.options.showname),
+ desc: safeTags(chargs[1]),
+ blips: safeTags(cini.options.blips).toLowerCase(),
+ gender: safeTags(cini.options.gender).toLowerCase(),
+ side: safeTags(cini.options.side).toLowerCase(),
+ chat: (cini.options.chat === '') ? safeTags(cini.options.chat).toLowerCase() : safeTags(cini.options.category).toLowerCase(),
evidence: chargs[3],
icon,
inifile: cini,
@@ -890,7 +878,7 @@ class Client extends EventEmitter {
if (this.chars[charid].blips === '') { this.chars[charid].blips = this.chars[charid].gender; }
const iniedit_select = document.getElementById('client_ininame');
- iniedit_select.add(new Option(safe_tags(chargs[0])));
+ iniedit_select.add(new Option(safeTags(chargs[0])));
} else {
console.warn(`missing charid ${charid}`);
const img = document.getElementById(`demo_${charid}`);
@@ -960,7 +948,7 @@ class Client extends EventEmitter {
this.evidences[i - 1] = {
name: prepChat(arg[0]),
desc: prepChat(arg[1]),
- filename: safe_tags(arg[2]),
+ filename: safeTags(arg[2]),
icon: `${AO_HOST}evidence/${encodeURI(arg[2].toLowerCase())}`,
};
}
@@ -1043,15 +1031,8 @@ class Client extends EventEmitter {
}
isAudio(trackname) {
- if (trackname.endsWith('.wav')
- || trackname.endsWith('.mp3')
- || trackname.endsWith('.mp4')
- || trackname.endsWith('.ogg')
- || trackname.endsWith('.opus')) // NOT category markers
- {
- return true;
- }
- return false;
+ const audioEndings = ['.wav', '.mp3', '.ogg', '.opus'];
+ return audioEndings.filter((ending) => trackname.endsWith(ending)).length === 1;
}
addTrack(trackname) {
@@ -1116,7 +1097,7 @@ class Client extends EventEmitter {
for (let i = 2; i < args.length - 1; i++) {
if (i % 2 === 0) {
document.getElementById('client_loadingtext').innerHTML = `Loading Music ${args[1]}/${this.music_list_length}`;
- const trackname = safe_tags(args[i]);
+ const trackname = safeTags(args[i]);
const trackindex = args[i - 1];
if (this.musics_time) {
this.addTrack(trackname);
@@ -1147,7 +1128,7 @@ class Client extends EventEmitter {
for (let i = 1; i < args.length - 1; i++) {
// Check when found the song for the first time
- const trackname = safe_tags(args[i]);
+ const trackname = safeTags(args[i]);
const trackindex = i - 1;
document.getElementById('client_loadingtext').innerHTML = `Loading Music ${i}/${this.music_list_length}`;
if (this.musics_time) {
@@ -1174,7 +1155,7 @@ class Client extends EventEmitter {
for (let i = 1; i < args.length - 1; i++) {
// Check when found the song for the first time
- this.addTrack(safe_tags(args[i]));
+ this.addTrack(safeTags(args[i]));
}
}
@@ -1186,7 +1167,7 @@ class Client extends EventEmitter {
this.resetAreaList();
for (let i = 1; i < args.length - 1; i++) {
- this.createArea(i - 1, safe_tags(args[i]));
+ this.createArea(i - 1, safeTags(args[i]));
}
}
@@ -1215,7 +1196,7 @@ class Client extends EventEmitter {
* @param {Array} args kick reason
*/
handleKK(args) {
- this.handleBans('Kicked', safe_tags(args[1]));
+ this.handleBans('Kicked', safeTags(args[1]));
}
/**
@@ -1224,7 +1205,7 @@ class Client extends EventEmitter {
* @param {Array} args ban reason
*/
handleKB(args) {
- this.handleBans('Banned', safe_tags(args[1]));
+ this.handleBans('Banned', safeTags(args[1]));
this.banned = true;
}
@@ -1234,7 +1215,7 @@ class Client extends EventEmitter {
* @param {Array} args ban reason
*/
handleBB(args) {
- alert(safe_tags(args[1]));
+ alert(safeTags(args[1]));
}
/**
@@ -1243,7 +1224,7 @@ class Client extends EventEmitter {
* @param {Array} args ban reason
*/
handleBD(args) {
- this.handleBans('Banned', safe_tags(args[1]));
+ this.handleBans('Banned', safeTags(args[1]));
this.banned = true;
}
@@ -1267,7 +1248,7 @@ class Client extends EventEmitter {
* @param {Array} args packet arguments
*/
handleBN(args) {
- viewport.bgname = safe_tags(args[1]);
+ viewport.bgname = safeTags(args[1]);
const bgfolder = viewport.bgFolder;
const bg_index = getIndexFromSelect('bg_select', viewport.bgname);
document.getElementById('bg_select').selectedIndex = bg_index;
@@ -1438,13 +1419,13 @@ class Client extends EventEmitter {
this.areas[i].players = Number(args[i + 1]);
break;
case 1: // status
- this.areas[i].status = safe_tags(args[i + 1]);
+ this.areas[i].status = safeTags(args[i + 1]);
break;
case 2:
- this.areas[i].cm = safe_tags(args[i + 1]);
+ this.areas[i].cm = safeTags(args[i + 1]);
break;
case 3:
- this.areas[i].locked = safe_tags(args[i + 1]);
+ this.areas[i].locked = safeTags(args[i + 1]);
break;
}
@@ -1624,7 +1605,7 @@ class Client extends EventEmitter {
iniedit_select.innerHTML = '';
function addIniswap(value) {
- iniedit_select.add(new Option(safe_tags(value)));
+ iniedit_select.add(new Option(safeTags(value)));
}
addIniswap(me.name);
@@ -1888,7 +1869,7 @@ class Viewport {
}
if ('def,pro,wit'.includes(position)) {
- bench.style.display = 'none';
+ document.getElementById(`client_${position}_bench`).style.display = 'none';
view.style.display = '';
document.getElementById('client_classicview').style.display = 'none';
switch (position) {
@@ -1942,13 +1923,19 @@ class Viewport {
* silently fail and return 0 instead.
* @param {string} filename the animation file name
*/
- async getAnimLength(filename) {
- try {
- const file = await requestBuffer(filename);
- return this.calculateGifLength(file);
- } catch (err) {
- return 0;
+
+ async getAnimLength(url) {
+ const extensions = ['.gif', '.webp'];
+ for (const extension of extensions) {
+ const urlWithExtension = url + extension;
+ const exists = await fileExists(urlWithExtension);
+ if (exists) {
+ const fileBuffer = await requestBuffer(urlWithExtension);
+ const length = calculatorHandler[extension](fileBuffer);
+ return length;
+ }
}
+ return 0;
}
oneSuccess(promises) {
@@ -2052,19 +2039,30 @@ class Viewport {
const png_s = document.getElementById(`client_${position}${pairID}_png`);
const apng_s = document.getElementById(`client_${position}${pairID}_apng`);
const webp_s = document.getElementById(`client_${position}${pairID}_webp`);
+ const extensionsMap = {
+ '.gif': gif_s,
+ '.png': png_s,
+ '.apng': apng_s,
+ '.webp': webp_s,
+ };
- if (this.lastChar !== this.chatmsg.name) {
- // hide the last sprite
- gif_s.src = transparentPNG;
- png_s.src = transparentPNG;
- apng_s.src = transparentPNG;
- webp_s.src = transparentPNG;
+ for (const [extension, htmlElement] of Object.entries(extensionsMap)) {
+ // Hides all sprites before creating a new sprite
+ if (this.lastChar !== this.chatmsg.name) {
+ htmlElement.src = transparentPNG;
+ }
+ let url;
+ if (extension === '.png') {
+ url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(emotename)}${extension}`;
+ } else {
+ url = `${characterFolder}${encodeURI(charactername)}/${encodeURI(prefix)}${encodeURI(emotename)}${extension}`;
+ }
+ const exists = fileExistsSync(url);
+ if (exists) {
+ htmlElement.src = url;
+ return;
+ }
}
-
- gif_s.src = `${characterFolder}${encodeURI(charactername)}/${encodeURI(prefix)}${encodeURI(emotename)}.gif`;
- png_s.src = `${characterFolder}${encodeURI(charactername)}/${encodeURI(emotename)}.png`;
- apng_s.src = `${characterFolder}${encodeURI(charactername)}/${encodeURI(prefix)}${encodeURI(emotename)}.apng`;
- webp_s.src = `${characterFolder}${encodeURI(charactername)}/${encodeURI(prefix)}${encodeURI(emotename)}.webp`;
}
/**
@@ -2167,7 +2165,7 @@ class Viewport {
// Hide message box
chatContainerBox.style.opacity = 0;
// If preanim existed then determine the length
- gifLength = await this.getAnimLength(`${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/${encodeURI(this.chatmsg.preanim)}.gif`);
+ gifLength = await this.getAnimLength(`${AO_HOST}characters/${encodeURI(this.chatmsg.name.toLowerCase())}/${encodeURI(this.chatmsg.preanim)}`);
this.chatmsg.startspeaking = false;
break;
// case 5:
@@ -2325,9 +2323,10 @@ class Viewport {
this.chatmsg.startspeaking = true;
} else if (this.textTimer >= this.shoutTimer + this.chatmsg.preanimdelay && !this.chatmsg.startpreanim) {
if (this.chatmsg.startspeaking) {
+ // Evidence Bullshit
if (this.chatmsg.evidence > 0) {
// Prepare evidence
- eviBox.src = safe_tags(client.evidences[this.chatmsg.evidence - 1].icon);
+ eviBox.src = safeTags(client.evidences[this.chatmsg.evidence - 1].icon);
eviBox.style.width = 'auto';
eviBox.style.height = '36.5%';
@@ -2410,78 +2409,6 @@ class Viewport {
}
}
-class INI {
- static parse(data) {
- const regex = {
- section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
- param: /^\s*([\w.\-_]+)\s*=\s*(.*?)\s*$/,
- comment: /^\s*;.*$/,
- };
- const value = {};
- const lines = data.split(/\r\n|\r|\n/);
- let section;
- lines.forEach((line) => {
- if (regex.comment.test(line)) {
-
- } else if (line.length === 0) {
-
- } else if (regex.param.test(line)) {
- const match = line.match(regex.param);
- if (section) {
- if (match[1].toLowerCase() === 'showname') { // don't lowercase the showname
- value[section].showname = match[2];
- } else {
- value[section][match[1].toLowerCase()] = match[2].toLowerCase();
- }
- // } else { // we don't care about attributes without a section
- // value[match[1]] = match[2];
- }
- } else if (regex.section.test(line)) {
- const match = line.match(regex.section);
- value[match[1].toLowerCase()] = {}; // lowercase everything else
- section = match[1].toLowerCase();
- }
- });
- return value;
- }
-}
-
-/**
- * read a cookie from storage
- * got this from w3schools
- * https://www.w3schools.com/js/js_cookies.asp
- * @param {String} cname The name of the cookie to return
- */
-function getCookie(cname) {
- try {
- const name = `${cname}=`;
- const decodedCookie = decodeURIComponent(document.cookie);
- const ca = decodedCookie.split(';');
- for (let i = 0; i < ca.length; i++) {
- let c = ca[i];
- while (c.charAt(0) === ' ') {
- c = c.substring(1);
- }
- if (c.indexOf(name) === 0) {
- return c.substring(name.length, c.length);
- }
- }
- return '';
- } catch (error) {
- return '';
- }
-}
-
-/**
- * set a cookie
- * the version from w3schools expects these to expire
- * @param {String} cname The name of the cookie to return
- * @param {String} value The value of that cookie option
- */
-function setCookie(cname, value) {
- document.cookie = `${cname}=${value}`;
-}
-
/**
* Triggered when the Return key is pressed on the out-of-character chat input box.
* @param {KeyboardEvent} event
@@ -2768,8 +2695,10 @@ export async function switchPanTilt(addcheck) {
const background = document.getElementById('client_fullview');
if (addcheck === 1) {
document.getElementById('client_pantilt').checked = true;
+ document.getElementById('client_court').style.display = '';
} else if (addcheck === 2) {
document.getElementById('client_pantilt').checked = false;
+ document.getElementById('client_court').style.display = 'none';
}
if (document.getElementById('client_pantilt').checked) {
background.style.transition = '0.5s ease-in-out';
@@ -2847,7 +2776,6 @@ window.imgError = imgError;
* @param {HTMLImageElement} image the element containing the missing sound
*/
export function opusCheck(channel) {
- console.info(channel);
console.info(`failed to load sound ${channel.src}`);
let oldsrc = '';
oldsrc = channel.src;
@@ -2894,40 +2822,6 @@ async function requestBuffer(url) {
}
/**
- * Make a GET request for a specific URI.
- * @param {string} url the URI to be requested
- * @returns response data
- * @throws {Error} if status code is not 2xx, or a network error occurs
- */
-async function request(url) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.responseType = 'text';
- xhr.addEventListener('error', () => {
- const err = new Error(`Request for ${url} failed: ${xhr.statusText}`);
- err.code = xhr.status;
- reject(err);
- });
- xhr.addEventListener('abort', () => {
- const err = new Error(`Request for ${url} was aborted!`);
- err.code = xhr.status;
- reject(err);
- });
- xhr.addEventListener('load', () => {
- if (xhr.status < 200 || xhr.status >= 300) {
- const err = new Error(`Request for ${url} failed with status code ${xhr.status}`);
- err.code = xhr.status;
- reject(err);
- } else {
- resolve(xhr.response);
- }
- });
- xhr.open('GET', url, true);
- xhr.send();
- });
-}
-
-/**
* Checks if a file exists at the specified URI.
* @param {string} url the URI to be checked
*/
@@ -2939,6 +2833,16 @@ async function fileExists(url) {
return false;
}
}
+const fileExistsSync = (url) => {
+ try {
+ const http = new XMLHttpRequest();
+ http.open('HEAD', url, false);
+ http.send();
+ return http.status != 404;
+ } catch (e) {
+ return false;
+ }
+};
/**
* Triggered when the reconnect button is pushed.
diff --git a/webAO/backgrounds.js b/webAO/constants/backgrounds.js
index c1807ce..c1807ce 100644
--- a/webAO/backgrounds.js
+++ b/webAO/constants/backgrounds.js
diff --git a/webAO/characters.js b/webAO/constants/characters.js
index e046cf8..e046cf8 100644
--- a/webAO/characters.js
+++ b/webAO/constants/characters.js
diff --git a/webAO/evidence.js b/webAO/constants/evidence.js
index 900455e..900455e 100644
--- a/webAO/evidence.js
+++ b/webAO/constants/evidence.js
diff --git a/webAO/music.js b/webAO/constants/music.js
index 09d830b..09d830b 100644
--- a/webAO/music.js
+++ b/webAO/constants/music.js
diff --git a/webAO/encoding.js b/webAO/encoding.js
index 21b5aa7..e6cc3ae 100644
--- a/webAO/encoding.js
+++ b/webAO/encoding.js
@@ -28,7 +28,7 @@ export function unescapeChat(estring) {
* XXX: This is unnecessary if we use `createTextNode` instead!
* @param {string} unsafe an unsanitized string
*/
-export function safe_tags(unsafe) {
+export function safeTags(unsafe) {
if (unsafe) {
return unsafe
.replace(/>/g, '&gt;')
diff --git a/webAO/iniParse.js b/webAO/iniParse.js
new file mode 100644
index 0000000..fb04e67
--- /dev/null
+++ b/webAO/iniParse.js
@@ -0,0 +1,42 @@
+const regexPatterns = {
+ section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
+ param: /^\s*([\w.\-_]+)\s*=\s*(.*?)\s*$/,
+ comment: /^\s*;.*$/,
+};
+
+const valueHandler = (matchKey, matchValue) => (matchKey === 'showname' ? matchValue : matchValue.toLowerCase());
+
+const lineFilter = (value) => {
+ const isEmpty = value.length === 0;
+ const isComment = regexPatterns.comment.test(value);
+ if (isComment || isEmpty) {
+ return false;
+ }
+ return true;
+};
+
+const iniParse = (data) => {
+ const parsedIni = {};
+ const lines = data.split(/\r\n|\r|\n/);
+ const filteredLines = lines.filter(lineFilter);
+
+ let currentSection;
+ filteredLines.forEach((line) => {
+ const isParameter = regexPatterns.param.test(line);
+ const isSection = regexPatterns.section.test(line);
+ if (isParameter && currentSection) {
+ const match = line.match(regexPatterns.param);
+ const matchKey = match[1].toLowerCase();
+ const matchValue = match[2];
+ parsedIni[currentSection][matchKey] = valueHandler(matchKey, matchValue);
+ } else if (isSection) {
+ const match = line.match(regexPatterns.section);
+ const matchKey = match[1].toLowerCase();
+ parsedIni[matchKey] = {};
+ currentSection = matchKey;
+ }
+ });
+ return parsedIni;
+};
+
+export default iniParse;
diff --git a/webAO/master.js b/webAO/master.js
index b2bf0b6..7e4906e 100644
--- a/webAO/master.js
+++ b/webAO/master.js
@@ -1,6 +1,6 @@
-import Fingerprint2 from 'fingerprintjs2';
+import FingerprintJS from '@fingerprintjs/fingerprintjs';
-import { unescapeChat, safe_tags } from './encoding.js';
+import { unescapeChat, safeTags } from './encoding.js';
const version = process.env.npm_package_version;
@@ -21,39 +21,22 @@ servers[-1] = {
name: 'Localhost', description: 'This is your computer on port 50001', ip: '127.0.0.1', port: 50001, assets: '', online: 'Online: ?/?',
};
-if (window.requestIdleCallback) {
- requestIdleCallback(() => {
- Fingerprint2.get(options, (components) => {
- hdid = Fingerprint2.x64hash128(components.reduce((a, b) => `${a.value || a}, ${b.value}`), 31);
+const fpPromise = FingerprintJS.load();
+fpPromise
+ .then((fp) => fp.get())
+ .then((result) => {
+ hdid = result.visitorId;
- check_https();
+ check_https();
- masterserver = new WebSocket(`ws://${MASTERSERVER_IP}`);
- masterserver.onopen = (evt) => onOpen(evt);
- masterserver.onerror = (evt) => onError(evt);
- masterserver.onmessage = (evt) => onMessage(evt);
+ masterserver = new WebSocket(`ws://${MASTERSERVER_IP}`);
+ masterserver.onopen = (evt) => onOpen(evt);
+ masterserver.onerror = (evt) => onError(evt);
+ masterserver.onmessage = (evt) => onMessage(evt);
- // i don't need the ms to play alone
- setTimeout(() => checkOnline(-1, '127.0.0.1:50001'), 0);
- });
+ // i don't need the ms to play alone
+ setTimeout(() => checkOnline(-1, '127.0.0.1:50001'), 0);
});
-} else {
- setTimeout(() => {
- Fingerprint2.get(options, (components) => {
- hdid = Fingerprint2.x64hash128(components.reduce((a, b) => `${a.value || a}, ${b.value}`), 31);
-
- check_https();
-
- masterserver = new WebSocket(`ws://${MASTERSERVER_IP}`);
- masterserver.onopen = (evt) => onOpen(evt);
- masterserver.onerror = (evt) => onError(evt);
- masterserver.onmessage = (evt) => onMessage(evt);
-
- // i don't need the ms to play alone
- setTimeout(() => checkOnline(-1, '127.0.0.1:50001'), 0);
- });
- }, 500);
-}
export function check_https() {
if (document.location.protocol === 'https:') {
@@ -67,7 +50,7 @@ export function setServ(ID) {
if (document.getElementById(`server${ID}`).className === '') { checkOnline(ID, `${servers[ID].ip}:${servers[ID].port}`); }
if (servers[ID].description !== undefined) {
- document.getElementById('serverdescription_content').innerHTML = `<b>${servers[ID].online}</b><br>${safe_tags(servers[ID].description)}`;
+ document.getElementById('serverdescription_content').innerHTML = `<b>${servers[ID].online}</b><br>${safeTags(servers[ID].description)}`;
} else {
document.getElementById('serverdescription_content').innerHTML = '';
}
@@ -124,7 +107,7 @@ function checkOnline(serverID, coIP) {
oserv.close();
return;
}
- if (serverID === selectedServer) { document.getElementById('serverdescription_content').innerHTML = `<b>${servers[serverID].online}</b><br>${safe_tags(servers[serverID].description)}`; }
+ if (serverID === selectedServer) { document.getElementById('serverdescription_content').innerHTML = `<b>${servers[serverID].online}</b><br>${safeTags(servers[serverID].description)}`; }
}
// assign the callbacks
@@ -162,7 +145,7 @@ function onMessage(e) {
const asset = args[4] ? `&asset=${args[4]}` : '';
document.getElementById('masterlist').innerHTML
- += `<li id="server${i}" onmouseover="setServ(${i})"><p>${safe_tags(servers[i].name)}</p>`
+ += `<li id="server${i}" onmouseover="setServ(${i})"><p>${safeTags(servers[i].name)}</p>`
+ `<a class="button" href="client.html?mode=watch&ip=${ipport}${asset}">Watch</a>`
+ `<a class="button" href="client.html?mode=join&ip=${ipport}${asset}">Join</a></li>`;
}
diff --git a/webAO/services/request.js b/webAO/services/request.js
new file mode 100644
index 0000000..6479dcc
--- /dev/null
+++ b/webAO/services/request.js
@@ -0,0 +1,32 @@
+/**
+ * Make a GET request for a specific URI.
+ * @param {string} url the URI to be requested
+ * @returns response data
+ * @throws {Error} if status code is not 2xx, or a network error occurs
+ */
+const request = async (url) => new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.responseType = 'text';
+ xhr.addEventListener('error', () => {
+ const err = new Error(`Request for ${url} failed: ${xhr.statusText}`);
+ err.code = xhr.status;
+ reject(err);
+ });
+ xhr.addEventListener('abort', () => {
+ const err = new Error(`Request for ${url} was aborted!`);
+ err.code = xhr.status;
+ reject(err);
+ });
+ xhr.addEventListener('load', () => {
+ if (xhr.status < 200 || xhr.status >= 300) {
+ const err = new Error(`Request for ${url} failed with status code ${xhr.status}`);
+ err.code = xhr.status;
+ reject(err);
+ } else {
+ resolve(xhr.response);
+ }
+ });
+ xhr.open('GET', url, true);
+ xhr.send();
+});
+export default request;
diff --git a/webAO/sounds.js b/webAO/sounds.js
deleted file mode 100644
index 0b7c8a7..0000000
--- a/webAO/sounds.js
+++ /dev/null
@@ -1,132 +0,0 @@
-export default [
- 'sfx-anotherpuzzle.wav',
- 'sfx-answeris.wav',
- 'sfx-ashamed.wav',
- 'sfx-badum.wav',
- 'sfx-bang.wav',
- 'sfx-bip.wav',
- 'sfx-bip2.wav',
- 'sfx-bip3.wav',
- 'sfx-bleep.wav',
- 'sfx-bling2.wav',
- 'sfx-blink.wav',
- 'sfx-blipfemale.wav',
- 'sfx-bliplayton.wav',
- 'sfx-blipmale.wav',
- 'sfx-bvvt.wav',
- 'sfx-cancel.wav',
- 'sfx-cello.wav',
- 'sfx-chocolatemilk.wav',
- 'sfx-chug.wav',
- 'sfx-crackle.wav',
- 'sfx-crowdgoeswhile.wav',
- 'sfx-damage1.wav',
- 'sfx-damage2.wav',
- 'sfx-deskslam.wav',
- 'sfx-dink.wav',
- 'sfx-dog bark.wav',
- 'sfx-dooropens.wav',
- 'sfx-dramapound.wav',
- 'sfx-everypuzzle.wav',
- 'sfx-evidenceshoop.wav',
- 'sfx-explosion.wav',
- 'sfx-explosive.wav',
- 'sfx-fan.wav',
- 'sfx-furio.wav',
- 'sfx-fwashing.wav',
- 'sfx-fwasshing.wav',
- 'sfx-gallery.wav',
- 'sfx-gallerycheer.wav',
- 'sfx-gavel.wav',
- 'sfx-grinding.wav',
- 'sfx-grinding2.wav',
- 'sfx-guilty.wav',
- 'sfx-guitar.wav',
- 'sfx-guitar2.wav',
- 'sfx-gunshot.wav',
- 'sfx-gunshot2.wav',
- 'sfx-gunshot3.wav',
- 'sfx-gunshot4.wav',
- 'sfx-happyping.wav',
- 'sfx-hint.wav',
- 'sfx-incomingpuzzle.wav',
- 'sfx-lightbang.wav',
- 'sfx-lightbulb.wav',
- 'sfx-lock.wav',
- 'sfx-locksbreak.wav',
- 'sfx-longwoosh.wav',
- 'sfx-losepuzzle.wav',
- 'sfx-loudbang.wav',
- 'sfx-maaagical.wav',
- 'sfx-maaagical2.wav',
- 'sfx-maaagical3.wav',
- 'sfx-meow.wav',
- 'sfx-nosale.wav',
- 'sfx-objection.wav',
- 'sfx-pageturn.wav',
- 'sfx-peep.wav',
- 'sfx-peep2.wav',
- 'sfx-phone1.wav',
- 'sfx-phone2.wav',
- 'sfx-phone3.wav',
- 'sfx-phone4.wav',
- 'sfx-phone5.wav',
- 'sfx-phonebeep.wav',
- 'sfx-photosnap.wav',
- 'sfx-pichoop.wav',
- 'sfx-powder.wav',
- 'sfx-psyche1.wav',
- 'sfx-realization.wav',
- 'sfx-roar.wav',
- 'sfx-saveblip.wav',
- 'sfx-scenechange.wav',
- 'sfx-scroll.wav',
- 'sfx-selectblip.wav',
- 'sfx-selectblip2.wav',
- 'sfx-selectjingle.wav',
- 'sfx-shattering.wav',
- 'sfx-shock.wav',
- 'sfx-shock2.wav',
- 'sfx-shook.wav',
- 'sfx-shook2.wav',
- 'sfx-shoomp.wav',
- 'sfx-shooop.wav',
- 'sfx-sirens.wav',
- 'sfx-smack.wav',
- 'sfx-spritz.wav',
- 'sfx-squee.wav',
- 'sfx-stab.wav',
- 'sfx-stab2.wav',
- 'sfx-supershock.wav',
- 'sfx-swing.wav',
- 'sfx-swoop.wav',
- 'sfx-swoop2.wav',
- 'sfx-tap.wav',
- 'sfx-tap2.wav',
- 'sfx-tap3.wav',
- 'sfx-testimony.wav',
- 'sfx-testimony2.wav',
- 'sfx-thud.wav',
- 'sfx-thud2.wav',
- 'sfx-thud3.wav',
- 'sfx-thud4.wav',
- 'sfx-thump.wav',
- 'sfx-thump2.wav',
- 'sfx-thump3.wav',
- 'sfx-thwap.wav',
- 'sfx-tong.wav',
- 'sfx-triplegavel.wav',
- 'sfx-typwriter.wav',
- 'sfx-weakgunshot.wav',
- 'sfx-whack.wav',
- 'sfx-wham.wav',
- 'sfx-whimper.wav',
- 'sfx-whip.wav',
- 'sfx-whoops.wav',
- 'sfx-winpuzzle.wav',
- 'sfx-wipe.wav',
- 'sfx-wubs.wav',
- 'sfx-yep.wav',
- 'sfx-yip.wav',
- 'sfx-zooma.wav',
-];
diff --git a/webAO/utils/calculateGifLength.js b/webAO/utils/calculateGifLength.js
new file mode 100644
index 0000000..1df0ba9
--- /dev/null
+++ b/webAO/utils/calculateGifLength.js
@@ -0,0 +1,27 @@
+/**
+ * Adds up the frame delays to find out how long a GIF is
+ * I totally didn't steal this
+ * @param {data} gifFile the GIF data
+ */
+const calculateGifLength = (gifFile) => {
+ const d = new Uint8Array(gifFile);
+ // Thanks to http://justinsomnia.org/2006/10/gif-animation-duration-calculation/
+ // And http://www.w3.org/Graphics/GIF/spec-gif89a.txt
+ let duration = 0;
+ for (let i = 0; i < d.length; i++) {
+ // Find a Graphic Control Extension hex(21F904__ ____ __00)
+ if (d[i] === 0x21
+ && d[i + 1] === 0xF9
+ && d[i + 2] === 0x04
+ && d[i + 7] === 0x00) {
+ // Swap 5th and 6th bytes to get the delay per frame
+ const delay = (d[i + 5] << 8) | (d[i + 4] & 0xFF);
+
+ // Should be aware browsers have a minimum frame delay
+ // e.g. 6ms for IE, 2ms modern browsers (50fps)
+ duration += delay < 2 ? 10 : delay;
+ }
+ }
+ return duration * 10;
+};
+export default calculateGifLength;
diff --git a/webAO/utils/calculateWebpLength.js b/webAO/utils/calculateWebpLength.js
new file mode 100644
index 0000000..1b422a0
--- /dev/null
+++ b/webAO/utils/calculateWebpLength.js
@@ -0,0 +1,22 @@
+const calculateWebpLength = (webpFile) => {
+ const d = new Uint8Array(webpFile);
+ // https://developers.google.com/speed/webp/docs/riff_container#animation
+ let duration = 0;
+ for (let i = 0; i < d.length; i++) {
+ // Find ANMF header (41 4E 4D 46)
+ if (d[i] === 0x41
+ && d[i + 1] === 0x4E
+ && d[i + 2] === 0x4D
+ && d[i + 3] === 0x46) {
+ // Swap 5th and 6th bytes to get the delay per frame
+ const delay = (d[i + 21] << 8) | (d[i + 20] & 0xFF);
+
+ // Should be aware browsers have a minimum frame delay
+ // e.g. 6ms for IE, 2ms modern browsers (50fps)
+ duration += delay < 2 ? 10 : delay;
+ }
+ }
+ return duration;
+};
+
+export default calculateWebpLength;
diff --git a/webAO/utils/calculatorHandler.js b/webAO/utils/calculatorHandler.js
new file mode 100644
index 0000000..7686573
--- /dev/null
+++ b/webAO/utils/calculatorHandler.js
@@ -0,0 +1,7 @@
+import calculateGifLength from './calculateGifLength';
+import calculateWebpLength from './calculateWebpLength';
+
+export default {
+ '.gif': calculateGifLength,
+ '.webp': calculateWebpLength,
+};
diff --git a/webAO/utils/getCookie.js b/webAO/utils/getCookie.js
new file mode 100644
index 0000000..3be0733
--- /dev/null
+++ b/webAO/utils/getCookie.js
@@ -0,0 +1,26 @@
+/**
+ * read a cookie from storage
+ * got this from w3schools
+ * https://www.w3schools.com/js/js_cookies.asp
+ * @param {String} cname The name of the cookie to return
+ */
+const getCookie = (cname) => {
+ try {
+ const name = `${cname}=`;
+ const decodedCookie = decodeURIComponent(document.cookie);
+ const ca = decodedCookie.split(';');
+ for (let i = 0; i < ca.length; i++) {
+ let c = ca[i];
+ while (c.charAt(0) === ' ') {
+ c = c.substring(1);
+ }
+ if (c.indexOf(name) === 0) {
+ return c.substring(name.length, c.length);
+ }
+ }
+ return '';
+ } catch (error) {
+ return '';
+ }
+};
+export default getCookie;
diff --git a/webAO/utils/setCookie.js b/webAO/utils/setCookie.js
new file mode 100644
index 0000000..9734eae
--- /dev/null
+++ b/webAO/utils/setCookie.js
@@ -0,0 +1,10 @@
+/**
+ * set a cookie
+ * the version from w3schools expects these to expire
+ * @param {String} cname The name of the cookie to return
+ * @param {String} value The value of that cookie option
+ */
+const setCookie = (cname, value) => {
+ document.cookie = `${cname}=${value}`;
+};
+export default setCookie;