Web-Archives-Misc/public/Repo/DeSmuME/app.js
2023-02-06 19:42:43 +01:00

1774 lines
57 KiB
JavaScript

var VER = 'v2023-01-19'
const whatsNew = _ => {
alert(`\
{{ DeSmuME-WASM - OctoSpacc Fork }}
< Changelog >
[ 2023-01-19 ]
- Add this changelog and copyright notice
- Fix URL loading happening prematurely
- Change notices and visible options in the HTML
[ 2023-01-18 ]
- Add support for loading ROMs from URL
`)};
var EngineIsReady = false;
var isIOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
var isMacOS = !!navigator.platform && /Mac/.test(navigator.platform);
if (isMacOS) {
if (navigator.maxTouchPoints > 2) {
// Nah, it is an iPad pretending to be a Mac
isIOS = true
isMacOS = false
}
}
var isWebApp = navigator.standalone || false
var isSaveSupported = true
var isSaveNagAppeared = false
if (isIOS) {
//document.getElementById('romFile').files = null;
if (!isWebApp) {
// On iOS Safari, the indexedDB will be cleared after 7 days.
// To prevent users from frustration, we don't allow savegame on iOS unless the we are in the PWA mode.
isSaveSupported = false
var divIosHint = $id('ios-hint')
divIosHint.hidden = false
divIosHint.style = 'position: absolute; bottom: 20px;'
$id('btn-choose-file').hidden = true
$id('welcome').hidden = true
} else {
$id('ios-power-hint').hidden = false
}
}
if (isMacOS) {
// Check if it is Safari
if (navigator.userAgent.indexOf('Chrome') < 0) {
$id('mac-warning').hidden = false
}
}
function $id(id) {
return document.getElementById(id);
}
// https://stackoverflow.com/a/48178043
function waitFor(condition, callback) {
if(!condition()) {
window.setTimeout(waitFor.bind(null, condition, callback), 100);
} else {
callback();
};
};
var optScaleMode = 0
var uiCurrentMode = 'welcome'
var plugins = {}
var body = document.getElementsByTagName("body")[0]
var html = document.getElementsByTagName("html")[0]
var config = {
swapABXY: false,
swapTopBottom: false,
swapTopBottomL: false,
powerSave: true,
micWhenR: true,
vkEnabled: true,
muteSound: false,
useDPad: false,
lsLayout: 0,
turbo: false,
scaleMode: 0,
fwLang: 1,
vkScale: 1
}
var emuUseTimer33 = false
var lidClosed = false
function afterConfigUpdated() {
emuUseTimer33 = false
if (config.powerSave || config.turbo) {
emuUseTimer33 = true
}
}
function loadConfig() {
var cfg = JSON.parse(window.localStorage['config'] || '{}')
for (var k in cfg) {
config[k] = cfg[k]
}
$id('power-save').checked = config.powerSave
$id('vk-enabled').checked = config.vkEnabled
$id('cfg-mute-sound').checked = config.muteSound
$id('vk-direction').value = config.useDPad ? "1" : "0"
$id('cfg-turbo').checked = config.turbo
$id('cfg-ls-layout').value = config.lsLayout
$id('cfg-scale-mode').value = config.scaleMode
$id('cfg-lang').value = config.fwLang
$id('cfg-swap-abxy').checked = config.swapABXY
afterConfigUpdated()
optScaleMode = config.scaleMode
}
loadConfig()
function uiSaveConfig() {
config.powerSave = !!($id('power-save').checked)
config.vkEnabled = !!($id('vk-enabled').checked)
config.muteSound = !!($id('cfg-mute-sound').checked)
config.useDPad = !!parseInt($id('vk-direction').value)
config.turbo = !!($id('cfg-turbo').checked)
config.lsLayout = parseInt($id('cfg-ls-layout').value)
config.scaleMode = parseInt($id('cfg-scale-mode').value)
config.fwLang = parseInt($id('cfg-lang').value)
config.swapABXY = !!($id('cfg-swap-abxy').checked)
window.localStorage['config'] = JSON.stringify(config)
afterConfigUpdated()
}
function uiMenuBack() {
tryInitSound()
uiSaveConfig()
emuIsGameLoaded ? uiSwitchTo('player') : uiSwitchTo('welcome');
}
function toyEncrypt(src) {
var dst = new Uint8Array(src.length)
for (var i = 0; i < src.length; i++) {
dst[i] = src[i] ^ 0xFB
}
return dst
}
async function emuBackupCloudSav() {
return toyEncrypt(pako.gzip(emuCopySavBuffer()))
}
async function emuRestoreCloudSav(u8Arr) {
var sav = pako.ungzip(toyEncrypt(u8Arr))
await localforage.setItem('sav-' + gameID, sav)
return true
}
function uiSaveExport() {
if (isSaveSupported) {
if (!confirm(`Auto-save is enabled, you DON'T have to export save data manually.
After you saved in the game, wait a few seconds and save data will be stored in the app's local storage automatically.
To prevent the data loss caused by accidential deletion of the Home Screen icon or damaged device, you may export the save data to a safe place.
Do you wish to continue?`)) {
return
}
if (prompt('You HAVE to save in the game before exporting the save data.\n\nPlease enter "Yes" to confirm.') !== 'Yes') {
return
}
}
var u8Arr = pako.gzip(emuCopySavBuffer())
var blob = new Blob([u8Arr], { type: "application/binary" });
var link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = 'sav-' + gameID + '.dsz';
link.click();
}
async function uiSaveRestore() {
var file = $id('restore-file').files[0]
if (!file) {
return
}
var fileExt = file.name.split('.').pop().toLowerCase()
var allowedExts = { '4dsav': true, 'sav': true, 'dsv': true, '4dsaz': true, 'dsz': true }
if (!allowedExts[fileExt]) {
alert('Invalid file extension: ' + fileExt)
return
}
var u8Arr = new Uint8Array(await file.arrayBuffer())
if (fileExt == '4dsav') {
u8Arr = toyEncrypt(u8Arr)
}
if (fileExt == '4dsaz') {
// Use pako to decompress
u8Arr = pako.ungzip(toyEncrypt(u8Arr))
}
if (fileExt == 'dsz') {
u8Arr = pako.ungzip(u8Arr)
}
if (fileExt == 'sav') {
var origSave = emuCopySavBuffer()
if (origSave.length <= 0) {
alert('You have to save in the game at least once, before importing .sav file.')
return
}
if (u8Arr.length > origSave.length) {
alert('The .sav file is too large.')
return
}
// Copy the u8Arr to the beginning of origSave, overwriting the original save data.
origSave.set(u8Arr, 0)
u8Arr = origSave
}
localforage.setItem('sav-' + gameID, u8Arr).then(() => {
alert('Save data updated. \nPlease close and reopen this app.')
setTimeout(() => {
location.reload()
}, 500)
})
}
var gameID = ''
var emuKeyState = new Array(14)
const emuKeyNames = ["right", "left", "down", "up", "select", "start", "b", "a", "y", "x", "l", "r", "debug", "lid"]
var vkMap = {}
var vkState = {}
var keyNameToKeyId = {}
var vkStickPos = [0, 0, 0, 0, 0]
var vkDPadRect = { x: 0, y: 0, w: 0, h: 0 }
for (var i = 0; i < emuKeyNames.length; i++) {
keyNameToKeyId[emuKeyNames[i]] = i
}
var isLandscape = false
const emuKeyboardMapping = [39, 37, 40, 38, 16, 13, 90, 88, 65, 83, 81, 87, -1, 8]
var emuIsRunning = false
var emuIsGameLoaded = false
var fps = 0
var divFPS = $id('fps')
var fileInput = $id('rom')
var romSize = 0
var FB = [0, 0]
var screenCanvas = [document.getElementById('top'), document.getElementById('bottom')]
var ctx2d;
var audioContext
var audioBuffer
var scriptNode
const audioFifoCap = 8192
var audioFifoL = new Int16Array(audioFifoCap)
var audioFifoR = new Int16Array(audioFifoCap)
var audioFifoHead = 0
var audioFifoLen = 0
var frameCount = 0
var prevCalcFPSTime = 0
var touched = 0
var touchX = 0
var touchY = 0
var prevSaveFlag = 0
var lastTwoFrameTime = 10
var fbSize
function callPlugin(type, arg) {
for (var k in plugins) {
if (plugins[k].handler) {
plugins[k].handler(type, arg)
}
}
}
function showMsg(msg) {
document.getElementById('msg-text').innerText = msg
document.getElementById('msg-layer').hidden = false
setTimeout(function () {
document.getElementById('msg-layer').hidden = true
}, 1000)
}
function emuRunAudio() {
var samplesRead = Module._fillAudioBuffer(4096)
if (config.muteSound) {
return
}
for (var i = 0; i < samplesRead; i++) {
if (audioFifoLen >= audioFifoCap) {
break
}
var wpos = (audioFifoHead + audioFifoLen) % audioFifoCap
audioFifoL[wpos] = audioBuffer[i * 2]
audioFifoR[wpos] = audioBuffer[i * 2 + 1]
audioFifoLen++
}
}
function emuRunFrame() {
processGamepadInput()
var keyMask = 0;
for (var i = 0; i < 14; i++) {
if (emuKeyState[i]) {
keyMask |= 1 << i
}
}
var mic = emuKeyState[11]
if (mic) {
keyMask |= 1 << 14
}
if (lidClosed) {
keyMask |= 1 << 13
}
if (config.turbo) {
for (var i = 0; i < 2; i++) {
Module._runFrame(0, keyMask, touched, touchX, touchY)
emuRunAudio()
}
} else if (config.powerSave) {
Module._runFrame(0, keyMask, touched, touchX, touchY)
emuRunAudio()
}
Module._runFrame(1, keyMask, touched, touchX, touchY)
emuRunAudio()
if (optScaleMode < 2) {
ctx2d[0].putImageData(FB[0], 0, 0)
ctx2d[1].putImageData(FB[1], 0, 0)
} else {
gpuDraw(screenCanvas[0], FB[0])
gpuDraw(screenCanvas[1], FB[1])
}
frameCount += 1
if (frameCount % 120 == 0) {
var time = performance.now()
fps = 120 / ((time - prevCalcFPSTime) / 1000)
prevCalcFPSTime = time
divFPS.innerText = 'fps:' + ('' + fps).substring(0, 5)
}
if (frameCount % 30 == 0) {
checkSaveGame()
}
}
function wasmReady() {
Module._setSampleRate(47856)
setTimeout(() => {
if ($id('loading').hidden == true) {
return;
}
$id('loading').hidden = true
$id('loadrom').hidden = false
var btn = $id('btn-choose-file')
if (!btn.onclick) {
btn.onclick = () => {
tryInitSound()
$id('rom').click()
}
}
}, 2000)
$id('ver-info').innerText = 'Ver: ' + VER + ' '
if (optScaleMode < 2) {
ctx2d = screenCanvas.map((v) => { return v.getContext('2d', { alpha: false }) })
} else {
gpuInit()
}
EngineIsReady = true;
}
function emuCopySavBuffer() {
var size = Module._savGetSize()
if (size > 0) {
var ptr = Module._savGetPointer(0)
var tmpSaveBuf = new Uint8Array(size)
tmpSaveBuf.set(Module.HEAPU8.subarray(ptr, ptr + size))
return tmpSaveBuf
} else {
return new Uint8Array(0)
}
}
function checkSaveGame() {
if (!isSaveSupported) {
return
}
var saveUpdateFlag = Module._savUpdateChangeFlag()
if ((saveUpdateFlag == 0) && (prevSaveFlag == 1)) {
var savBuf = emuCopySavBuffer()
if (savBuf.length > 0) {
localforage.setItem('sav-' + gameID, savBuf)
showMsg('Auto saving...')
}
}
prevSaveFlag = saveUpdateFlag
}
async function tryLoadROM(file) {
if (!file) {
return
}
if (file.size < 1024) {
return
}
var header = new Uint8Array(await (file.slice(0, 1024)).arrayBuffer())
gameID = ''
for (var i = 0; i < 0x10; i++) {
gameID += (header[i] == 0) ? ' ' : String.fromCharCode(header[i])
}
if (gameID[0xC] == '#') {
// a homebrew!
gameID = file.name
}
console.log('gameID', gameID)
romSize = file.size
var romBufPtr = Module._prepareRomBuffer(romSize)
console.log(romSize, romBufPtr)
const blockSize = 4 * 1024 * 1024
// Load the file by small chunks, to make browser heap happier
for (var pos = 0; pos < romSize; pos += blockSize) {
var chunk = await (file.slice(pos, pos + blockSize)).arrayBuffer()
Module.HEAPU8.set(new Uint8Array(chunk), romBufPtr + pos)
}
var saveData = await localforage.getItem('sav-' + gameID)
if (saveData) {
Module.HEAPU8.set(saveData, Module._savGetPointer(saveData.length))
try {
dpTryAutoBackup().then(function (ret) {
if (ret) {
showMsg("Daily cloud backup done!")
}
})
dpGameLoaded()
} catch (e) {
console.log(e)
alert("Cloud backup failed: " + e)
}
}
Module._savUpdateChangeFlag()
Module._emuSetOpt(0, config.fwLang)
var ret = Module._loadROM(romSize);
if (ret != 0) {
msg = 'LoadROM failed: ' + ret + "\n"
if (ret == -1001) {
msg += 'This file is encrypted and not supported for now.\n'
}
alert(msg)
return;
}
ptrFrontBuffer = Module._getSymbol(5)
var fb = Module._getSymbol(4)
for (var i = 0; i < 2; i++) {
FB[i] = new ImageData(new Uint8ClampedArray(Module.HEAPU8.buffer).subarray(fb + 256 * 192 * 4 * i, fb + 256 * 192 * 4 * (i + 1)), 256, 192)
}
var ptrAudio = Module._getSymbol(6)
audioBuffer = new Int16Array(Module.HEAPU8.buffer).subarray(ptrAudio / 2, ptrAudio / 2 + 16384 * 2)
emuIsGameLoaded = true
callPlugin('loaded', gameID)
if (window.eaRunHook) {
window.eaRunHook();
} else {
emuStart();
}
}
// Allow drag and drop of files on entire window
window.ondragover = function (e) {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
}
window.ondrop = function (e) {
e.preventDefault()
tryLoadROM(e.dataTransfer.files[0])
}
function emuStart() {
if (!emuIsGameLoaded) {
return
}
console.log('Start!!!')
emuIsRunning = true
uiSwitchTo('player')
}
function initVK() {
var vks = document.getElementsByClassName('vk')
for (var i = 0; i < vks.length; i++) {
var vk = vks[i]
var k = vks[i].getAttribute('data-k')
if (k) {
vkMap[k] = vk
vkState[k] = [0, 0]
}
}
}
initVK()
function makeVKStyle(top, left, w, h, fontSize) {
return 'top:' + top + 'px;left:' + left + 'px;width:' + w + 'px;height:' + h + 'px;' + 'font-size:' + fontSize + 'px;line-height:' + h + 'px;'
}
function uiAdjustVKLayout() {
var baseSize = Math.min(window.innerWidth, window.innerHeight) * 0.14 * config.vkScale
var fontSize = baseSize * 0.7
var offTop = Math.min(fbSize[0][1] + fbSize[1][1], window.innerHeight - Math.ceil(baseSize * 3.62))
var offLeft = 0
var abxyWidth = baseSize * 3
var abxyHeight = baseSize * 3
var vkw = baseSize
var vkh = baseSize
vkw = baseSize * 1.5
vkh = baseSize * 0.6
fontSize = baseSize * 0.5
vkMap['l'].style = makeVKStyle(offTop, 0, vkw, vkh, fontSize)
vkMap['r'].style = makeVKStyle(offTop, window.innerWidth - vkw, vkw, vkh, fontSize)
vkw = baseSize * 0.4
vkh = baseSize * 0.4
$id('vk-menu').style = makeVKStyle(offTop, window.innerWidth / 2 - vkw / 2, vkw, vkh, fontSize)
offTop += baseSize * 0.62
vkw = baseSize
vkh = baseSize
offLeft = window.innerWidth - abxyWidth
vkMap['a'].style = makeVKStyle(offTop + abxyHeight / 2 - vkh / 2, offLeft + abxyWidth - vkw, vkw, vkh, fontSize)
vkMap['b'].style = makeVKStyle(offTop + abxyHeight - vkh, offLeft + abxyWidth / 2 - vkw / 2, vkw, vkh, fontSize)
vkMap['x'].style = makeVKStyle(offTop, offLeft + abxyWidth / 2 - vkw / 2, vkw, vkh, fontSize)
vkMap['y'].style = makeVKStyle(offTop + abxyHeight / 2 - vkh / 2, offLeft, vkw, vkh, fontSize)
vkw = baseSize * 1.0
vkh = baseSize * 1.0
offLeft = 0
$id('vk-stick').style = config.useDPad ? 'display:none;' : makeVKStyle(offTop + abxyHeight / 2 - vkh / 2, offLeft + abxyHeight / 2 - vkw / 2, vkw, vkh, fontSize)
vkStickPos = [offTop + abxyHeight / 2, offLeft + abxyHeight / 2, vkw, vkh, fontSize]
var dpadW = abxyWidth
var dpadH = abxyHeight
var dpadX = offLeft
var dpadY = offTop
vkDPadRect = { x: dpadX, y: dpadY, width: dpadW, height: dpadH }
$id('vk-dpad-1').style = config.useDPad ? makeVKStyle(dpadY + dpadH / 3, dpadX, dpadW, dpadH / 3, fontSize) : 'display:none;'
$id('vk-dpad-2').style = config.useDPad ? makeVKStyle(dpadY, dpadX + dpadW / 3, dpadW / 3, dpadH, fontSize) : 'display:none;'
vkw = baseSize * 0.4
vkh = baseSize * 0.4
fontSize = baseSize * 0.2
vkMap['select'].style = makeVKStyle(offTop + abxyHeight - vkh, window.innerWidth / 2 - vkw * 1.5, vkw, vkh, fontSize)
vkMap['start'].style = makeVKStyle(offTop + abxyHeight - vkh, window.innerWidth / 2 + vkw * 0.5, vkw, vkh, fontSize)
}
function maxScreenSize(maxWidth, maxHeight) {
var w = maxWidth
var h = w / 256 * 192
if (h > maxHeight) {
h = maxHeight
w = h / 192 * 256
}
return [w, h]
}
function setScreenPos(c, left, top, w, h) {
var sty = 'left:' + left + 'px;top:' + top + "px;width:" + w + "px;height:" + h + "px;"
if (optScaleMode == 0) {
sty += 'image-rendering:pixelated;'
}
c.style = sty
if (optScaleMode >= 2) {
var devicePixelRatio = window.devicePixelRatio || 1
c.width = w * devicePixelRatio
c.height = h * devicePixelRatio
}
}
function uiUpdateLayout() {
isLandscape = isScreenLandscape()
if ((!isLandscape) || (config.lsLayout == 0)) {
// Top-bottom
var maxWidth = window.innerWidth
var maxHeight = window.innerHeight / 2
var sz = maxScreenSize(maxWidth, maxHeight); var w = sz[0]; var h = sz[1];
var left = 0
left += (window.innerWidth - w) / 2;
var top = 0
fbSize = [[w, h], [w, h]]
for (var i = 0; i < 2; i++) {
setScreenPos(screenCanvas[i], left, top, fbSize[i][0], fbSize[i][1])
top += h
}
} else if (config.lsLayout == 1) {
// Left-right 1:1
var maxWidth = window.innerWidth / 2
var maxHeight = window.innerHeight
var sz = maxScreenSize(maxWidth, maxHeight); var w = sz[0]; var h = sz[1];
var left = 0
var top = 0
fbSize = [[w, h], [w, h]]
for (var i = 0; i < 2; i++) {
setScreenPos(screenCanvas[i], left, top, fbSize[i][0], fbSize[i][1])
left += w
}
} else if (config.lsLayout == 2) {
// Left-right X:1
var maxWidth = window.innerWidth - 256
var maxHeight = window.innerHeight
var sz = maxScreenSize(maxWidth, maxHeight); var w = sz[0]; var h = sz[1];
var left = 0
var top = 0
fbSize = [[w, h], [256, 192]]
for (var i = 0; i < 2; i++) {
setScreenPos(screenCanvas[i], left, top, fbSize[i][0], fbSize[i][1])
left += w
}
}
uiAdjustVKLayout()
}
function uiSwitchTo(mode) {
if (mode == uiCurrentMode) {
return
}
uiCurrentMode = mode
$id('welcome').hidden = true
$id('vk-layer').hidden = true
$id('menu').hidden = true
$id('player').hidden = true
body.style = ''
html.style = ''
emuIsRunning = false
if (mode == 'player') {
body.style = 'touch-action: none;'
html.style = 'position: fixed;overflow:hidden;touch-action: none;'
for (var i = 0; i < 14; i++) {
emuKeyState[i] = false
}
if (config.vkEnabled) {
$id('vk-layer').hidden = false
}
uiUpdateLayout()
if (emuIsGameLoaded) {
emuIsRunning = true
}
$id('player').hidden = false
}
if (mode == 'menu') {
$id('player').hidden = false
$id('menu').hidden = false
$id('menu-savegame').hidden = emuIsGameLoaded ? false : true
}
if (mode == 'welcome') {
$id('welcome').hidden = false
}
}
fileInput.onchange = async () => {
var file = fileInput.files[0]
if (!file) {
return
}
var fileNameLower = file.name.toLowerCase()
if (fileNameLower.endsWith('.json')) {
var obj = JSON.parse(await file.text())
var pluginName = obj.name || 'unknown'
plugins[pluginName] = obj
if (obj.js) {
plugins[pluginName].handler = eval(obj.js)(obj)
}
alert('Plugin loaded!')
return
} else if (fileNameLower.endsWith('.gba')) {
alert('This is a GBA file, redirecting to the GBA player...')
window.location.href = '/gba';
} else if (fileNameLower.endsWith('.zip')) {
alert('ZIP files are not supported.\nOn iOS, you can unzip it with the built-in Files app.')
} else if (fileNameLower.endsWith('.3ds')) {
alert('No, 3DS is not supported.')
} else if (fileNameLower.endsWith('.nds')) {
tryLoadROM(file)
return
} else {
alert('Unknown file type!')
}
}
function onScriptNodeAudioProcess(e) {
var chanL = e.outputBuffer.getChannelData(0)
var chanR = e.outputBuffer.getChannelData(1)
if (config.muteSound) {
return
}
for (var i = 0; i < chanL.length; i++) {
if (audioFifoLen <= 0) {
return
}
audioFifoLen--
chanL[i] = audioFifoL[audioFifoHead] / 32768.0
chanR[i] = audioFifoR[audioFifoHead] / 32768.0
audioFifoHead = (audioFifoHead + 1) % audioFifoCap
}
}
// must be called in user gesture
function tryInitSound() {
try {
if (audioContext) {
if (audioContext.state != 'running') {
audioContext.resume()
}
return;
}
audioContext = new (window.AudioContext || window.webkitAudioContext)({ latencyHint: 0.0001, sampleRate: 48000 });
scriptNode = audioContext.createScriptProcessor(2048, 0, 2);
scriptNode.onaudioprocess = onScriptNodeAudioProcess;
scriptNode.connect(audioContext.destination);
audioContext.resume()
} catch (e) {
console.log(e)
//alert('Cannnot init sound ')
}
}
function emuLoop() {
window.requestAnimationFrame(emuLoop)
if (emuIsRunning && (!emuUseTimer33)) {
prevRunFrameTime = performance.now()
emuRunFrame()
}
}
emuLoop()
function emuTimer33() {
if (emuIsRunning && emuUseTimer33) {
emuRunFrame()
}
}
setInterval(emuTimer33, 33)
var stickTouchID = null
var tpadTouchID = null
function isPointInRect(x, y, r) {
if ((x >= r.x) && (x < r.x + r.width)) {
if ((y >= r.y) && (y < r.y + r.height)) {
return true
}
}
return false
}
function clamp01(a) {
if (a < 0) {
return 0
}
if (a > 1) {
return 1
}
return a
}
function handleTouch(event) {
tryInitSound()
if (!emuIsRunning) {
return
}
event.preventDefault();
event.stopPropagation();
var isDown = false
var x = 0
var y = 0
var needUpdateStick = false
var stickY = vkStickPos[0]
var stickX = vkStickPos[1]
var stickW = vkStickPos[2]
var stickH = vkStickPos[3]
var stickPressed = false
var stickDeadZone = stickW * 0.2
var nextStickTouchID = null
var nextTpadTouchID = null
var tsRect = screenCanvas[1].getBoundingClientRect()
for (var i = 0; i < emuKeyState.length; i++) {
emuKeyState[i] = false
}
for (var k in vkState) {
vkState[k][1] = 0
}
for (var i = 0; i < event.touches.length; i++) {
var t = event.touches[i];
var tid = t.identifier
var dom = document.elementFromPoint(t.clientX, t.clientY)
var k = dom ? dom.getAttribute('data-k') : null
if (config.useDPad) {
if (isPointInRect(t.clientX, t.clientY, vkDPadRect)) {
var xgrid = Math.floor((t.clientX - vkDPadRect.x) / vkDPadRect.width * 3)
var ygrid = Math.floor((t.clientY - vkDPadRect.y) / vkDPadRect.height * 3)
var xygrid = xgrid + ygrid * 3
if (xygrid == 5)
// right
emuKeyState[0] = true
else if (xygrid == 3)
// left
emuKeyState[1] = true
else if (xygrid == 7)
// down
emuKeyState[2] = true
else if (xygrid == 1)
// up
emuKeyState[3] = true
}
} else {
if ((tid === stickTouchID) || ((dom == vkMap['stick']) && (tid != tpadTouchID))) {
stickPressed = true
vkState['stick'][1] = 1
var sx = t.clientX
var sy = t.clientY
if (sx < stickX - stickDeadZone) {
emuKeyState[1] = true
}
if (sx > stickX + stickDeadZone) {
emuKeyState[0] = true
}
if (sy < stickY - stickDeadZone) {
emuKeyState[3] = true
}
if (sy > stickY + stickDeadZone) {
emuKeyState[2] = true
}
sx = Math.max(stickX - stickW / 2, sx)
sx = Math.min(stickX + stickW / 2, sx)
sy = Math.max(stickY - stickH / 2, sy)
sy = Math.min(stickY + stickH / 2, sy)
stickX = sx
stickY = sy
needUpdateStick = true
nextStickTouchID = tid
continue
}
}
if ((tid === tpadTouchID) || (isPointInRect(t.clientX, t.clientY, tsRect) && (!k))) {
isDown = true
x = clamp01((t.clientX - tsRect.x) / tsRect.width) * 256
y = clamp01((t.clientY - tsRect.y) / tsRect.height) * 192
nextTpadTouchID = tid
continue
}
if (k) {
vkState[k][1] = 1
continue
}
}
touched = isDown ? 1 : 0;
touchX = x
touchY = y
for (var k in vkState) {
if (vkState[k][0] != vkState[k][1]) {
var dom = vkMap[k]
vkState[k][0] = vkState[k][1]
if (vkState[k][1]) {
//dom.classList.add('vk-touched')
if (k == 'menu') {
uiSwitchTo('menu')
}
} else {
//dom.classList.remove('vk-touched')
if (k == "stick") {
needUpdateStick = true
}
}
}
}
for (var i = 0; i < emuKeyState.length; i++) {
var k = emuKeyNames[i]
if (vkState[k]) {
if (vkState[k][1]) {
emuKeyState[i] = true
}
}
}
if (needUpdateStick) {
vkMap['stick'].style = makeVKStyle(stickY - stickW / 2, stickX - stickW / 2, stickW, stickH, vkStickPos[4])
}
stickTouchID = nextStickTouchID
tpadTouchID = nextTpadTouchID
}
['touchstart', 'touchmove', 'touchend', 'touchcancel', 'touchenter', 'touchleave'].forEach((val) => {
window.addEventListener(val, handleTouch)
})
window.onmousedown = window.onmouseup = window.onmousemove = (e) => {
if (!emuIsRunning) {
return
}
if (e.type == 'mousedown') {
tryInitSound()
}
var r = screenCanvas[1].getBoundingClientRect()
e.preventDefault()
e.stopPropagation()
var isDown = (e.buttons != 0) && (isPointInRect(e.clientX, e.clientY, r))
var x = (e.clientX - r.x) / r.width * 256
var y = (e.clientY - r.y) / r.height * 192
touched = isDown ? 1 : 0;
touchX = x
touchY = y
}
window.onresize = window.onorientationchange = () => {
uiUpdateLayout()
}
function convertKeyCode(keyCode) {
for (var i = 0; i < 14; i++) {
if (keyCode == emuKeyboardMapping[i]) {
return i
}
}
return -1
}
window.onkeydown = window.onkeyup = (e) => {
if (!emuIsRunning) {
return
}
e.preventDefault()
var isDown = (e.type === "keydown")
var k = convertKeyCode(e.keyCode)
if (k >= 0) {
emuKeyState[k] = isDown
}
if (e.keyCode == 27) {
uiSwitchTo('menu')
}
}
var currentConnectedGamepad = -1
var gamePadKeyMap = {
a: 1,
b: 0,
x: 3,
y: 2,
l: 4,
r: 5,
'select': 8,
'start': 9,
'up': 12,
'down': 13,
'left': 14,
'right': 15
}
var gamePadKeyMap2 = {
a: 0,
b: 1,
x: 2,
y: 3,
l: 4,
r: 5,
'select': 8,
'start': 9,
'up': 12,
'down': 13,
'left': 14,
'right': 15
}
if (isSaveSupported) {
window.addEventListener("gamepadconnected", function (e) {
console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.",
e.gamepad.index, e.gamepad.id,
e.gamepad.buttons.length, e.gamepad.axes.length);
showMsg('Gamepad connected.')
currentConnectedGamepad = e.gamepad.index
$id('a-gamepad').innerText = 'Gamepad connected'
});
}
function processGamepadInput() {
var padMap = gamePadKeyMap
if (config.swapABXY) {
padMap = gamePadKeyMap2
}
if (currentConnectedGamepad < 0) {
return
}
var gamepad = navigator.getGamepads()[currentConnectedGamepad]
if (!gamepad) {
showMsg('Gamepad disconnected.')
currentConnectedGamepad = -1
return
}
for (var i = 0; i < emuKeyState.length; i++) {
emuKeyState[i] = false
}
// Check L+R+X+Y
if (gamepad.buttons[4].pressed && gamepad.buttons[5].pressed && gamepad.buttons[2].pressed && gamepad.buttons[3].pressed) {
uiSwitchTo('menu')
return
}
for (var k in padMap) {
if (gamepad.buttons[padMap[k]].pressed) {
emuKeyState[keyNameToKeyId[k]] = true
}
}
if (gamepad.axes[0] < -0.5) {
emuKeyState[keyNameToKeyId['left']] = true
}
if (gamepad.axes[0] > 0.5) {
emuKeyState[keyNameToKeyId['right']] = true
}
if (gamepad.axes[1] < -0.5) {
emuKeyState[keyNameToKeyId['up']] = true
}
if (gamepad.axes[1] > 0.5) {
emuKeyState[keyNameToKeyId['down']] = true
}
}
var isMicrophoneEnabled = false
var micPtr
var micBuf
var micScriptNode
var micSource
function enableMicrophone() {
if (!micPtr) {
micPtr = Module._realloc(0, 0x1000)
micBuf = Module.HEAPU8.subarray(micPtr, micPtr + 0x1000)
}
if (micScriptNode) {
micScriptNode.disconnect()
}
if (micSource) {
micSource.disconnect()
}
tryInitSound()
isMicrophoneEnabled = true
// console.log(micPtr, micBuf)
// Request access to the Microphone, and get raw PCM samples at 8000Hz, use WebAudio API
navigator.mediaDevices.getUserMedia({ audio: true, video: false })
.then(function (stream) {
// Create a MediaStreamSource from the stream
micSource = audioContext.createMediaStreamSource(stream);
// Create a ScriptProcessorNode with a bufferSize of 2048
micScriptNode = audioContext.createScriptProcessor(2048, 1, 1);
// Connect the ScriptProcessorNode to the MediaStreamSource
micSource.connect(micScriptNode);
// Connect the ScriptProcessorNode to the destination
micScriptNode.onaudioprocess = function (e) {
var buf = e.inputBuffer.getChannelData(0) // 48000Hz mono float
// Convert to 16000Hz 7bit mono PCM
var dstPtr = 0;
for (var i = 0; i <= 2045; i += 3) {
var val = (buf[i] + buf[i + 1] + buf[i + 2]) / 3
// Convert -1~1 to 0~127
val = Math.floor(val * 64 + 64)
if (val > 127) {
val = 127
} else if (val < 0) {
val = 0
}
micBuf[dstPtr] = val
dstPtr++
}
// Write to the buffer
Module._micWriteSamples(micPtr, 682)
for (var outputChan = 0; outputChan < 1; outputChan++) {
var buf = e.outputBuffer.getChannelData(outputChan)
for (var i = 0; i < 2048; i++) {
buf[i] = 0
}
}
}
micScriptNode.connect(audioContext.destination);
});
}
function isScreenLandscape() {
return (window.innerWidth / window.innerHeight) > 1.2
}
if (location.origin == 'https://ds.44670.org') {
if (isSaveSupported) {
// Register Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function (reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function (error) {
// registration failed
console.log('Registration failed with ' + error);
});
navigator.serviceWorker.addEventListener('message', event => {
console.log('sw msg', event);
if (event.data.msg) {
$id('whats-new').innerText = event.data.msg
}
});
}
}
(function () {
var cnt = 0;
// Prompt to install PWA
window.onbeforeinstallprompt = function (e) {
cnt += 1;
if (cnt > 2) {
return;
}
console.log('Before install prompt', e);
e.preventDefault();
var deferredPrompt = e;
window.onclick = function (e) {
deferredPrompt.prompt();
window.onclick = null;
}
};
})();
}
var vertShaderSource = `
precision mediump float;
attribute vec2 a_position; //(0,0)-(1,1)
varying vec2 v_texCoord; //(0,0)-(1,1)
void main() {
// Convert a_position to gl_Position
gl_Position = vec4(a_position.x * 2.0 - 1.0, 1.0 - a_position.y * 2.0, 0, 1);
v_texCoord = a_position;
}
`;
var fragShaderSource = `
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_TEXTURE texture
out COMPAT_PRECISION vec4 FragColor;
#else
#define COMPAT_VARYING varying
#define FragColor gl_FragColor
#define COMPAT_TEXTURE texture2D
#endif
precision mediump float;
uniform sampler2D u_image;
varying vec2 v_texCoord;
uniform vec2 u_outResolution;
uniform vec2 u_inResolution;
#define Source u_image
#define vTexCoord v_texCoord
#define SourceSize vec4(u_inResolution, 1.0 / u_inResolution)
#define OutSize vec4(u_outResolution, 1.0 / u_outResolution)
#define BLEND_NONE 0
#define BLEND_NORMAL 1
#define BLEND_DOMINANT 2
#define LUMINANCE_WEIGHT 1.0
#define EQUAL_COLOR_TOLERANCE 30.0/255.0
#define STEEP_DIRECTION_THRESHOLD 2.2
#define DOMINANT_DIRECTION_THRESHOLD 3.6
float DistYCbCr(vec3 pixA, vec3 pixB)
{
const vec3 w = vec3(0.2627, 0.6780, 0.0593);
const float scaleB = 0.5 / (1.0 - w.b);
const float scaleR = 0.5 / (1.0 - w.r);
vec3 diff = pixA - pixB;
float Y = dot(diff.rgb, w);
float Cb = scaleB * (diff.b - Y);
float Cr = scaleR * (diff.r - Y);
return sqrt(((LUMINANCE_WEIGHT * Y) * (LUMINANCE_WEIGHT * Y)) + (Cb * Cb) + (Cr * Cr));
}
bool IsPixEqual(const vec3 pixA, const vec3 pixB)
{
return (DistYCbCr(pixA, pixB) < EQUAL_COLOR_TOLERANCE);
}
float get_left_ratio(vec2 center, vec2 origin, vec2 direction, vec2 scale)
{
vec2 P0 = center - origin;
vec2 proj = direction * (dot(P0, direction) / dot(direction, direction));
vec2 distv = P0 - proj;
vec2 orth = vec2(-direction.y, direction.x);
float side = sign(dot(P0, orth));
float v = side * length(distv * scale);
// return step(0, v);
return smoothstep(-sqrt(2.0)/2.0, sqrt(2.0)/2.0, v);
}
#define eq(a,b) (a == b)
#define neq(a,b) (a != b)
#define P(x,y) COMPAT_TEXTURE(Source, coord + SourceSize.zw * vec2(x, y)).rgb
void main()
{
//---------------------------------------
// Input Pixel Mapping: -|x|x|x|-
// x|A|B|C|x
// x|D|E|F|x
// x|G|H|I|x
// -|x|x|x|-
vec2 scale = OutSize.xy * SourceSize.zw;
vec2 pos = fract(vTexCoord * SourceSize.xy) - vec2(0.5, 0.5);
vec2 coord = vTexCoord - pos * SourceSize.zw;
vec3 A = P(-1.,-1.);
vec3 B = P( 0.,-1.);
vec3 C = P( 1.,-1.);
vec3 D = P(-1., 0.);
vec3 E = P( 0., 0.);
vec3 F = P( 1., 0.);
vec3 G = P(-1., 1.);
vec3 H = P( 0., 1.);
vec3 I = P( 1., 1.);
// blendResult Mapping: x|y|
// w|z|
ivec4 blendResult = ivec4(BLEND_NONE,BLEND_NONE,BLEND_NONE,BLEND_NONE);
// Preprocess corners
// Pixel Tap Mapping: -|-|-|-|-
// -|-|B|C|-
// -|D|E|F|x
// -|G|H|I|x
// -|-|x|x|-
if (!((eq(E,F) && eq(H,I)) || (eq(E,H) && eq(F,I))))
{
float dist_H_F = DistYCbCr(G, E) + DistYCbCr(E, C) + DistYCbCr(P(0,2), I) + DistYCbCr(I, P(2.,0.)) + (4.0 * DistYCbCr(H, F));
float dist_E_I = DistYCbCr(D, H) + DistYCbCr(H, P(1,2)) + DistYCbCr(B, F) + DistYCbCr(F, P(2.,1.)) + (4.0 * DistYCbCr(E, I));
bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_H_F) < dist_E_I;
blendResult.z = ((dist_H_F < dist_E_I) && neq(E,F) && neq(E,H)) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
}
// Pixel Tap Mapping: -|-|-|-|-
// -|A|B|-|-
// x|D|E|F|-
// x|G|H|I|-
// -|x|x|-|-
if (!((eq(D,E) && eq(G,H)) || (eq(D,G) && eq(E,H))))
{
float dist_G_E = DistYCbCr(P(-2.,1.) , D) + DistYCbCr(D, B) + DistYCbCr(P(-1.,2.), H) + DistYCbCr(H, F) + (4.0 * DistYCbCr(G, E));
float dist_D_H = DistYCbCr(P(-2.,0.) , G) + DistYCbCr(G, P(0.,2.)) + DistYCbCr(A, E) + DistYCbCr(E, I) + (4.0 * DistYCbCr(D, H));
bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_D_H) < dist_G_E;
blendResult.w = ((dist_G_E > dist_D_H) && neq(E,D) && neq(E,H)) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
}
// Pixel Tap Mapping: -|-|x|x|-
// -|A|B|C|x
// -|D|E|F|x
// -|-|H|I|-
// -|-|-|-|-
if (!((eq(B,C) && eq(E,F)) || (eq(B,E) && eq(C,F))))
{
float dist_E_C = DistYCbCr(D, B) + DistYCbCr(B, P(1.,-2.)) + DistYCbCr(H, F) + DistYCbCr(F, P(2.,-1.)) + (4.0 * DistYCbCr(E, C));
float dist_B_F = DistYCbCr(A, E) + DistYCbCr(E, I) + DistYCbCr(P(0.,-2.), C) + DistYCbCr(C, P(2.,0.)) + (4.0 * DistYCbCr(B, F));
bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_B_F) < dist_E_C;
blendResult.y = ((dist_E_C > dist_B_F) && neq(E,B) && neq(E,F)) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
}
// Pixel Tap Mapping: -|x|x|-|-
// x|A|B|C|-
// x|D|E|F|-
// -|G|H|-|-
// -|-|-|-|-
if (!((eq(A,B) && eq(D,E)) || (eq(A,D) && eq(B,E))))
{
float dist_D_B = DistYCbCr(P(-2.,0.), A) + DistYCbCr(A, P(0.,-2.)) + DistYCbCr(G, E) + DistYCbCr(E, C) + (4.0 * DistYCbCr(D, B));
float dist_A_E = DistYCbCr(P(-2.,-1.), D) + DistYCbCr(D, H) + DistYCbCr(P(-1.,-2.), B) + DistYCbCr(B, F) + (4.0 * DistYCbCr(A, E));
bool dominantGradient = (DOMINANT_DIRECTION_THRESHOLD * dist_D_B) < dist_A_E;
blendResult.x = ((dist_D_B < dist_A_E) && neq(E,D) && neq(E,B)) ? ((dominantGradient) ? BLEND_DOMINANT : BLEND_NORMAL) : BLEND_NONE;
}
vec3 res = E;
// Pixel Tap Mapping: -|-|-|-|-
// -|-|B|C|-
// -|D|E|F|x
// -|G|H|I|x
// -|-|x|x|-
if(blendResult.z != BLEND_NONE)
{
float dist_F_G = DistYCbCr(F, G);
float dist_H_C = DistYCbCr(H, C);
bool doLineBlend = (blendResult.z == BLEND_DOMINANT ||
!((blendResult.y != BLEND_NONE && !IsPixEqual(E, G)) || (blendResult.w != BLEND_NONE && !IsPixEqual(E, C)) ||
(IsPixEqual(G, H) && IsPixEqual(H, I) && IsPixEqual(I, F) && IsPixEqual(F, C) && !IsPixEqual(E, I))));
vec2 origin = vec2(0.0, 1.0 / sqrt(2.0));
vec2 direction = vec2(1.0, -1.0);
if(doLineBlend)
{
bool haveShallowLine = (STEEP_DIRECTION_THRESHOLD * dist_F_G <= dist_H_C) && neq(E,G) && neq(D,G);
bool haveSteepLine = (STEEP_DIRECTION_THRESHOLD * dist_H_C <= dist_F_G) && neq(E,C) && neq(B,C);
origin = haveShallowLine? vec2(0.0, 0.25) : vec2(0.0, 0.5);
direction.x += haveShallowLine? 1.0: 0.0;
direction.y -= haveSteepLine? 1.0: 0.0;
}
vec3 blendPix = mix(H,F, step(DistYCbCr(E, F), DistYCbCr(E, H)));
res = mix(res, blendPix, get_left_ratio(pos, origin, direction, scale));
}
// Pixel Tap Mapping: -|-|-|-|-
// -|A|B|-|-
// x|D|E|F|-
// x|G|H|I|-
// -|x|x|-|-
if(blendResult.w != BLEND_NONE)
{
float dist_H_A = DistYCbCr(H, A);
float dist_D_I = DistYCbCr(D, I);
bool doLineBlend = (blendResult.w == BLEND_DOMINANT ||
!((blendResult.z != BLEND_NONE && !IsPixEqual(E, A)) || (blendResult.x != BLEND_NONE && !IsPixEqual(E, I)) ||
(IsPixEqual(A, D) && IsPixEqual(D, G) && IsPixEqual(G, H) && IsPixEqual(H, I) && !IsPixEqual(E, G))));
vec2 origin = vec2(-1.0 / sqrt(2.0), 0.0);
vec2 direction = vec2(1.0, 1.0);
if(doLineBlend)
{
bool haveShallowLine = (STEEP_DIRECTION_THRESHOLD * dist_H_A <= dist_D_I) && neq(E,A) && neq(B,A);
bool haveSteepLine = (STEEP_DIRECTION_THRESHOLD * dist_D_I <= dist_H_A) && neq(E,I) && neq(F,I);
origin = haveShallowLine? vec2(-0.25, 0.0) : vec2(-0.5, 0.0);
direction.y += haveShallowLine? 1.0: 0.0;
direction.x += haveSteepLine? 1.0: 0.0;
}
origin = origin;
direction = direction;
vec3 blendPix = mix(H,D, step(DistYCbCr(E, D), DistYCbCr(E, H)));
res = mix(res, blendPix, get_left_ratio(pos, origin, direction, scale));
}
// Pixel Tap Mapping: -|-|x|x|-
// -|A|B|C|x
// -|D|E|F|x
// -|-|H|I|-
// -|-|-|-|-
if(blendResult.y != BLEND_NONE)
{
float dist_B_I = DistYCbCr(B, I);
float dist_F_A = DistYCbCr(F, A);
bool doLineBlend = (blendResult.y == BLEND_DOMINANT ||
!((blendResult.x != BLEND_NONE && !IsPixEqual(E, I)) || (blendResult.z != BLEND_NONE && !IsPixEqual(E, A)) ||
(IsPixEqual(I, F) && IsPixEqual(F, C) && IsPixEqual(C, B) && IsPixEqual(B, A) && !IsPixEqual(E, C))));
vec2 origin = vec2(1.0 / sqrt(2.0), 0.0);
vec2 direction = vec2(-1.0, -1.0);
if(doLineBlend)
{
bool haveShallowLine = (STEEP_DIRECTION_THRESHOLD * dist_B_I <= dist_F_A) && neq(E,I) && neq(H,I);
bool haveSteepLine = (STEEP_DIRECTION_THRESHOLD * dist_F_A <= dist_B_I) && neq(E,A) && neq(D,A);
origin = haveShallowLine? vec2(0.25, 0.0) : vec2(0.5, 0.0);
direction.y -= haveShallowLine? 1.0: 0.0;
direction.x -= haveSteepLine? 1.0: 0.0;
}
vec3 blendPix = mix(F,B, step(DistYCbCr(E, B), DistYCbCr(E, F)));
res = mix(res, blendPix, get_left_ratio(pos, origin, direction, scale));
}
// Pixel Tap Mapping: -|x|x|-|-
// x|A|B|C|-
// x|D|E|F|-
// -|G|H|-|-
// -|-|-|-|-
if(blendResult.x != BLEND_NONE)
{
float dist_D_C = DistYCbCr(D, C);
float dist_B_G = DistYCbCr(B, G);
bool doLineBlend = (blendResult.x == BLEND_DOMINANT ||
!((blendResult.w != BLEND_NONE && !IsPixEqual(E, C)) || (blendResult.y != BLEND_NONE && !IsPixEqual(E, G)) ||
(IsPixEqual(C, B) && IsPixEqual(B, A) && IsPixEqual(A, D) && IsPixEqual(D, G) && !IsPixEqual(E, A))));
vec2 origin = vec2(0.0, -1.0 / sqrt(2.0));
vec2 direction = vec2(-1.0, 1.0);
if(doLineBlend)
{
bool haveShallowLine = (STEEP_DIRECTION_THRESHOLD * dist_D_C <= dist_B_G) && neq(E,C) && neq(F,C);
bool haveSteepLine = (STEEP_DIRECTION_THRESHOLD * dist_B_G <= dist_D_C) && neq(E,G) && neq(H,G);
origin = haveShallowLine? vec2(0.0, -0.25) : vec2(0.0, -0.5);
direction.x -= haveShallowLine? 1.0: 0.0;
direction.y += haveSteepLine? 1.0: 0.0;
}
vec3 blendPix = mix(D,B, step(DistYCbCr(E, B), DistYCbCr(E, D)));
res = mix(res, blendPix, get_left_ratio(pos, origin, direction, scale));
}
FragColor = vec4(res, 1.0);
} `
function gpuInitWithCanvas(canvas) {
var gl = canvas.getContext("webgl");
canvas.gl = gl;
if (!gl) {
alert("Unable to initialize WebGL. Your browser or machine may not support it.");
return null;
}
gl.viewport(0, 0, canvas.width, canvas.height);
// Create shader.
program = gl.createProgram();
var vertShader = gl.createShader(gl.VERTEX_SHADER);
var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(vertShader, vertShaderSource);
gl.shaderSource(fragShader, fragShaderSource);
gl.compileShader(vertShader);
gl.compileShader(fragShader);
// Check if compilation succeeded.
if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) {
alert("Error in vertex shader: " + gl.getShaderInfoLog(vertShader));
return;
}
if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) {
alert("Error in fragment shader: " + gl.getShaderInfoLog(fragShader));
return;
}
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
alert("Error in program: " + gl.getProgramInfoLog(program));
return;
}
gl.useProgram(program);
// Create texture.
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// Use nearest neighbor interpolation.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Create vertex buffer, a rectangle to (0,0)-(width,height).
var vertices = new Float32Array([
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1
]);
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Create attribute.
var positionAttribLocation = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(positionAttribLocation);
gl.vertexAttribPointer(positionAttribLocation, 2, gl.FLOAT, false, 0, 0);
// Set uniform.
canvas.outResolutionUniformLocation = gl.getUniformLocation(program, "u_outResolution");
var inResolutionUniformLocation = gl.getUniformLocation(program, "u_inResolution");
gl.uniform2f(inResolutionUniformLocation, 256, 192);
return gl;
}
function gpuDraw(canvas, idata) {
var gl = canvas.gl;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, idata);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.uniform2f(canvas.outResolutionUniformLocation, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function gpuInit() {
if (!gpuInitWithCanvas(screenCanvas[0])) {
return
}
gpuInitWithCanvas(screenCanvas[1]);
}
var DP_BASE_PATH = "/dssav"
var DP_EXT = ".4dsaz"
function dpGetCurrentDayInt() {
// yyyymmdd
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
var retInt = year * 10000 + month * 100 + day;
return retInt;
}
function dpIsConnected() {
return localStorage['d-token'] ? true : false
}
async function dpIDHash(gameID) {
if (!localStorage['d-id']) {
throw "Not connected"
}
// Using SHA256
var inputData = localStorage['d-id'] + ',' + gameID
var hash = await window.crypto.subtle.digest('SHA-256', new TextEncoder("utf-8").encode(inputData))
var digestHex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('')
return digestHex.substring(0, 8)
}
async function dpGameLoaded() {
if (dpIsConnected()) {
var hash = await dpIDHash(gameID)
$id('span-cloud-id').innerText = hash
}
}
async function dpConnect() {
var redirectUri = encodeURIComponent(location.origin)
var url = "https://www.dropbox.com/oauth2/authorize?client_id=zro5k6xlnsxu4gz&response_type=code&token_access_type=offline"
url += "&redirect_uri=" + redirectUri
location.href = url
}
async function dpCheckUser() {
var resp = await fetch('https://api.dropboxapi.com/2/check/user', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage['d-token'],
'Content-Type': 'application/json'
},
body: JSON.stringify({ query: "foo" })
})
var obj = await resp.text()
console.log(obj)
}
async function dpUploadFile(path, u8Arr, mode) {
mode = mode || "overwrite"
var uploadArg = JSON.stringify({
"autorename": true,
"mode": mode,
"mute": true,
"strict_conflict": false,
"path": path,
})
var blob = new Blob([u8Arr], { type: "application/octet-stream" })
for (var retry = 0; retry < 2; retry++) {
var resp = await fetch('https://content.dropboxapi.com/2/files/upload', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage['d-token'],
'Dropbox-API-Arg': uploadArg,
'Content-Type': 'application/octet-stream'
},
body: blob
})
console.log("status: ", resp.status)
// Check http status.
if (resp.status != 200) {
if (resp.status == 401) {
var ret = await dpRefreshToken()
if (!ret) {
throw "Unable to refresh token"
}
continue
}
else {
throw "Upload failed, unknown http status: " + resp.status
}
} else {
var obj = await resp.json()
console.log(obj)
return obj
}
}
return false
}
async function dpDownloadFile(path) {
var downloadArg = JSON.stringify({
"path": path,
})
for (var retry = 0; retry < 2; retry++) {
var resp = await fetch('https://content.dropboxapi.com/2/files/download', {
method: 'POST',
headers: {
"Authorization": "Bearer " + localStorage['d-token'],
"Dropbox-API-Arg": downloadArg,
}
})
console.log("status: ", resp.status)
if (resp.status != 200) {
if (resp.status == 401) {
var ret = await dpRefreshToken()
if (!ret) {
throw "Unable to refresh token"
}
continue
}
else {
throw "Download failed, unknown http status: " + resp.status
}
}
// Get result from header
var obj = JSON.parse(resp.headers.get("dropbox-api-result"))
console.log(obj)
// Get result from body
var u8Arr = await resp.arrayBuffer()
return new Uint8Array(u8Arr)
}
return false
}
async function dpOnLoad() {
if (location.search.startsWith("?code=")) {
var code = location.search.slice(6)
var resp = await fetch("https://c.44670.org/d", {
method: "POST",
body: "1," + location.origin + "," + code,
})
var obj = await resp.json()
if (!obj.error) {
localStorage['d-token'] = obj.token
localStorage['d-token-r'] = obj.tokenr
localStorage['d-id'] = obj.id
alert("Dropbox connected.")
location.href = location.origin
} else {
alert(obj.error)
}
}
document.getElementById('btn-dp-connect').innerText = (dpIsConnected() ? "Disconnect" : "Connect") + " Dropbox"
}
async function dpRefreshToken() {
console.log("Refreshing token...")
if (!localStorage['d-token-r']) {
throw "No refresh token"
}
var resp = await fetch("https://c.44670.org/d", {
method: "POST",
body: "2," + location.origin + "," + localStorage['d-token-r'],
})
var obj = await resp.json()
if (!obj.error) {
localStorage['d-token'] = obj.token
return true
} else {
alert("Failed to update DropBox token: " + obj.error)
}
return false
}
async function dpGetPath(gameID, tag) {
var hash = await dpIDHash(gameID)
var path = DP_BASE_PATH + "/" + hash + "/" + tag + DP_EXT
return path
}
async function dpTryUploadCloudSave(gameID, tag, u8Arr, mode) {
if (!dpIsConnected()) {
return false
}
var path = await dpGetPath(gameID, tag)
try {
var resp = await dpUploadFile(path, u8Arr, mode)
return resp
} catch (e) {
alert("Failed to upload cloud save: " + e)
return false
}
return false
}
async function dpTryAutoBackup() {
if (!dpIsConnected()) {
return false
}
var nowDay = '' + dpGetCurrentDayInt()
if (localStorage['d-last-' + gameID] == nowDay) {
return false
}
var ret = await dpTryUploadCloudSave(gameID, "auto-" + dpGetCurrentDayInt(), await emuBackupCloudSav(), "add")
if (ret) {
localStorage['d-last-' + gameID] = nowDay
return true
}
return false
}
function dpOnConnectButtonClicked() {
if (dpIsConnected()) {
if (confirm("Are you sure to disconnect from Dropbox?")) {
localStorage['d-token'] = ""
localStorage['d-token-r'] = ""
localStorage['d-id'] = ""
alert("Dropbox disconnected.")
location.href = location.origin
}
} else {
dpConnect()
}
}
async function dpManualBtn(isUpload) {
if (!dpIsConnected()) {
alert("Please connect to Dropbox first.")
return
}
if (!gameID) {
alert("Please load a game first.")
return
}
try {
if (isUpload) {
var ret = await dpTryUploadCloudSave(gameID, "manual", await emuBackupCloudSav(), "overwrite")
if (ret) {
alert("Uploaded successfully.")
} else {
alert("Failed to upload.")
}
} else {
var path = await dpGetPath(gameID, "manual")
var u8Arr = await dpDownloadFile(path)
if (!u8Arr) {
alert("Failed to download.")
return
}
if (u8Arr.length < 1) {
alert("No cloud save found.")
return
}
if (await emuRestoreCloudSav(u8Arr)) {
alert("Restored successfully.")
setTimeout(function () {
location.reload()
}, 1000)
} else {
alert("Failed to restore.")
}
}
} catch (e) {
alert("Error:" + e)
return
}
}
const LoadRomFromUrl = _ => {
const RomUrl = new URLSearchParams(window.location.hash).get('#RomUrl');
if (RomUrl) {
const Req = new XMLHttpRequest();
Req.open('GET', RomUrl, true);
Req.responseType = 'blob';
Req.onload = function() {
waitFor(() => (EngineIsReady == true), () => tryLoadROM(Req.response));
};
Req.send();
};
};
/*
const RomUrlBoxChange = _ => {
if (RomUrlBox.value) {
$id('btn-choose-file').innerHTML = 'Load from URL!';
} else {
$id('btn-choose-file').innerHTML = 'Choose File (or drag/drop)...';
};
};
['onchange', 'oninput', 'onpaste'].forEach(function (i) {
RomUrlBox[i] = RomUrlBoxChange;
});
*/
dpOnLoad();
LoadRomFromUrl();