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)
}