Skip to content

Instantly share code, notes, and snippets.

@JamesTheAwesomeDude
Last active February 19, 2023 00:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JamesTheAwesomeDude/e025706778aeb5473ab28fc7bd78cb47 to your computer and use it in GitHub Desktop.
Save JamesTheAwesomeDude/e025706778aeb5473ab28fc7bd78cb47 to your computer and use it in GitHub Desktop.
Advance Wars By Web QoL fixes

This is a collection of small quality-of-life tweaks to improve the Advance Wars By Web experience. You can use just one, or all of them; it's up to personal preference.:

  • Pixel Art
    Changes the sprites to be crisp instead of blurry (only works on 2.0x and 3.0x Zoom)
  • Game Saver
    Automatically stores copies of all played games so you never lose them
  • Cap-Limit Info
    Show property percentages when creating a match with a cap-limit. Also shows "relative share" values when creating ≥3-player matches with cap limits.
  • Jade Sun Accesibility Fix
    Modified version of the Jade Sun sprites that's easier to identify (this is primarily for color-impaired users; I am not color-impaired, myself, so I'd love feedback in the comments!)
  • Page Title (DEPRECATED)
    Updates the page title so your browser history isn't a mess, and so you can tell the tabs apart

Obviously, any bug reports are welcome in the comments!

To install these, you must first install a userscript manager, then click the links above to install the scripts you want.

// ==UserScript==
// @name Advance Wars Cap-Limit Info
// @description Changes the Advance Wars match-creation page to show saner capture values
// @namespace .xyz.ishygddt
// @author squeegily
// @updateURL https://gist.githubusercontent.com/JamesTheAwesomeDude/e025706778aeb5473ab28fc7bd78cb47/raw/AWBW_CapLimInfo.user.js
// @version 0.4.4
// @grant none
// @match https://awbw.amarriner.com/create.php?*
// @run-at document-end
// ==/UserScript==
const propertyTypes = ['city', 'base', 'port', 'airport', 'ctower', 'lab'];
const nonCountedPropertyTypes = new Set(['ctower', 'lab']);
let buildingNumbers = document.querySelectorAll('table[style*="#EEEEEE"] .small_text_11');
let HQIcons = document.querySelectorAll('*[align="center"] > img[src*=logo]');
let nPlayers = HQIcons.length;
if( (buildingNumbers.length != 6) || (HQIcons.length < 2 || HQIcons.length > 16) ) throw new Error('Advance Wars Capture Helper was unable to identify the number of properties.');
let propertyCounts;
{
propertyCounts = new Map([['hq', HQIcons.length]]);
let t = propertyTypes[Symbol.iterator]();
for( let e of buildingNumbers ) {
propertyCounts.set(t.next().value, parseInt(e.textContent));
// FIXME: this is inaccurate on maps that have multiple HQs, or that have labs and HQs
}
if( (propertyCounts.get('lab') >= nPlayers) &&
((propertyCounts.get('lab') % nPlayers) == 0) ) {
console.warn('Warning: the number of labs is divisible by the number of players; assuming no HQs. Property count may be inaccurate.');
propertyCounts.set('hq', 0);
}
}
let n = 0;
for( let [type, amount] of propertyCounts.entries() ) {
if( nonCountedPropertyTypes.has(type) ) continue;
n += amount;
}
console.info(`There are ${n} properties on this map.`, propertyCounts);
for( let e of document.querySelector('select[name="capture"]')) {
let m = parseInt(e.value);
if( m == 1000 ) continue;
if( m > n ) {
e.disabled = true;
continue;
}
e.textContent = `${m} (${((m * 100) / n).toFixed(1)}%)`;
if( nPlayers != 2 ) {
e.textContent += ` \{${(HQIcons.length * m / n).toFixed(2)}\}`;
}
}
// ==UserScript==
// @name Advance Wars Game Saver
// @description Saves Advance Wars By Web games automatically
// @namespace .xyz.ishygddt
// @author squeegily
// @updateURL https://gist.githubusercontent.com/JamesTheAwesomeDude/e025706778aeb5473ab28fc7bd78cb47/raw/AWBW_GameSaver.user.js
// @version 0.9.2
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.listValues
// @grant GM.deleteValue
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @match https://awbw.amarriner.com/yourgames.php
// @run-at document-end
// ==/UserScript==
async function main() {
const db = await db_v1();
try {
await MIGRATE_GM_TO_IDB(db);
} catch (error) {
// Don't re-raise, but do log
console.error(error);
}
// Archive existing non-archived games
let gamesTable = getElementByXPath('//text()[contains(.,"Completed Games")]/ancestor::tr[1]/following-sibling::tr//*[tr]');
let gameRows = Array.from(getElementsByXPath('./tr[not(*[contains(@class,"header")])]', gamesTable));
let gameLinks = gameRows.map(e => new URL(e.querySelector('a[href*="ndx="]').href));
let gameIds = gameLinks.map(u => parseInt(u.searchParams.get('games_id')));
for( let id of gameIds ) {
if( await get_key(db, 'games', id) ) continue;
await put_value(db, 'games', id, await getReplay(id));
}
// Render list of archived games
let archivedGames = document.createElement('div');
archivedGames.appendChild(document.createElement('div')).textContent = "Archived games:";
let ul = archivedGames.appendChild(document.createElement('ul'));
for( let id of await get_allKeys(db, 'games') ){
// TODO: dynamically fetch these instead of unconditionally preloading them
// the status quo demands an inappropriate amount of RAM
let fname = `${id}.awbwreplay`;
let replay = await get_value(db, 'games', id);
console.info(replay);
let a = document.createElement('a');
let u = URL.createObjectURL(replay);
a.textContent = id;
a.download = fname; // this shouldn't be necessary, but it is...
a.href = u;
ul.appendChild(document.createElement('li')).appendChild(a);
}
gamesTable.parentElement.appendChild(archivedGames);
}
async function get_key(db, os_name, key) {
return new Promise((resolve, reject) => {
const txn = db.transaction(os_name);
const os = txn.objectStore(os_name);
const req = os.getKey(key);
req.onsuccess = ({target: {result}}) => resolve(result);
req.onerror = ({target: {error}}) => reject(error);
});
}
async function get_value(db, os_name, key) {
return new Promise((resolve, reject) => {
const txn = db.transaction(os_name);
const os = txn.objectStore(os_name);
const req = os.get(key);
req.onsuccess = ({target: {result}}) => resolve(result);
req.onerror = ({target: {error}}) => reject(error);
});
}
async function put_value(db, os_name, key, value) {
return new Promise((resolve, reject) => {
const txn = db.transaction(os_name, "readwrite");
const os = txn.objectStore(os_name);
const req = os.put(value, key);
req.onsuccess = ({target: {result}}) => resolve(result);
req.onerror = ({target: {error}}) => reject(error);
});
}
async function get_allKeys(db, os_name) {
return new Promise((resolve, reject) => {
const txn = db.transaction(os_name);
const os = txn.objectStore(os_name);
const req = os.getAllKeys();
req.onsuccess = ({target: {result}}) => resolve(result);
req.onerror = ({target: {error}}) => reject(error);
});
}
async function blobToDataURL(b) {
return new Promise((res, err) => {
const fr = new FileReader();
fr.onload = ev => res(ev.target.result);
fr.onerror = err;
fr.readAsDataURL(b);
});
}
function dataURLToBlob(u, name=null) {
let [m, type, data] = u.match(/^data:([^\/]*\/[^;]*);base64,(.*)$/);
data = atob(data);
let arr = new Uint8Array(data.length);
for( let i in arr ) {
arr[i] = data.charCodeAt(i);
}
if( name !== null ) {
return new File([arr], name, {type: type});
} else {
return new Blob([arr], {type: type});
}
}
async function getReplay(games_id) {
let u = new URL('https://awbw.amarriner.com/replay_download.php');
u.searchParams.set('games_id', games_id);
let r = fetch(u);
console.info("Downloading:", u.toString());
r = await r;
let b = await r.blob(); // TODO: use the File API instead?
return b.slice(0, b.size, "application/awbw-replay");
}
function getElementByXPath(path, context=undefined, document=undefined) {
if (context === undefined) context = window.document;
if (document === undefined) document = context.getRootNode();
const result = document.evaluate(path, context, document, XPathResult.FIRST_ORDERED_NODE_TYPE);
return result.singleNodeValue || null;
}
function getElementsByXPath(path, context=undefined, document=undefined, snapshot=false) {
if (context === undefined) context = window.document;
if (document === undefined) document = context.getRootNode();
if (snapshot) {
const result = document.evaluate(path, context, document, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
return Array.from({ [Symbol.iterator] () {var i = 0; return { next() {var value = result.snapshotItem(i++); return { value, done: !value };} };} });
}
var result = document.evaluate(path, context, document, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
return ({ next() {var value = result.iterateNext(); return { value, done: !value };}, [Symbol.iterator] () {return this;} });
}
function getTextByXPath(path, context=null, _document=undefined) {
let e = getElementByXPath(path, context, _document);
if( !e ) return undefined;
return e.textContent;
}
async function _assert_persist() {
if (navigator.storage && navigator.storage.persist) {
if (await navigator.storage.persist())
return;
}
throw new Error("Persistent storage is not enabled.");
}
async function MIGRATE_GM_TO_IDB(db) {
console.debug("BEGINNING MIGRATION...");
await _assert_persist();
if (!('getValue' in (GM || {})))
throw new Error("COULD NOT ACCESS OLD USER DATA FOR MIGRATION!");
if (db.version !== 1)
throw new Error("DB FORMAT TOO NEW, DON'T UNDERSTAND HOW TO MIGRATE!");
for (let old_key of await GM.listValues()) {
let m = old_key.match(/^replay_(\d+)$/);
if (!m) {
console.warn("MIGRATION SCRIPT SKIPPING UNRECOGNIZED KEY", old_key);
continue;
}
let games_id = parseInt(m[1]);
let blob = await GM.getValue(old_key).then(u => dataURLToBlob(u));
let os = db.transaction('games', "readwrite").objectStore('games');
os.put(blob, games_id);
}
}
async function db_v1(name="GameSaver") {
return new Promise((resolve, reject) => {
const req = this.window.indexedDB.open(name, 1);
req.onsuccess = ({target: {result}}) => resolve(result);
req.onerror = ({target: {error}}) => reject(error);
req.onabort = ({target}) => reject(target);
req.onupgradeneeded = ({target: {result: db}, oldVersion, newVersion}) => {
//if (oldVersion < 0) throw new Error("database too old!");
if (oldVersion > 0) throw new Error("database too new!");
/* v1 SCHEMA */
db.createObjectStore('games');
};
});
}
main()
.then(result => result !== undefined ? console.info(result) : undefined)
.catch(error => {console.error(error); alert(`Error: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`);});
// ==UserScript==
// @name Advance Wars Jade Sun
// @description Changes the Jade Sun sprites from Advance Wars by Web to be easier to recognize
// @namespace .xyz.ishygddt
// @author squeegily
// @updateURL https://gist.githubusercontent.com/JamesTheAwesomeDude/e025706778aeb5473ab28fc7bd78cb47/raw/AWBW_JadeSunAccessibility.user.js
// @version 0.1
// @grant GM.addStyle
// @match https://awbw.amarriner.com/*
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @run-at document-start
// ==/UserScript==
GM.addStyle(`
img[src*="/jadesun"] {
filter: saturate(1.5);
}
img[src*="/js"] {
filter: brightness(1.125) saturate(1.25);
}
`);
// ==UserScript==
// @name Advance Wars Pixel Art
// @description Changes the Advance Wars by Web sprites to have crisp edges instead of a blurry upscale
// @namespace .xyz.ishygddt
// @author squeegily
// @updateURL https://gist.githubusercontent.com/JamesTheAwesomeDude/e025706778aeb5473ab28fc7bd78cb47/raw/AWBW_Pixelart.user.js
// @version 2.1.0
// @grant GM.addStyle
// @grant GM_addStyle
// @match https://awbw.amarriner.com/*?*
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js#md5=3dda97488a907c2bf30e1bbc1e94b848
// @run-at document-end
// ==/UserScript==
/* I RECOMMEND USING 2.0x or 3.0x ZOOM WHEN USING THIS */
GM.addStyle(`#gamemap:not([style*="."]) {
image-rendering: pixelated; /* Chrome */
image-rendering: -moz-crisp-edges; /* Firefox */
-ms-interpolation-mode: nearest-neighbor; /* IE/Edge */
}`);
function patchCrystallizedJitter() {
//console.info("Removing stored jitter...");
if (window.localStorage.hasOwnProperty('scale')) {
var originalScale = parseFloat(window.localStorage.getItem('scale'));
window.localStorage.setItem('scale', originalScale.toFixed(5) - 1e-10);
}
}
(async () => {
let documentComplete = new Promise(res => {
if(document.readyState == "complete") return res();
window.addEventListener("load", res, {once: true});
});
await documentComplete;
patchCrystallizedJitter();
})();
// ==UserScript==
// @name Advance Wars Page Title
// @description Changes the Advance Wars page titles so you can sort out your tabs and have a usable browser history
// @namespace .xyz.ishygddt
// @author squeegily
// @updateURL https://gist.githubusercontent.com/JamesTheAwesomeDude/e025706778aeb5473ab28fc7bd78cb47/raw/AWBW_Title.user.js
// @version 0.8.2
// @grant none
// @match https://awbw.amarriner.com/*
// @run-at document-end
// ==/UserScript==
// https://www.ishygddt.xyz/~blog/2021/07/javascript-get-textcontent-by-xpath-or-query-selector
function queryText(selector) {
try { return document.querySelector(selector).textContent; }
catch { return undefined; }
}
function xpathText(path, context=null) {
try { return document.evaluate(path, document, context, XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue.textContent; }
catch { return undefined; }
}
// https://www.ishygddt.xyz/~blog/2021/06/javascript-parsing-query-parameters
function _parse_qs_array(search) {
let m = search.match(/^\?(.+)$/s);
let params = m ? m[1].split('&') : new Array();
return params.map(s => {
let [key, value] = s.match(/^(.*?)(?:=(.*))?$/s).slice(1);
key = decodeURIComponent(key.replaceAll('+', '%20'));
value = (value !== undefined ) ? decodeURIComponent(value.replaceAll('+', '%20')) : null;
return [key, value];
});
}
function parse_qs_map(search=window.location.search) {
return new Map(_parse_qs_array(search));
}
let orig_document_title = document.title;
function meta_title(prefix) {
document.title = [prefix, orig_document_title].join(' - ');
}
switch(window.location.pathname) {
case '/yourgames.php':
meta_title('Your Games');
break;
case '/yourturn.php': {
let e = document.querySelector('.yt-alert');
let notifPrefix = `(${e.textContent.trim()}) `;
if( orig_document_title.startsWith(notifPrefix) ) {
orig_document_title = orig_document_title.replace(notifPrefix, "");
meta_title(notifPrefix + 'Your Turn Games');
} else {
meta_title(`Your Turn Games`);
}
} break;
case '/prevmaps.php': {
let map_name = queryText('.bordertitle');
meta_title(`Map Preview: ${map_name}`);
} break;
case '/2030.php':
case '/game.php': {
let g_name = queryText('.game-header-header>*');
let map_name = queryText('.game-header-info a[href*="map"]');
let view_mode = parse_qs_map(window.location.search).has('ndx') ? 'Replay' : 'Game';
meta_title(`${view_mode}: "${g_name}" (${map_name})`);
} break;
case '/create.php': {
let map_name = queryText('#main a[href*="maps_id"]');
meta_title(`Create Game: ${map_name}`);
} break;
case '/join.php': {
let game_name = xpathText('//*[text()="Game"]//ancestor::table[1]//span/*[text()]');
let map_name = xpathText('//a[starts-with(@href,"prevmaps.php")]/text()[normalize-space()]');
meta_title(`Join Game: "${game_name}" (${map_name})`);
} break;
case '/messages.php':
meta_title('Your Messages');
break;
case '/gameswait.php':
meta_title(queryText('#main .bordertitle'));
break;
case '/press.php': {
let game_name = queryText(`#main .bordertitle a[href*="games_id"]`);
meta_title(`Press Messages: "${game_name}"`);
} break;
case '/sendmessage.php': {
let username = parse_qs_map(window.location.search).get('to_username');
if( username ) meta_title(`Send Message to ${username}`);
else meta_title('Send Message');
} break;
case '/viewmessage.php': {
let sender = xpathText("//*[text()='From:']/parent::*//*[text()!='From:']");
let subject = (xpathText("//*[text()='Subject:']/parent::*/text()") || "undefined").trim();
let msg_type = 'Message';
if( subject.startsWith('Game Invite - ') ){
subject = subject.replace('Game Invite - ', '');
msg_type = 'Game Invite';
}
meta_title(`${msg_type} from ${sender}: "${subject}"`);
} break;
case '/friends.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`Friends (${username})`);
} break;
case '/following.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`Followed Games (${username})`);
} break;
case '/profile.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`Profile: ${username}`);
} break;
//TODO '/gameswait.php'
case '/changelog.php':
meta_title('Changelog');
break;
case '/chart.php':
meta_title('Charts');
break;
case '/co.php':
meta_title('CO Chart');
break;
case '/percent.php':
meta_title('CO Percentage Chart');
break;
case '/countries.php':
meta_title('Countries');
break;
case '/damage.php':
meta_title('Damage Chart');
break;
case '/terrain.php':
meta_title('Terrain Chart');
break;
case '/terrain_map.php':
meta_title('Terrain Map');
break;
case '/units.php':
meta_title('Unit Map');
break;
case '/userinfo.php':
meta_title('Settings');
break;
case '/history.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`History for ${username}`);
} break;
case '/newleague.php':
meta_title('AWBW Global League');
break;
case '/newleague_standings.php':
meta_title(xpathText('//td[contains(@class, "bordertitle")]/parent::tr').trim());
break;
case '/newleague_profile.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`AWBW Global League Profile: ${username}`);
} break;
case '/viewtournament.php': {
let tournament_name;
if( parse_qs_map(window.location.search).has('tournaments_id') ) {
tournament_name = queryText('.bordertitle');
meta_title(`Tournament: ${tournament_name}`);
} else {
meta_title('Tournaments');
}
} break;
case '/tourney_profile.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`Tournament Profile: ${username}`);
} break;
case '/design_map.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`Design Maps by ${username}`);
} break;
case '/categories.php':
meta_title(queryText('td.bordertitle b'));
break;
case '/favorites.php': {
let username = parse_qs_map(window.location.search).get('username');
meta_title(`Favorite Maps (${username})`);
} break;
}
@JamesTheAwesomeDude
Copy link
Author

TODO: nearest-neighbor works perfectly for integer scaling values, but it's still awkward-looking for non-integer values. I've got to either figure out how to get browsers to use the next-most-trivial algorithm, box sampling, or just make SVG versions of all the AWBW sprites and set the interpolation directives appropriately

@JamesTheAwesomeDude
Copy link
Author

TODO: notify the user in some way when it's their turn


draft / the code will look something like this

// this'll work?
async function _decorateWindowedFunction(f, name) {
    let originalFunction = window[name];
    window[name] = await f(originalFunction);
}

_decorateWindowedFunction(filteredEndTurnHandlerInterceptorNotificationSenderDecorator, endTurnHandler);

...

async function filteredEndTurnHandlerInterceptorNotificationSenderDecorator(endTurnHandler) {
    if( await Notification.requestPermission() != "granted" ) throw new Error("Permission not granted; aborting notification wrapper");
    return function wrappedEndTurnHandler(...arguments) {
        let endTurnRes = arguments[0];
        if( (endTurnRes ? endTurnRes.nextPId : null) == viewerPId ) {
            let noot = new Notification('Your turn', {
                body: JSON.stringify(endTurnRes),
                icon: '/favicon.ico',
                requireInteraction: document.hidden,
            });
            noot.onclick = () => window.focus();
        }
        endTurnHandler(...arguments);
    }
}

@JamesTheAwesomeDude
Copy link
Author

@JamesTheAwesomeDude
Copy link
Author

Fw: https://discord.com/channels/313453805150928906/314370192098459649/1035191640190615572

semi-joke suggestion but can we have the option to follow players?

like say i follow VoA and now im updated on all their league games?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment