UnityのWebGLのリソース更新ロジック
27444 ワード
直接コードを貼りました
// md5 function
!function (n) { "use strict"; function t(n, t) { var r = (65535 & n) + (65535 & t); return (n >> 16) + (t >> 16) + (r >> 16) << 16 | 65535 & r } function r(n, t) { return n << t | n >>> 32 - t } function e(n, e, o, u, c, f) { return t(r(t(t(e, n), t(u, f)), c), o) } function o(n, t, r, o, u, c, f) { return e(t & r | ~t & o, n, t, u, c, f) } function u(n, t, r, o, u, c, f) { return e(t & o | r & ~o, n, t, u, c, f) } function c(n, t, r, o, u, c, f) { return e(t ^ r ^ o, n, t, u, c, f) } function f(n, t, r, o, u, c, f) { return e(r ^ (t | ~o), n, t, u, c, f) } function i(n, r) { n[r >> 5] |= 128 << r % 32, n[14 + (r + 64 >>> 9 << 4)] = r; var e, i, a, d, h, l = 1732584193, g = -271733879, v = -1732584194, m = 271733878; for (e = 0; e < n.length; e += 16)i = l, a = g, d = v, h = m, g = f(g = f(g = f(g = f(g = c(g = c(g = c(g = c(g = u(g = u(g = u(g = u(g = o(g = o(g = o(g = o(g, v = o(v, m = o(m, l = o(l, g, v, m, n[e], 7, -680876936), g, v, n[e + 1], 12, -389564586), l, g, n[e + 2], 17, 606105819), m, l, n[e + 3], 22, -1044525330), v = o(v, m = o(m, l = o(l, g, v, m, n[e + 4], 7, -176418897), g, v, n[e + 5], 12, 1200080426), l, g, n[e + 6], 17, -1473231341), m, l, n[e + 7], 22, -45705983), v = o(v, m = o(m, l = o(l, g, v, m, n[e + 8], 7, 1770035416), g, v, n[e + 9], 12, -1958414417), l, g, n[e + 10], 17, -42063), m, l, n[e + 11], 22, -1990404162), v = o(v, m = o(m, l = o(l, g, v, m, n[e + 12], 7, 1804603682), g, v, n[e + 13], 12, -40341101), l, g, n[e + 14], 17, -1502002290), m, l, n[e + 15], 22, 1236535329), v = u(v, m = u(m, l = u(l, g, v, m, n[e + 1], 5, -165796510), g, v, n[e + 6], 9, -1069501632), l, g, n[e + 11], 14, 643717713), m, l, n[e], 20, -373897302), v = u(v, m = u(m, l = u(l, g, v, m, n[e + 5], 5, -701558691), g, v, n[e + 10], 9, 38016083), l, g, n[e + 15], 14, -660478335), m, l, n[e + 4], 20, -405537848), v = u(v, m = u(m, l = u(l, g, v, m, n[e + 9], 5, 568446438), g, v, n[e + 14], 9, -1019803690), l, g, n[e + 3], 14, -187363961), m, l, n[e + 8], 20, 1163531501), v = u(v, m = u(m, l = u(l, g, v, m, n[e + 13], 5, -1444681467), g, v, n[e + 2], 9, -51403784), l, g, n[e + 7], 14, 1735328473), m, l, n[e + 12], 20, -1926607734), v = c(v, m = c(m, l = c(l, g, v, m, n[e + 5], 4, -378558), g, v, n[e + 8], 11, -2022574463), l, g, n[e + 11], 16, 1839030562), m, l, n[e + 14], 23, -35309556), v = c(v, m = c(m, l = c(l, g, v, m, n[e + 1], 4, -1530992060), g, v, n[e + 4], 11, 1272893353), l, g, n[e + 7], 16, -155497632), m, l, n[e + 10], 23, -1094730640), v = c(v, m = c(m, l = c(l, g, v, m, n[e + 13], 4, 681279174), g, v, n[e], 11, -358537222), l, g, n[e + 3], 16, -722521979), m, l, n[e + 6], 23, 76029189), v = c(v, m = c(m, l = c(l, g, v, m, n[e + 9], 4, -640364487), g, v, n[e + 12], 11, -421815835), l, g, n[e + 15], 16, 530742520), m, l, n[e + 2], 23, -995338651), v = f(v, m = f(m, l = f(l, g, v, m, n[e], 6, -198630844), g, v, n[e + 7], 10, 1126891415), l, g, n[e + 14], 15, -1416354905), m, l, n[e + 5], 21, -57434055), v = f(v, m = f(m, l = f(l, g, v, m, n[e + 12], 6, 1700485571), g, v, n[e + 3], 10, -1894986606), l, g, n[e + 10], 15, -1051523), m, l, n[e + 1], 21, -2054922799), v = f(v, m = f(m, l = f(l, g, v, m, n[e + 8], 6, 1873313359), g, v, n[e + 15], 10, -30611744), l, g, n[e + 6], 15, -1560198380), m, l, n[e + 13], 21, 1309151649), v = f(v, m = f(m, l = f(l, g, v, m, n[e + 4], 6, -145523070), g, v, n[e + 11], 10, -1120210379), l, g, n[e + 2], 15, 718787259), m, l, n[e + 9], 21, -343485551), l = t(l, i), g = t(g, a), v = t(v, d), m = t(m, h); return [l, g, v, m] } function a(n) { var t, r = "", e = 32 * n.length; for (t = 0; t < e; t += 8)r += String.fromCharCode(n[t >> 5] >>> t % 32 & 255); return r } function d(n) { var t, r = []; for (r[(n.length >> 2) - 1] = void 0, t = 0; t < r.length; t += 1)r[t] = 0; var e = 8 * n.length; for (t = 0; t < e; t += 8)r[t >> 5] |= (255 & n.charCodeAt(t / 8)) << t % 32; return r } function h(n) { return a(i(d(n), 8 * n.length)) } function l(n, t) { var r, e, o = d(n), u = [], c = []; for (u[15] = c[15] = void 0, o.length > 16 && (o = i(o, 8 * n.length)), r = 0; r < 16; r += 1)u[r] = 909522486 ^ o[r], c[r] = 1549556828 ^ o[r]; return e = i(u.concat(d(t)), 512 + 8 * t.length), a(i(c.concat(e), 640)) } function g(n) { var t, r, e = ""; for (r = 0; r < n.length; r += 1)t = n.charCodeAt(r), e += "0123456789abcdef".charAt(t >>> 4 & 15) + "0123456789abcdef".charAt(15 & t); return e } function v(n) { return unescape(encodeURIComponent(n)) } function m(n) { return h(v(n)) } function p(n) { return g(m(n)) } function s(n, t) { return l(v(n), v(t)) } function C(n, t) { return g(s(n, t)) } function A(n, t, r) { return t ? r ? s(t, n) : C(t, n) : r ? m(n) : p(n) } "function" == typeof define && define.amd ? define(function () { return A }) : "object" == typeof module && module.exports ? module.exports = A : n.md5 = A }(this);
const GameConfig = (function () {
let _baseUrl = window.location.href.replace(/(https?.*)\/[^\/]*/, "$1");
let _bundleUrl = `${_baseUrl}/StreamingAssets/AssetsBundles`;
let _DB_NAME = '/idbfs';
let _DB_STORE = "FILE_DATA";
let _BASE_DIR = `/idbfs/${md5(_baseUrl)}`;
let _bundleDir = `${_BASE_DIR}/AssetsBundles`;
return {
get baseUrl() { return _baseUrl; },
get bundleUrl() { return _bundleUrl; },
get DB_NAME() { return _DB_NAME; },
get DB_STORE() { return _DB_STORE; },
get DB_BASE_DIR() { return _BASE_DIR; },
get DB_BundleDir() { return _bundleDir; }
};
})();
let EmUtil = (function () {
// download files from net
function _netGet(url, type, param) {
// Return a new promise.
return new Promise(function (resolve, reject) {
// Do the usual XHR stuff
let req = new XMLHttpRequest();
req.open('GET', url);
req.responseType = type || "arraybuffer"; // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
req.onload = function () {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
if (param) {
resolve({ val: req.response, param: param });
} else {
resolve(req.response);
}
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function () {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt
/* utf.js - UTF-8 <=> UTF-16 convertion
*
* Copyright (C) 1999 Masanao Izumo
* Version: 1.0
* LastModified: Dec 25 1999
* This library is free. You can redistribute it and/or modify it.
*/
function _bin2Str(array) {
let out, i, len, c;
let char2, char3;
out = "";
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
}
}
return out;
}
function b64EncodeUnicode(str) {
// first we use encodeURIComponent to get percent-encoded UTF-8,
// then we convert the percent encodings into raw bytes which
// can be fed into btoa.
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
function b64DecodeUnicode(str) {
// Going backwards: from bytestream, to percent-encoding, to original string.
return decodeURIComponent(atob(str).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
let _setCookie = (cname, cvalue, exdays) => {
let d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
cname = `${cname}_${webconfig.compileType}`;
cvalue = b64EncodeUnicode(cvalue);
document.cookie = `${cname}=${cvalue};expires=${d.toUTCString()};secure;path=/`;
}
let _getCookie = cname => {
cname = `${cname}_${webconfig.compileType}`;
let val = document.cookie.replace(new RegExp(`(?:(?:^|.*;\\s*)${cname}\\s*=\\s*([^;]*).*$)|^.*$`, 'i'), "$1");
return b64DecodeUnicode(val);
}
let _delCookie = cname => {
document.cookie = `${cname}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
return {
netGet: _netGet,
bin2Str: _bin2Str,
setCookie: _setCookie,
getCookie: _getCookie,
delCookie: _delCookie
}
})();
// Database Manager
// refer: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
DBManager = (function () {
const READONLY = "readonly";
const READWRITE = "readwrite";
let db = null;
let _open = () => {
if (db != null) {
return Promise.resolve(db);
}
return new Promise((resolve, reject) => {
this.req = window.indexedDB.open(GameConfig.DB_NAME);
req.onupgradeneeded = evt => {
db = evt.currentTarget.result;
let store = db.createObjectStore(GameConfig.DB_STORE);
store.transaction.oncomplete = function (event) {
let tmp = db.transaction(GameConfig.DB_STORE, READWRITE).objectStore(GameConfig.DB_STORE);
// create default folders
tmp.add({ timestamp: new Date(), mode: 16877 }, GameConfig.DB_BASE_DIR); // 40755
tmp.add({ timestamp: new Date(), mode: 16895 }, GameConfig.DB_BundleDir); // 40777
};
};
req.onsuccess = function () {
db = this.result;
resolve(db);
};
req.onerror = evt => {
reject(Error("Error openDB:" + evt.target.errorCode))
};
});
};
return {
iterateAll: async callback => {
let db = await _open();
return new Promise((resolve, reject) => {
let tx = db.transaction(GameConfig.DB_STORE, READONLY);
// tx.oncomplete = resolve;
tx.onabort = reject;
tx.onerror = reject;
let store = tx.objectStore(GameConfig.DB_STORE);
let cHandler = store.openCursor();
cHandler.onsuccess = function (event) {
let cursor = event.target.result;
if (cursor && callback(cursor)) {
cursor.continue();
} else {
resolve("No more entries!");
}
};
cHandler.onerror = evt => {
reject(Error("Cursor error!"));
};
});
},
doRead: async arr => {
let db = await _open();
return new Promise((resolve, reject) => {
let tx = db.transaction(GameConfig.DB_STORE, READONLY);
tx.oncomplete = resolve;
tx.onabort = reject;
tx.onerror = reject;
let store = tx.objectStore(GameConfig.DB_STORE);
let doJob = e => {
let req = store.get(e.path);
req.onsuccess = evt => {
e.onDBRead(evt.target.result);
};
req.onerror = evt => {
e.onDBError(evt);
};
};
if (arr.constructor === Array) { // array
arr.forEach(e => {
doJob(e);
});
} else if (arr.constructor === Entity) { // single
doJob(arr);
} else {
throw "fail to read!"
}
});
},
doInsert: async arr => {
let db = await _open();
return new Promise((resolve, reject) => {
let tx = db.transaction(GameConfig.DB_STORE, READWRITE);
tx.oncomplete = resolve;
tx.onabort = reject;
tx.onerror = reject;
let store = tx.objectStore(GameConfig.DB_STORE);
if (arr.constructor === Array) { // array
arr.forEach(e => {
store.add(e.Value, e.Path);
});
} else if (arr.constructor === Entity) { // single
let e = arr;
store.add(e.Value, e.Path);
} else {
throw "fail to read!"
}
});
},
doInsertOrUpdate: async arr => {
let db = await _open();
return new Promise((resolve, reject) => {
let tx = db.transaction(GameConfig.DB_STORE, READWRITE);
tx.oncomplete = resolve;
tx.onabort = reject;
tx.onerror = reject;
let store = tx.objectStore(GameConfig.DB_STORE);
if (arr.constructor === Array) { // array
arr.forEach(e => {
store.put(e.Value, e.Path);
});
} else if (arr.constructor === Entity) { // single
let e = arr;
store.put(e.Value, e.Path);
} else {
throw "fail to insertOrUpdate!"
}
});
},
doDelete: async arr => {
let db = await _open();
return new Promise((resolve, reject) => {
let tx = db.transaction(GameConfig.DB_STORE, READWRITE);
tx.oncomplete = resolve;
tx.onabort = reject;
tx.onerror = reject;
let store = tx.objectStore(GameConfig.DB_STORE);
if (arr.constructor === Array) { // array
arr.forEach(e => {
store.delete(e.Path);
});
} else if (arr.constructor === Entity) { // single
let e = arr;
store.delete(e.Path);
} else {
throw "fail to delete!"
}
});
},
close: () => {
if (db) {
db.close();
db = null;
}
},
};
})();
class Entity {
static genBundle(name, content) {
return new Entity(name,
`${GameConfig.DB_BundleDir}/${name}`,
content && { timestamp: new Date(), mode: 33206, contents: content },
content ? content.byteLength : 1,
true);
}
constructor(name, path, value, size, isBundle) {
this.name = name;
this.path = path;
this.value = value;
this.isBundle = isBundle;
this.size = size;
}
async checkUpdated(md5Str) {
await DBManager.doRead(this);
if (md5Str != md5(this.ContentAsStr)) {
let tmp = await EmUtil.netGet(`${GameConfig.bundleUrl}/${this.name}`);
let val = new Uint8Array(tmp);
if (md5Str != md5(EmUtil.bin2Str(val))) {
throw new Error(`Fail to update ${entity.name}`);
}
this.value = { timestamp: new Date(), mode: 33206, contents: val };
return false;
}
return true;
}
onDBRead(vv) { this.value = vv; }
onDBWrite(vv) { }
onDBError(vv) { console.error(vv); }
get Size() { return this.size; }
get Name() { return this.name; }
get Path() { return this.path; }
get Value() { return this.value; }
get Content() { return this.value && this.value.contents; }
get ContentAsStr() { return this.value && this.value.contents && EmUtil.bin2Str(new Uint8Array(this.value.contents)); }
}
class DBInsertJob {
constructor(condCount, condSize) {
this.condCount = condCount || Number.MAX_SAFE_INTEGER;
this.condSize = condSize || Number.MAX_SAFE_INTEGER;
this.batches = [];
this.curBatch = [];
this.curSize = 0;
this.isWorking = false;
}
insert(entity) {
if (entity) {
this.curSize += entity.Size;
this.curBatch.push(entity);
if (this._formBatch()) {
this._doJob();
}
}
}
_formBatch(isForce) {
if (this.curBatch.length > 0 &&
(isForce || this.curBatch.length >= this.condCount || this.curSize > this.condSize)) { // meet the batch condition
this.batches.push(this.curBatch);
this.curBatch = [];
this.curSize = 0;
return true;
}
return false;
}
_doJob(doneEvent) {
if (this.batches.length > 0 && !this.isWorking) { // do job
this.isWorking = true;
(async () => {
while (this.batches.length > 0) {
await DBManager.doInsertOrUpdate(this.batches.shift());
}
this.isWorking = false;
if (doneEvent) {
doneEvent();
}
})();
}
}
flush() {
return new Promise((resolve, reject) => {
if (this._formBatch(true)) {
this._doJob(resolve);
} else {
resolve();
}
});
}
get IsWorking() {
return this.isWorking;
}
}
function EmBundleMap(_name) {
let entity = Entity.genBundle(_name);
let resList = null;
return {
get name() {
return entity && entity.name;
},
get entity() {
return entity;
},
get resList() {
return resList;
},
updateMap: async (md5Str, dbJob) => {
let isUpdated = await entity.checkUpdated(md5Str);
if (!isUpdated) {
dbJob.insert(entity);
}
resList = JSON.parse(entity.ContentAsStr);
},
updateBundles: async dict => {
try {
let reg = /^AssetsBundles\/(.*)$/;
let upds = [];
resList.forEach(r => {
let name = r.path.replace(reg, "$1");
let info = dict[name];
if (info.mStatus != 1 || info.bStatus != 1) {
upds.push(info);
}
});
// alrady updated!!
if (upds.length == 0) {
console.log(`${entity && entity.name} is updated!`);
return true;
}
// start update
let dbJob = new DBInsertJob(20, 10 * 1024 * 1024); // 20 entities or 10M as a batch!
let waitList = [];
upds.forEach(j => {
let name = j.val.path.replace(reg, "$1");
waitList.push(new Promise((resolve, reject) => {
EmUtil.netGet(`${GameConfig.bundleUrl}/${name}`).then(val => {
dbJob.insert(Entity.genBundle(name, new Uint8Array(val)));
resolve();
}).catch(ex => {
reject(ex);
});
}));
waitList.push(new Promise((resolve, reject) => {
EmUtil.netGet(`${GameConfig.bundleUrl}/${name}.manifest`).then(val => {
dbJob.insert(Entity.genBundle(`${name}.manifest`, new Uint8Array(val)));
resolve();
}).catch(ex => {
reject(ex);
});
}));
});
await Promise.all(waitList);
await dbJob.flush();
return true;
} catch (ex) {
console.error(ex);
return false;
}
},
};
};
let AllBundle = (() => {
let bundleMd5 = `${GameConfig.bundleUrl}/BundleMD5.txt`;
let local = new EmBundleMap("LocalBundleMap.txt");
let patch = new EmBundleMap("BundleMap.txt");
let sprite = new EmBundleMap("SpriteSortingLayers.txt");
let fixDatabase = async () => { // delete old resource, pareparing for updating
let dict = {};
let dels = new Set();
let reg = /^AssetsBundles\/(.*)$/;
local.resList.forEach(r => { dict[r.path.replace(reg, "$1")] = { bStatus: 0, mStatus: 0, val: r }; });
patch.resList.forEach(r => { dict[r.path.replace(reg, "$1")] = { bStatus: 0, mStatus: 0, val: r }; });
let reg1 = /AssetsBundles\/(?:([^\/\.]+?)\.manifest|([^\/\.]+))$/;
let reg2 = /[^]+CRC: (\d+)[^]+/i;
await DBManager.iterateAll(c => {
let m = c.key.match(reg1);
if (m) {
let mName = m[1];
let bName = m[2];
if (mName) { // found manifest
let info = dict[mName];
if (info) {
let isValid = (c.value && c.value.contents
&& EmUtil.bin2Str(new Uint8Array(c.value.contents.slice(0, 50))).replace(reg2, "$1") == info.val.crc);
info.mStatus = isValid ? 1 : -1;
} else {
dels.add(`${mName}.manifest`);
}
} else if (bName) { // found bundle
let info = dict[bName];
if (info) {
let isValid = c.value && c.value.contents;
info.bStatus = isValid ? 1 : -1;
} else {
dels.add(bName);
}
} else {
throw `error when validating bundles: ${c.key}`;
}
}
return true;
});
Object.values(dict).filter(info => info.mStatus != 1 || info.bStatus != 1).forEach(info => {
if (info.mStatus != 0) {
info.mStatus = 0;
dels.add(`${info.val.path.replace(reg, "$1")}.manifest`);
}
if (info.bStatus != 0) {
info.bStatus = 0;
dels.add(info.val.path.replace(reg, "$1"));
}
});
if (dels.size > 0) {
let tmp = [];
dels.forEach(d => {
tmp.push(new Entity(d));
});
await DBManager.doDelete(tmp);
}
return dict;
};
return {
checkDB: async () => { // validate database!
let baseFolders = [
new Entity("baseDir", GameConfig.DB_BASE_DIR),
new Entity("bundleDir", GameConfig.DB_BundleDir),
];
await DBManager.doRead(baseFolders);
if (baseFolders.filter(e => !e.Value).length > 0) {
await DBManager.doInsertOrUpdate([
new Entity("baseDir", GameConfig.DB_BASE_DIR, { timestamp: new Date(), mode: 16877 }, 1, false),
new Entity("bundleDir", GameConfig.DB_BundleDir, { timestamp: new Date(), mode: 16895 }, 1, false),
]);
}
},
updateMap: async () => { // update bundleMaps !
let res = await EmUtil.netGet(bundleMd5, "text");
let mm = res.split(",");
if (mm.length != 3) {
throw new Error("The format of BundleMD5.txt is not correct!");
}
let dbJob = new DBInsertJob();
await Promise.all([
local.updateMap(mm[0], dbJob),
patch.updateMap(mm[1], dbJob),
sprite.updateMap(mm[2], dbJob)
]);
await dbJob.flush();
return await fixDatabase();
},
updateLocal: async dict => {
let retryTime = 0;
while (retryTime++ < 3) {
let isOK = await local.updateBundles(dict);
if (isOK) {
break;
} else {
console.warn(`failed to update ${local.name} at retryTime: ${retryTime}`);
}
}
},
updatePatch: async dict => {
let retryTime = 0;
while (retryTime++ < 3) {
let isOK = await patch.updateBundles(dict);
if (isOK) {
break;
} else {
console.warn(`failed to update ${patch.name} at retryTime: ${retryTime}`);
}
}
}
};
})();
let UpdateTool = (function () {
return {
start: async () => {
try {
await AllBundle.checkDB();
let dict = await AllBundle.updateMap();
await AllBundle.updateLocal(dict);
await AllBundle.updatePatch(dict);
} catch (ex) {
console.error(ex);
} finally {
DBManager.close();
}
}
};
})();