diff options
Diffstat (limited to 'webAO')
| -rw-r--r-- | webAO/__tests__/iniParse.test.js | 47 | ||||
| -rw-r--r-- | webAO/client.html | 2 | ||||
| -rw-r--r-- | webAO/client.js | 318 | ||||
| -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.js | 2 | ||||
| -rw-r--r-- | webAO/iniParse.js | 42 | ||||
| -rw-r--r-- | webAO/master.js | 51 | ||||
| -rw-r--r-- | webAO/services/request.js | 32 | ||||
| -rw-r--r-- | webAO/sounds.js | 132 | ||||
| -rw-r--r-- | webAO/utils/calculateGifLength.js | 27 | ||||
| -rw-r--r-- | webAO/utils/calculateWebpLength.js | 22 | ||||
| -rw-r--r-- | webAO/utils/calculatorHandler.js | 7 | ||||
| -rw-r--r-- | webAO/utils/getCookie.js | 26 | ||||
| -rw-r--r-- | webAO/utils/setCookie.js | 10 |
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, '>') 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; |
