diff --git a/assets/bin/main/index.html b/assets/bin/main/index.html index 4ec8209..a8b6601 100644 --- a/assets/bin/main/index.html +++ b/assets/bin/main/index.html @@ -81,7 +81,6 @@
diff --git a/docs/IPC.md b/docs/IPC.md index 8d8a40c..931e71c 100644 --- a/docs/IPC.md +++ b/docs/IPC.md @@ -6,17 +6,16 @@ 下表列出了主进程接收到的请求: -| ID | 接收者 | 名称 | 参数 | 说明 | -| --- | ------------- | ----------------------- | -------- | ------------------------------------------------ | -| 01 | ManagerWindow | start-game | 无 | 启动游戏。 | -| 02 | ManagerWindow | start-tool | `id` | 启动 `id` 对应的工具。 | -| 03 | ManagerWindow | close-manager | 无 | 关闭 Manager 窗口。 | -| 04 | ManagerWindow | clear-cache | 无 | 清理缓存(`appDataDir/static/`) | -| 05 | ManagerWindow | update-user-config | `config` | 更新主进程中的 `config` 并保存。 | -| 06 | GameWindow | main-loader-ready | 无 | 游戏宿主窗口已创建并初始化完毕,需要加载端口信息 | -| 07 | sandbox | sandbox-dirname-request | 无 | 返回当前的 `dirname`。 | -| 08 | sandbox | sandbox-appdata-request | 无 | 返回当前的 `appDataDir` | -| 09 | screenshot | save-screenshot | `buffer` | 对截屏进行保存。 | +| ID | 接收者 | 名称 | 参数 | 说明 | +| --- | ------------- | ----------------------- | -------- | -------------------------------- | +| 01 | ManagerWindow | start-game | 无 | 启动游戏。 | +| 02 | ManagerWindow | start-tool | `id` | 启动 `id` 对应的工具。 | +| 03 | ManagerWindow | close-manager | 无 | 关闭 Manager 窗口。 | +| 04 | ManagerWindow | clear-cache | 无 | 清理缓存(`appDataDir/static/`) | +| 05 | ManagerWindow | update-user-config | `config` | 更新主进程中的 `config` 并保存。 | +| 06 | sandbox | sandbox-dirname-request | 无 | 返回当前的 `dirname`。 | +| 07 | sandbox | sandbox-appdata-request | 无 | 返回当前的 `appDataDir` | +| 08 | screenshot | save-screenshot | `buffer` | 对截屏进行保存。 | ### 通用请求 diff --git a/src/bin/main/mainLoader.ts b/src/bin/main/mainLoader.ts index f36c668..d9337bb 100644 --- a/src/bin/main/mainLoader.ts +++ b/src/bin/main/mainLoader.ts @@ -1,4 +1,4 @@ -import { ipcRenderer, screen as electronScreen } from 'electron' +import { ipcRenderer } from 'electron' import i18n from '../../i18n' import Global from '../../manager/global' import { MajsoulPlus } from '../../majsoul_plus' @@ -43,21 +43,24 @@ function showScreenshotLabel(src: string) { }, 8000) } -ipcRenderer.on('take-screenshot', (event, scaleFactor: number) => { - if (webContents) { - webContents.capturePage( - { - x: 0, - y: 0, - width: Math.floor(mainWindow.clientWidth * scaleFactor), - height: Math.floor(mainWindow.clientHeight * scaleFactor) - }, - image => { - ipcRenderer.send('save-screenshot', image.toPNG()) - } - ) +ipcRenderer.on( + 'take-screenshot', + (event, index: number, scaleFactor: number) => { + if (webContents) { + webContents.capturePage( + { + x: 0, + y: 0, + width: Math.floor(mainWindow.clientWidth * scaleFactor), + height: Math.floor(mainWindow.clientHeight * scaleFactor) + }, + image => { + ipcRenderer.send('save-screenshot', index, image.toPNG()) + } + ) + } } -}) +) ipcRenderer.on('screenshot-saved', (event, filePath: string) => { showScreenshotLabel('file://' + filePath) @@ -112,7 +115,6 @@ mainWindow.addEventListener('dom-ready', () => { if (!webContents) { webContents = mainWindow.getWebContents() webContents.setZoomFactor(1) - ipcRenderer.send('main-loader-ready') webContents.on('will-navigate', (event, url) => { if (isVanillaGameUrl(url)) { @@ -136,11 +138,14 @@ mainWindow.addEventListener('dom-ready', () => { ipcRenderer.on( 'load-url', - (event, url: string, port: number, http: boolean) => { + (event, url: string, port: number, http: boolean, partition?: string) => { serverInfo = { url, port, http } console.log('[Majsoul Plus] LoadURL', serverInfo) - mainWindow.loadURL(url) + if (partition) { + mainWindow.partition = partition + } + mainWindow.src = url mainWindowBox.style.width = '100vw' mainWindowBox.style.height = '100vh' mainWindowBox.style.transform = 'none' @@ -152,7 +157,6 @@ ipcRenderer.on('get-local-storage', () => { 'Object.entries(localStorage)', false, result => { - console.log(result) ipcRenderer.send('save-local-storage', result) } ) diff --git a/src/index.ts b/src/index.ts index 5e22427..bbd402d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,21 @@ -import { app, ipcMain, dialog } from 'electron' +import { app, dialog, ipcMain } from 'electron' import * as os from 'os' import * as path from 'path' import { UserConfigs } from './config' import { LoadExtension } from './extension/extension' import { Global, InitGlobal, Logger } from './global' +import i18n from './i18n' import { LoadResourcePack } from './resourcepack/resourcepack' -import { LoadServer, ListenServer } from './server' +import { ListenServer, LoadServer } from './server' import { LoadTool } from './tool/tool' import bossKey from './utilities/bossKey' import openFile from './utilities/openFile' import sandbox from './utilities/sandbox' import screenshot from './utilities/screenshot' -import { initGameWindow, GameWindow } from './windows/game' +import { initPlayer, AudioPlayer } from './windows/audioPlayer' +import { GameWindows, initGameWindow } from './windows/game' import { initManagerWindow, ManagerWindow } from './windows/manager' import { initToolManager } from './windows/tool' -import { initPlayer } from './windows/audioPlayer' -import i18n from './i18n' // 初始化全局变量 InitGlobal() @@ -107,7 +107,7 @@ const shouldQuit = app.makeSingleInstance((argv, directory) => { } else { // GameWindow Mode if (argv.length > 2 + Number(process.env.NODE_ENV === 'development')) { - dialog.showMessageBox(GameWindow, { + dialog.showMessageBox(AudioPlayer, { type: 'info', title: i18n.text.main.programName(), // TODO: i18n @@ -144,6 +144,7 @@ app.on('ready', () => { if (!process.env.SERVER_ONLY) { // 初始化游戏窗口 initGameWindow() + GameWindows.newWindow() } else { // 通过 audioPlayer 窗口阻止程序退出 initPlayer() diff --git a/src/utilities/bossKey.ts b/src/utilities/bossKey.ts index b774bfe..a676a41 100644 --- a/src/utilities/bossKey.ts +++ b/src/utilities/bossKey.ts @@ -1,24 +1,19 @@ import Utility from './utility' import { BrowserWindow, globalShortcut } from 'electron' -import { MajsoulPlus } from '../majsoul_plus' -import { GameWindowStatus, GameWindow } from '../windows/game' -import { ManagerWindow, ManagerWindowStatus } from '../windows/manager' +import { GameWindows } from '../windows/game' +import { ManagerWindow } from '../windows/manager' -function hideWindow(window: BrowserWindow, option: MajsoulPlus.WindowStatus) { - if (window) { - option.visible = window.isVisible() - option.muted = ManagerWindow.webContents.isAudioMuted() +function hideWindow(window: BrowserWindow) { + if (window && !window.isDestroyed()) { window.hide() window.webContents.setAudioMuted(true) } } -function showWindow(window: BrowserWindow, option: MajsoulPlus.WindowStatus) { - if (window) { - if (option.visible) { - window.show() - } - window.webContents.setAudioMuted(option.muted) +function showWindow(window: BrowserWindow) { + if (window && !window.isDestroyed()) { + window.show() + window.webContents.setAudioMuted(false) } } @@ -34,12 +29,12 @@ class BossKey extends Utility { globalShortcut.register('Alt+X', () => { if (this.isActive) { // 备份窗口信息 & 隐藏窗口 - hideWindow(ManagerWindow, ManagerWindowStatus) - hideWindow(GameWindow, GameWindowStatus) + hideWindow(ManagerWindow) + GameWindows.forEach(window => hideWindow(window)) } else { // 重新显示窗口 - showWindow(ManagerWindow, ManagerWindowStatus) - showWindow(GameWindow, GameWindowStatus) + showWindow(ManagerWindow) + GameWindows.forEach(window => showWindow(window)) } }) } diff --git a/src/utilities/screenshot.ts b/src/utilities/screenshot.ts index 5db23b0..765e99f 100644 --- a/src/utilities/screenshot.ts +++ b/src/utilities/screenshot.ts @@ -1,8 +1,8 @@ -import Utility from './utility' -import { ipcMain, app, clipboard, nativeImage } from 'electron' +import { app, clipboard, ipcMain, nativeImage } from 'electron' import * as path from 'path' import { writeFile } from '../utils' -import { GameWindow } from '../windows/game' +import { GameWindows } from '../windows/game' +import Utility from './utility' class ScreenShot extends Utility { constructor() { @@ -11,7 +11,7 @@ class ScreenShot extends Utility { } protected execute() { - ipcMain.on('save-screenshot', (event, buf: Buffer) => { + ipcMain.on('save-screenshot', (event, index: number, buf: Buffer) => { // 接收到的截图 Buffer const buffer: Buffer = buf // 由主进程进行保存 @@ -23,7 +23,7 @@ class ScreenShot extends Utility { // 写入文件 writeFile(filePath, buffer).then(() => { // 通知渲染进程(游戏宿主窗口)截图已保存,并由渲染进程弹窗 - GameWindow.webContents.send('screenshot-saved', filePath) + GameWindows.get(index).webContents.send('screenshot-saved', filePath) }) // 写入图像到剪切板 clipboard.writeImage(nativeImage.createFromBuffer(buffer)) diff --git a/src/utils.ts b/src/utils.ts index e018ab9..9a62e0c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -187,7 +187,7 @@ export function mkdirsSync(dirname: string) { export function getRemoteUrl(originalUrl: string): string { return ( RemoteDomains[UserConfigs.userData.serverToPlay].domain + - originalUrl.replace(/^\/(0\/)?/g, '') + originalUrl.replace(/^\/\d\/?/g, '') ) } diff --git a/src/windows/game.ts b/src/windows/game.ts index 8cf831b..d266753 100644 --- a/src/windows/game.ts +++ b/src/windows/game.ts @@ -4,6 +4,7 @@ import { dialog, ipcMain, Menu, + MenuItem, screen } from 'electron' import { AddressInfo } from 'net' @@ -11,7 +12,6 @@ import * as path from 'path' import { SaveConfigJson, UserConfigs } from '../config' import { Global, Logger, RemoteDomains } from '../global' import i18n from '../i18n' -import { MajsoulPlus } from '../majsoul_plus' import { CloseServer, httpServer, @@ -24,66 +24,101 @@ import { AudioPlayer, initPlayer, shutoffPlayer } from './audioPlayer' import { ManagerWindow } from './manager' // tslint:disable-next-line -export let GameWindow: BrowserWindow -// tslint:disable-next-line -export const GameWindowStatus: MajsoulPlus.WindowStatus = { - visible: false, - muted: false +export class GameWindows { + private static windows: Map = new Map() + private static windowIdCount = 0 + private static windowCount = 0 + static get size() { + return this.windowCount + } + + static get(index: number) { + return this.windows.get(index) + } + + static newWindow() { + const id = this.windowIdCount++ + this.windowCount++ + this.windows.set(id, newGameWindow(id)) + + if (id === 0) { + initPlayer() + } + return id + } + + static destroyWindow(id: number) { + const window = this.windows.get(id) + this.windows.delete(id) + + if (this.windowCount === 1) { + // 关闭后台音频播放器 + shutoffPlayer() + // 关闭本地镜像服务器 + CloseServer() + // 依据用户设置显示被隐藏的管理器窗口 + if (UserConfigs.window.isManagerHide) { + if (ManagerWindow) { + ManagerWindow.show() + } + } + } + this.windowCount-- + } + + static forEach(callback: (window: BrowserWindow, id: number) => void) { + this.windows.forEach((window, id) => callback(window, id)) + } } export function initGameWindow() { + ipcMain.on( + 'save-local-storage', + (event: Electron.Event, localStorage: string[][]) => { + UserConfigs.localStorage[ + RemoteDomains[UserConfigs.userData.serverToPlay.toString()].name + ] = localStorage.filter(arr => arr[1] !== '' && arr[1] !== 'FKU!!!') + SaveConfigJson(UserConfigs) + dialog.showMessageBox(AudioPlayer, { + type: 'info', + title: i18n.text.main.programName(), + // TODO: i18n + message: '保存帐号信息成功!', + buttons: ['OK'] + }) + } + ) +} + +export function newGameWindow(id: number) { + let window: BrowserWindow + const config: BrowserWindowConstructorOptions = { ...Global.GameWindowConfig, - title: getGameWindowTitle(), - frame: !UserConfigs.window.isNoBorder + title: getGameWindowTitle(id), + frame: !UserConfigs.window.isNoBorder, + webPreferences: + GameWindows.size > 1 + ? { + partition: String(id) + } + : {} } if (UserConfigs.window.gameWindowSize !== '') { - const windowSize: number[] = UserConfigs.window.gameWindowSize + const windowSize = UserConfigs.window.gameWindowSize .split(',') .map((value: string) => Number(value)) config.width = windowSize[0] config.height = windowSize[1] } - GameWindow = new BrowserWindow(config) + window = new BrowserWindow(config) // 阻止标题更改 - GameWindow.on('page-title-updated', event => event.preventDefault()) - - // 监听到崩溃事件,输出 console - GameWindow.webContents.on('crashed', () => - Logger.warning(i18n.text.main.webContentsCrashed()) - ) + window.on('page-title-updated', event => event.preventDefault()) - // 监听尺寸更改事件,用于正确得到截图所需要的窗口尺寸 - GameWindow.on('resize', () => { - UserConfigs.window.gameWindowSize = GameWindow.getSize().toString() - if (!ManagerWindow.isDestroyed()) { - ManagerWindow.webContents.send( - 'change-config-game-window-size', - UserConfigs.window.gameWindowSize - ) - } - }) - - GameWindow.on('closed', () => { - // 关闭后台音频播放器 - shutoffPlayer() - // 关闭本地镜像服务器 - CloseServer() - // 依据用户设置显示被隐藏的管理器窗口 - if (UserConfigs.window.isManagerHide) { - if (ManagerWindow) { - ManagerWindow.show() - } - } - }) - - initPlayer() - Menu.setApplicationMenu(getGameWindowMenu()) - - ipcMain.on('main-loader-ready', () => { + window.webContents.on('dom-ready', () => { // 加载本地服务器地址 const http = UserConfigs.userData.useHttpServer const port = (UserConfigs.userData.useHttpServer @@ -93,46 +128,63 @@ export function initGameWindow() { const url = `http${ UserConfigs.userData.useHttpServer ? '' : 's' }://localhost:${port}/` - GameWindow.webContents.send('load-url', url, port, http) + + window.webContents.send( + 'load-url', + url, + port, + http, + id > 0 ? String(id) : undefined + ) }) - ipcMain.on( - 'save-local-storage', - (event: Electron.Event, localStorage: string[][]) => { - UserConfigs.localStorage[ - RemoteDomains[UserConfigs.userData.serverToPlay.toString()].name - ] = localStorage.filter(arr => arr[1] !== '' && arr[1] !== 'FKU!!!') - SaveConfigJson(UserConfigs) - dialog.showMessageBox(GameWindow, { - type: 'info', - title: i18n.text.main.programName(), - // TODO: i18n - message: '保存帐号信息成功!', - buttons: ['OK'] - }) - } + window.on('close', () => { + // TODO: 退出确认 + GameWindows.destroyWindow(id) + }) + + // 监听到崩溃事件,输出 console + window.webContents.on('crashed', () => + Logger.error(i18n.text.main.webContentsCrashed()) ) - GameWindow.once('ready-to-show', () => { + // 当且仅当只有一个游戏窗口时修改游戏窗口 + // 监听尺寸更改事件,保存用户的窗口大小 + if (GameWindows.size === 1) { + window.on('resize', () => { + UserConfigs.window.gameWindowSize = window.getSize().toString() + if (!ManagerWindow.isDestroyed()) { + ManagerWindow.webContents.send( + 'change-config-game-window-size', + UserConfigs.window.gameWindowSize + ) + } + }) + } + + Menu.setApplicationMenu(getGameWindowMenu(id)) + + window.once('ready-to-show', () => { // 设置页面缩放比例为 1 来防止缩放比例异常 // 但这样会造成截图提示悬浮窗尺寸不合适 - GameWindow.webContents.setZoomFactor(1) - GameWindow.show() + window.webContents.setZoomFactor(1) + window.show() }) // 载入本地启动器 - GameWindow.loadURL('file://' + path.join(__dirname, '../bin/main/index.html')) + window.loadURL('file://' + path.join(__dirname, '../bin/main/index.html')) - // Detect environment variable to open developer tools // 在 debug 启动环境下打开开发者工具 if (process.env.NODE_ENV === 'development') { - GameWindow.webContents.openDevTools({ + window.webContents.openDevTools({ mode: 'detach' }) } + + return window } -function getGameWindowMenu() { +function getGameWindowMenu(id: number) { const template = [ ...(process.platform === 'darwin' ? [ @@ -161,52 +213,58 @@ function getGameWindowMenu() { label: '截图', accelerator: acc, visible: index === 0, - click: () => { - takeScreenshot() + click: (item: MenuItem, window: BrowserWindow) => { + takeScreenshot(id) } } }), { label: '写入帐号信息', accelerator: 'CmdOrCtrl+Y', - click: () => { - GameWindow.webContents.send('get-local-storage') + click: (item: MenuItem, window: BrowserWindow) => { + window.webContents.send('get-local-storage') } }, - ...(process.platform === 'darwin' - ? [] - : [ - { - label: '退出游戏', - accelerator: 'Alt+F4', - click: () => { - GameWindow.close() - } - } - ]) + { + label: GameWindows.size > 1 ? '关闭窗口' : '结束游戏', + accelerator: 'Alt+F4', + click: (item: MenuItem, window: BrowserWindow) => { + window.close() + } + } ] }, { label: '窗口', role: 'window', submenu: [ + { + label: '多开', + accelerator: 'CmdOrCtrl+Shift+N', + click: (item: MenuItem, window: BrowserWindow) => { + GameWindows.newWindow() + } + }, { label: '置顶', accelerator: 'CmdOrCtrl+T', - click: () => { - GameWindow.setAlwaysOnTop(!GameWindow.isAlwaysOnTop()) + type: 'checkbox' as 'checkbox', + click: (item: MenuItem, window: BrowserWindow) => { + window.setAlwaysOnTop(!window.isAlwaysOnTop()) } }, ...['F11', 'F5'].map((acc, index) => { return { label: '全屏', accelerator: acc, + type: 'checkbox' as 'checkbox', + enabled: GameWindows.size === 1, visible: index === 0, - click: () => { + click: (item: MenuItem, window: BrowserWindow) => { if (!UserConfigs.window.isKioskModeOn) { - GameWindow.setFullScreen(!GameWindow.isFullScreen()) + window.setFullScreen(!window.isFullScreen()) } else { - GameWindow.setKiosk(!GameWindow.isKiosk()) + window.setKiosk(!window.isKiosk()) } } } @@ -223,7 +281,7 @@ function getGameWindowMenu() { submenu: Object.entries(ToolManager.getDetails()).map(([id, tool]) => { return { label: tool.metadata.name, - click: () => { + click: (item: MenuItem, window: BrowserWindow) => { ipcMain.emit('start-tool', {}, id) } } @@ -233,25 +291,42 @@ function getGameWindowMenu() { label: '开发', submenu: [ { - label: '重新载入', - accelerator: 'CmdOrCtrl+R', - click: () => { + label: '重载全部资源', + accelerator: 'Shift+Alt+R', + click: (item: MenuItem, window: BrowserWindow) => { CloseServer() ipcMain.emit('refresh-resourcepack', {}) ipcMain.emit('refresh-extension', {}) LoadServer() ListenServer(Global.ServerPort) - GameWindow.reload() + GameWindows.forEach(window => window.reload()) + } + }, + { + label: '重新载入本窗口', + accelerator: 'CmdOrCtrl+R', + click: (item: MenuItem, window: BrowserWindow) => { + window.reload() + } + }, + { + label: '重新载入所有窗口', + accelerator: 'CmdOrCtrl+Shift+R', + click: (item: MenuItem, window: BrowserWindow) => { + // FIXME: 某些情况下会直接卡死 + // 非常快速地创建新窗口可以稳定复现(一秒内5个左右) + // 原因未知 但用户应该不会用这么快手速多开( + GameWindows.forEach(window => window.reload()) } }, { label: '开发者工具', - accelerator: 'CmdOrCtrl+I', - click: () => { + accelerator: 'CmdOrCtrl+Shift+I', + click: (item: MenuItem, window: BrowserWindow) => { if (process.env.NODE_ENV === 'development') { - GameWindow.webContents.openDevTools({ mode: 'detach' }) + window.webContents.openDevTools({ mode: 'detach' }) } - GameWindow.webContents.send('open-devtools') + window.webContents.send('open-devtools') } } ] @@ -262,7 +337,7 @@ function getGameWindowMenu() { } // 获取窗口标题,有 0.5% 概率显示为喵喵喵 -function getGameWindowTitle(): string { +function getGameWindowTitle(id: number): string { // 彩蛋标题 const titles = [ { @@ -290,22 +365,23 @@ function getGameWindowTitle(): string { return null }, null) - return titles[index].text + return titles[index].text + (id > 0 ? ` #${id}` : '') } // 截取屏幕画面 -function takeScreenshot() { +function takeScreenshot(id: number) { AudioPlayer.webContents.send( 'audio-play', path.join(__dirname, '../bin/audio/screenshot.mp3') ) - const rect = GameWindow.getBounds() + const window = GameWindows.get(id) + const rect = window.getBounds() const display = screen.getDisplayMatching({ x: Math.floor(rect.x), y: Math.floor(rect.y), width: Math.floor(rect.width), height: Math.floor(rect.height) }) - GameWindow.webContents.send('take-screenshot', display.scaleFactor) + window.webContents.send('take-screenshot', id, display.scaleFactor) }