Skip to content

Commit

Permalink
feat: add download/upload archive
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidKk committed Mar 17, 2024
1 parent 3b58b13 commit 9345eb5
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 102 deletions.
6 changes: 0 additions & 6 deletions app/components/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ export default class App extends Component {
window.location.reload()
return
}

// 退出
if (event.ctrlKey && event.key.toLowerCase() === 'c') {
this.game.stop()
return
}
}),
jQuery(document.body).addEventsListener(
PointerEvent.Move,
Expand Down
197 changes: 124 additions & 73 deletions app/components/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import TouchPad from '@/controls/TouchPad'
import Joystick from '@/controls/Joystick'
import DPad from '@/controls/DPad'
import Keyboard from '@/controls/Keyboard'
import jQuery from '@/services/jQuery'
import { pickByLanguage } from '@/services/lang'
import { supported, isMobile } from '@/services/device'
import { googleSyncService } from '@/services/googleSyncService'
Expand All @@ -18,7 +19,6 @@ import { xor } from '@/utils/xor'
import { deprecated } from '@/utils'
import { TITLE, WASM_FILE } from '@/constants/definations'
import type { DosBoxProgressEvent, Game as GameInfo } from '@/types'
import jQuery from '@/services/jQuery'

const EQ_DIVIDE = ''.padEnd(32, '=')

Expand Down Expand Up @@ -65,7 +65,33 @@ export default class Game extends Component {
}
}

return deprecated(jQuery(document.body).addEventsListener('keydown', createListener(true)), jQuery(document.body).addEventsListener('keyup', createListener(false)))
return deprecated(
jQuery(document.body).addEventsListener('keydown', createListener(true)),
jQuery(document.body).addEventsListener('keyup', createListener(false)),
Menu.Events.Download.listen(async () => {
if (!(this.isPlaying && this.game)) {
return
}

const complete = await Notification.loading('Prepare archive files for you.')
await this.exportArchiveFromDB(this.game.id)
complete('Archive file export completed.')
}),
Menu.Events.Upload.listen(async (event) => {
if (this.isPlaying) {
return
}

const { romId, files } = event.detail
if (!(await this.requestGame(romId))) {
return
}

const complete = await Notification.loading('Importing archive files for you.')
await this.importArchiveFromDB(romId, files)
complete('Archive file import completed.')
})
)
}

protected unbindings() {
Expand Down Expand Up @@ -94,9 +120,8 @@ export default class Game extends Component {

this.disableContextMenu()

const games = await fetchGames()
// 尝试读取本地存储的ROM
this.game = games.get(id)!
this.game = await this.requestGame(id)
// 运行游戏
await this.play(this.game)

Expand Down Expand Up @@ -124,72 +149,8 @@ export default class Game extends Component {
this.isPlaying = false
}

/**
* 获取游戏存储的唯一值
* @description
* 主要用于同一款游戏不同ROM的存储
* 例如不同版本语言的游戏
*/
public async getGameStoreUniqKey(game: string | GameInfo): Promise<string> {
if (typeof game === 'string') {
if (!isGameName(game)) {
throw new Error(`Game is not exists.`)
}

const games = await fetchGames()
const info = games.get(game)!
return this.getGameStoreUniqKey(info)
}

return game.id
}

protected checkSupport() {
// 不支持 Webassembly 的时候提示用户升级
if (!supported.webAssembly) {
this.stage.simulateClean()
this.stage.toggleTerminal(true)

i18n?.support.webassembly.forEach((content) => {
this.stage.print(content)
})

if (isMobile) {
this.stage.touchToContinue()
} else {
this.stage.pressToContinue().then(() => {
this.stage.toggleTerminal(false)
DosBox.Events.Exit.dispatch()
})
}

throw new Error('WebAssembly is not supported.')
}
}

/** 打印游戏信息 */
protected printGameInfo(game: GameInfo) {
this.stage.print(EQ_DIVIDE)

const translatedName = typeof game.translates === 'string' ? game.translates : pickByLanguage(game.translates!)
game.name && this.stage.print(`${i18n.game.name}: ${game.name} ${game.name !== translatedName ? `(${translatedName})` : ''}`)
game.type && this.stage.print(`${i18n.game.type}: ${game.type}`)
game.developers && this.stage.print(`${i18n.game.developers}: ${game.developers}`)
game.publisher && this.stage.print(`${i18n.game.publisher}: ${game.publisher}`)
game.release && this.stage.print(`${i18n.game.release}: ${game.release}`)

const summary = !Array.isArray(game.summary) && typeof game.summary === 'object' ? pickByLanguage(game.summary) : game.summary
if (typeof summary === 'string') {
this.stage.print(`${i18n.game.summary}: ${summary}`)
} else if (Array.isArray(summary)) {
this.stage.print(`${i18n.game.summary}:\n${summary.join('\n')}`)
}

this.stage.print(EQ_DIVIDE)
}

/** 执行游戏 */
protected async play(game: GameInfo) {
protected async play(game = this.game) {
const url = typeof game.url === 'string' ? game.url : pickByLanguage(game.url)!
const key = await this.getGameStoreUniqKey(game)
game.rom = await this.model.loadRom(key)
Expand Down Expand Up @@ -267,8 +228,57 @@ export default class Game extends Component {
document.title = `${game.name} - SimDOS`
}

/** 打印游戏信息 */
protected printGameInfo(game = this.game) {
this.stage.print(EQ_DIVIDE)

const translatedName = typeof game.translates === 'string' ? game.translates : pickByLanguage(game.translates!)
game.name && this.stage.print(`${i18n.game.name}: ${game.name} ${game.name !== translatedName ? `(${translatedName})` : ''}`)
game.type && this.stage.print(`${i18n.game.type}: ${game.type}`)
game.developers && this.stage.print(`${i18n.game.developers}: ${game.developers}`)
game.publisher && this.stage.print(`${i18n.game.publisher}: ${game.publisher}`)
game.release && this.stage.print(`${i18n.game.release}: ${game.release}`)

const summary = !Array.isArray(game.summary) && typeof game.summary === 'object' ? pickByLanguage(game.summary) : game.summary
if (typeof summary === 'string') {
this.stage.print(`${i18n.game.summary}: ${summary}`)
} else if (Array.isArray(summary)) {
this.stage.print(`${i18n.game.summary}:\n${summary.join('\n')}`)
}

this.stage.print(EQ_DIVIDE)
}

/**
* 获取游戏存储的唯一值
* @description
* 主要用于同一款游戏不同ROM的存储
* 例如不同版本语言的游戏
*/
protected async getGameStoreUniqKey(game: string | GameInfo): Promise<string> {
if (typeof game === 'string') {
const info = await this.requestGame(game)
return this.getGameStoreUniqKey(info)
}

return game.id
}

protected async requestGame(id: string) {
if (!isGameName(id)) {
throw new Error(`Game ${id} is not exists.`)
}

const games = await fetchGames()
if (!games.has(id)) {
throw new Error(`Game ${id} is not exists.`)
}

return games.get(id)!
}

/** 注册控制器 */
protected registerMobileControls(game: GameInfo) {
protected registerMobileControls(game = this.game) {
/**
* 根据游戏需求注册不同的虚拟按键
* sendKeydown, sendKeyup 分别记住
Expand Down Expand Up @@ -386,7 +396,7 @@ export default class Game extends Component {
}

/** 激活自动存档 */
protected activeAutoSave(game: GameInfo, intervalMillisecond = 3e3) {
protected activeAutoSave(game = this.game, intervalMillisecond = 3e3) {
const sync = () => this.saveArchiveFromDB(game)
this.syncIntervalId && clearInterval(this.syncIntervalId)
this.syncIntervalId = setInterval(sync, intervalMillisecond)
Expand All @@ -405,7 +415,7 @@ export default class Game extends Component {
* 存储存档到本地 IndexedDB
* @param game 游戏信息
*/
public async saveArchiveFromDB(game: GameInfo) {
public async saveArchiveFromDB(game = this.game) {
const { save } = game
if (!save) {
return
Expand Down Expand Up @@ -449,7 +459,7 @@ export default class Game extends Component {
* 从本地 IndexedDB 中读取游戏存档
* @param game 游戏信息
*/
public async loadArchiveFromDB(game: GameInfo) {
public async loadArchiveFromDB(game = this.game) {
const { save } = game
if (!save) {
return
Expand Down Expand Up @@ -479,4 +489,45 @@ export default class Game extends Component {
})
}
}

/** 导出存档文件 */
public async exportArchiveFromDB(id: string) {
if (!isGameName(id)) {
throw new Error(`Game ${id} is not exists.`)
}

await this.model.exportArchive(id)
}

/** 导入存档文件 */
public async importArchiveFromDB(id: string, files: Record<string, Uint8Array>) {
if (!isGameName(id)) {
throw new Error(`Game ${id} is not exists.`)
}

await this.model.importArchive(id, files)
}

protected checkSupport() {
// 不支持 Webassembly 的时候提示用户升级
if (!supported.webAssembly) {
this.stage.simulateClean()
this.stage.toggleTerminal(true)

i18n?.support.webassembly.forEach((content) => {
this.stage.print(content)
})

if (isMobile) {
this.stage.touchToContinue()
} else {
this.stage.pressToContinue().then(() => {
this.stage.toggleTerminal(false)
DosBox.Events.Exit.dispatch()
})
}

throw new Error('WebAssembly is not supported.')
}
}
}
2 changes: 1 addition & 1 deletion app/components/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default class Notification extends Component {
}

public async toast(message: string) {
const node = this.appendElement('notification')
const node = this.create({ loading: false })
this.writeContentToToast(node, message)
await this.fadeOutToast(node)
}
Expand Down
12 changes: 12 additions & 0 deletions app/constants/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@ export const SCREENSHOT_ICON = `
</g>
</svg>
`

export const UPLOAD_ARCHIVE_ICON = `
<svg viewBox="0 0 32 32">
<path d="M0 26.016q0 2.496 1.76 4.224t4.256 1.76h4v-4h-4q-0.832 0-1.44-0.576t-0.576-1.408v-14.016h24v14.016q0 0.832-0.576 1.408t-1.408 0.576h-4v4h4q2.464 0 4.224-1.76t1.76-4.224v-20q0-2.496-1.76-4.256t-4.224-1.76h-20q-2.496 0-4.256 1.76t-1.76 4.256v20zM4 10.016v-4q0-0.832 0.576-1.408t1.44-0.608h20q0.8 0 1.408 0.608t0.576 1.408v4h-24zM6.016 8h1.984v-1.984h-1.984v1.984zM10.016 24h4v8h4v-8h4l-6.016-8zM10.016 8h1.984v-1.984h-1.984v1.984zM14.016 8h12v-1.984h-12v1.984z"></path>
</svg>
`

export const DOWNLOAD_ARCHIVE_ICON = `
<svg viewBox="0 0 32 32">
<path d="M0 26.016q0 2.496 1.76 4.224t4.256 1.76h4.992l-2.496-4h-2.496q-0.832 0-1.44-0.576t-0.576-1.408v-14.016h24v14.016q0 0.832-0.576 1.408t-1.408 0.576h-2.528l-2.496 4h5.024q2.464 0 4.224-1.76t1.76-4.224v-20q0-2.496-1.76-4.256t-4.224-1.76h-20q-2.496 0-4.256 1.76t-1.76 4.256v20zM4 10.016v-4q0-0.832 0.576-1.408t1.44-0.608h20q0.8 0 1.408 0.608t0.576 1.408v4h-24zM6.016 8h1.984v-1.984h-1.984v1.984zM10.016 24l5.984 8 6.016-8h-4v-8h-4v8h-4zM10.016 8h1.984v-1.984h-1.984v1.984zM14.016 8h12v-1.984h-12v1.984z"></path>
</svg>
`
40 changes: 30 additions & 10 deletions app/controls/Menu.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Notification from '@/components/Notification'
import jQuery from '@/services/jQuery'
import { isMobile } from '@/services/device'
import { googleSyncService } from '@/services/googleSyncService'
import { define, Component } from '@/libs/Component'
import SimEvent from '@/libs/SimEvent'
import { deprecated } from '@/utils'
import { GOOGLE_ICON, KEYBOARD_ICON } from '@/constants/icons'
import { deprecated, extract, upload } from '@/utils'
import { DOWNLOAD_ARCHIVE_ICON, GOOGLE_ICON, KEYBOARD_ICON, UPLOAD_ARCHIVE_ICON } from '@/constants/icons'
import { PointerEvent } from '@/constants/event'
import type { EmnuSyncEventPayload, MenuGamePlayEventPayload, MenuSwitchEventPayload } from '@/types'
import jQuery from '@/services/jQuery'
import type { EmnuSyncEventPayload, EmunUploadEventPayload, MenuGamePlayEventPayload, MenuSwitchEventPayload } from '@/types'

/** 菜单 */
@define('menu')
Expand All @@ -16,6 +16,8 @@ export default class Menu extends Component {
GamePlay: SimEvent.create<MenuGamePlayEventPayload>('MENU_GAME_PLAY'),
KeyboardSwitch: SimEvent.create<MenuSwitchEventPayload>('MENU_KEYBOARD_SWITCH'),
Sync: SimEvent.create<EmnuSyncEventPayload>('MENU_SYNC_EVENT'),
Download: SimEvent.create<void>('MENU_DOWNLOAD_EVENT'),
Upload: SimEvent.create<EmunUploadEventPayload>('MENU_UPLOAD_EVENT'),
}

static Messages = {
Expand All @@ -28,8 +30,19 @@ export default class Menu extends Component {
protected isUploading = false
protected keyboard: Component
protected google: Component
protected download: Component
protected upload: Component

protected bindings() {
this.download = this.appendElement('menu-item')
this.download.setAttr('menu', 'download')
this.download.innerHTML = DOWNLOAD_ARCHIVE_ICON
this.download.hide()

this.upload = this.appendElement('menu-item')
this.upload.setAttr('menu', 'upload')
this.upload.innerHTML = UPLOAD_ARCHIVE_ICON

this.google = this.appendElement('menu-item')
this.google.setAttr('menu', 'google')
this.google.innerHTML = GOOGLE_ICON
Expand All @@ -41,12 +54,7 @@ export default class Menu extends Component {

return deprecated(
jQuery(document).addEventsListener('fullscreenchange', () => {
if (document.fullscreenElement) {
this.google.hide()
return
}

this.google.show()
this.toggle(!document.fullscreenElement)
}),
this.google.addEventsListener(PointerEvent.Start, async () => {
if (this.isGamePlay) {
Expand Down Expand Up @@ -97,6 +105,16 @@ export default class Menu extends Component {

this.dispatchEvent(new Menu.Events.KeyboardSwitch({ visible: !this.isKeyboardVisible }, { bubbles: true }))
}),
this.download.addEventsListener(PointerEvent.Start, () => {
Menu.Events.Download.dispatch()
}),
this.upload.addEventsListener(PointerEvent.Start, async () => {
const [zip] = await upload()
const { name, content: source } = zip
const files = await extract(source)
const romId = name.replace(/\.zip$/, '')
Menu.Events.Upload.dispatch({ romId, files })
}),
googleSyncService.onAuthChanged(({ authorized }) => {
if (authorized) {
this.google.addClass('authorized')
Expand All @@ -112,6 +130,8 @@ export default class Menu extends Component {

const isGamePlay = !!event.detail?.gameplay
isMobile && this.keyboard.toggle(isGamePlay)
!isMobile && this.download.toggle(isGamePlay)
!isMobile && this.upload.toggle(!isGamePlay)
this.isGamePlay = isGamePlay
})
)
Expand Down
Loading

0 comments on commit 9345eb5

Please sign in to comment.