|
// ==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)}`);}); |
TODO: notify the user in some way when it's their turn
draft / the code will look something like this
...