'use strict'
// ******************************** 文件系统 ********************************
const File = new class {
// 同步加载文件映射表
syncLoadings = new Map()
// 加载文件Promise集合
loadingPromises = {}
// 加载进度
loadingProgress = 0
// 正在引用的二进制对象列表
referencedBlobs = []
// 更新数据
update() {
if (this.referencedBlobs.length !== 0) {
for (const blob of this.referencedBlobs) {
URL.revokeObjectURL(blob.url)
}
this.referencedBlobs.length = 0
}
}
// 解密
async decrypt({path, sync, type}) {
const buffer = decrypt(await File.xhr({path, sync, type: 'arraybuffer'}))
switch (type) {
case 'url': {
const blob = new Blob([buffer])
const url = URL.createObjectURL(blob)
this.referencedBlobs.push(blob)
return blob.url = url
}
case 'text':
return Codec.textDecoder.decode(buffer)
case 'json':
return JSON.parse(Codec.textDecoder.decode(buffer))
case 'arraybuffer':
return buffer
}
}
/**
* 获取文件
* @param {Object} descriptor 文件描述器
* @returns {Promise<Object|Image|null>}
*/
get(descriptor) {
// 可以指定路径或GUID来加载文件
const path = descriptor.path ?? this.getPathByGUID(descriptor.guid)
const sync = descriptor.sync
const type = descriptor.type
switch (type) {
case 'image': {
const {loadingPromises} = this
// 如果当前图像已在加载中,则返回Promise,否则新建
return loadingPromises[path] || (
loadingPromises[path] = new Promise(async resolve => {
const image = new Image()
// 给图像元素设置guid用于纹理的查找
image.guid = descriptor.guid ?? ''
let url = path
let callback
if (/\.dat$/.test(path)) {
try {
url = await this.decrypt({path, sync, type: 'url'})
} catch (error) {
delete loadingPromises[path]
return resolve(null)
}
} else if (sync) {
// 同步加载图像资源
switch (Stats.isOnClient) {
case true: {
// 若是本地模式运行,模拟加载进度
const progress = {
complete: false,
lengthComputable: true,
loaded: 0,
total: 1,
}
this.syncLoadings.set(image, progress)
callback = () => {
progress.complete = true
progress.loaded = 1
}
break
}
case false:
// 若是Web模式运行,先用XHR加载数据块,再解析成图像
// 这样可以获取加载进度,用来显示进度条
try {
const blob = await File.xhr({path, sync, type: 'blob'})
url = blob.url = URL.createObjectURL(blob)
this.referencedBlobs.push(blob)
} catch (error) {
delete loadingPromises[path]
return resolve(null)
}
break
}
}
// 加载图像资源
image.onload = () => {
delete loadingPromises[path]
image.onload = null
image.onerror = null
callback?.()
resolve(image)
}
image.onerror = () => {
delete loadingPromises[path]
image.onload = null
image.onerror = null
image.src = ''
callback?.()
resolve(null)
}
image.src = url
}))
}
default:
return /\.dat$/.test(path)
? this.decrypt({path, sync, type})
: this.xhr({path, sync, type})
}
}
/**
* 使用XHR加载文件
* @param {Object} $
* @param {string} $.path 文件路径
* @param {boolean} $.sync 同步开关
* @param {string} $.type 类型
* @returns {Promise}
*/
xhr({path, sync, type}) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest()
if (sync) {
// 同步加载即时更新进度
request.onloadstart =
request.onprogress = event => {
event.complete = false
this.syncLoadings.set(request, event)
}
}
request.onload = event => {
event.complete = true
this.syncLoadings.set(request, event)
resolve(request.response)
}
request.onerror = event => {
this.syncLoadings.delete(request)
reject(request.response)
}
request.open('GET', path)
request.responseType = type
request.send()
})
}
/** 获取文件路径(客户端专用) */
route(relativePath) {
const root = /^\$[\\\/]/
let dirname = __dirname
// 如果使用了根目录标记
if (root.test(relativePath)) {
// 如果用户使用electron-builder打包应用,重新定位到根目录
dirname = dirname.replace(/[\\\/]resources[\\\/]app\.asar$/, '')
relativePath = relativePath.replace(root, '')
}
return require('path').resolve(dirname, relativePath)
}
/**
* 获取文件路径(通过GUID)
* @param {string} guid 文件GUID
* @returns {string} 文件路径或空字符串
*/
getPathByGUID(guid) {
return Data.manifest.guidMap[guid]?.path ?? ''
}
/**
* 更新同步加载进度
* @returns {boolean} 加载是否完成
*/
updateLoadingProgress() {
// 如果不存在同步加载,则继续
const {syncLoadings} = this
if (syncLoadings.size === 0) {
return false
}
// 统计已加载和总的数据字节大小
let loaded = 0
let total = 0
let complete = true
for (const progress of syncLoadings.values()) {
if (progress.lengthComputable) {
loaded += progress.loaded
total += progress.total
}
if (!progress.complete) {
complete = false
}
}
// 计算加载进度
this.loadingProgress = loaded / (total || Infinity)
// 加载进度为100%,不存在未知数据大小,已导入所有字体,则判定为加载完成
if (complete && !Printer.importing.length) {
// 删除同步加载进度表中的所有键值对
for (const key of syncLoadings.keys()) {
syncLoadings.delete(key)
}
// 移除进度条后继续
GL.container.progress &&
GL.container.progress.remove()
return false
}
// 未加载完成
return true
}
/** 渲染同步加载进度 */
renderLoadingProgress() {
// 擦除游戏画布内容(显示为黑屏)
GL.clearColor(0, 0, 0, 0)
GL.clear(GL.COLOR_BUFFER_BIT)
// 只有Web模式下才会显示进度条
if (!Stats.isOnClient) {
let {progress} = GL.container
if (!progress) {
// 创建进度条并设置样式
progress = document.createElement('div')
progress.style.position = 'absolute'
progress.style.left = '0'
progress.style.bottom = '0'
progress.style.height = '10px'
progress.style.backgroundImage = `
linear-gradient(
to right,
white 0%,
white 33%,
transparent 33%,
transparent 100%
)`
progress.style.backgroundSize = '3px 1px'
progress.style.pointerEvents = 'none'
// 设置移除进度条方法(加载完成时调用)
progress.remove = () => {
GL.container.progress = null
GL.container.removeChild(progress)
}
// 添加进度条到容器元素中
GL.container.progress = progress
GL.container.appendChild(progress)
}
// 更新当前的进度
const percent = Math.round(this.loadingProgress * 100)
if (progress.percent !== percent) {
progress.percent = percent
progress.style.width = `${percent}%`
}
}
}
}
// ******************************** 全局唯一标识符 ********************************
const GUID = new class {
// 检查用的正则表达式
regExpForChecking = /[a-f]/
/**
* 生成32位GUID(8个字符)
* @returns {string}
*/
generate32bit() {
const n = Math.random() * 0x100000000
const s = Math.floor(n).toString(16)
return s.length === 8 ? s : s.padStart(8, '0')
}
/**
* 生成64位GUID(16个字符)
* @returns {string}
*/
generate64bit() {
let id
// GUID通常用作哈希表的键
// 避免纯数字的键(会降低访问速度)
do {id = this.generate32bit() + this.generate32bit()}
while (!this.regExpForChecking.test(id))
return id
}
}