82 lines
3.8 KiB
TypeScript
82 lines
3.8 KiB
TypeScript
import type { GameExitedPayload, InstallProgressPayload } from '../types/launcher'
|
|
|
|
export type GameOperation =
|
|
| { phase: 'idle' }
|
|
| { phase: 'syncing' | 'launching' | 'running'; profileId: string }
|
|
| { phase: 'installing'; profileId: string; progress: InstallProgressPayload | null }
|
|
|
|
export interface GameState {
|
|
operation: GameOperation
|
|
error: string | null
|
|
}
|
|
|
|
export type GameAction =
|
|
| { type: 'sync'; profileId: string }
|
|
| { type: 'synced'; profileId: string }
|
|
| { type: 'repaired'; profileId: string }
|
|
| { type: 'install'; profileId: string }
|
|
| { type: 'progress'; progress: InstallProgressPayload }
|
|
| { type: 'launch'; profileId: string }
|
|
| { type: 'started'; profileId: string }
|
|
| { type: 'exited'; result: GameExitedPayload }
|
|
| { type: 'failed'; error: string }
|
|
|
|
export const initialGameState: GameState = { operation: { phase: 'idle' }, error: null }
|
|
|
|
export function gameReducer(state: GameState, action: GameAction): GameState {
|
|
const operation = state.operation
|
|
switch (action.type) {
|
|
case 'sync':
|
|
case 'install':
|
|
if (operation.phase !== 'idle') return state
|
|
return {
|
|
operation: action.type === 'sync'
|
|
? { phase: 'syncing', profileId: action.profileId }
|
|
: { phase: 'installing', profileId: action.profileId, progress: null },
|
|
error: null,
|
|
}
|
|
case 'synced':
|
|
return operation.phase === 'syncing' && operation.profileId === action.profileId
|
|
? initialGameState : state
|
|
case 'repaired':
|
|
return operation.phase === 'installing' && operation.profileId === action.profileId ? initialGameState : state
|
|
case 'progress':
|
|
if (operation.phase === 'installing' && action.progress.stage === 'launch') return { ...state, operation: { phase: 'launching', profileId: operation.profileId } }
|
|
return operation.phase === 'installing'
|
|
? { ...state, operation: { ...operation, progress: action.progress } } : state
|
|
case 'launch':
|
|
return operation.phase === 'installing' && operation.profileId === action.profileId
|
|
? { ...state, operation: { phase: 'launching', profileId: action.profileId } } : state
|
|
case 'started':
|
|
// A fast-exiting child can emit game-exited before invoke resolves.
|
|
return (operation.phase === 'launching' || operation.phase === 'installing') && operation.profileId === action.profileId
|
|
? { ...state, operation: { phase: 'running', profileId: action.profileId } } : state
|
|
case 'exited':
|
|
if ((operation.phase !== 'launching' && operation.phase !== 'running' && operation.phase !== 'installing') ||
|
|
operation.profileId !== action.result.profileId) return state
|
|
return {
|
|
operation: { phase: 'idle' },
|
|
error: action.result.exitCode === 0 ? null
|
|
: action.result.exitCode === null ? 'Игра завершилась без кода выхода. Проверьте журнал игры.'
|
|
: `Игра завершилась с кодом ${action.result.exitCode}. Проверьте журнал игры.`,
|
|
}
|
|
case 'failed':
|
|
return { operation: { phase: 'idle' }, error: action.error }
|
|
}
|
|
}
|
|
|
|
export const installStageLabels: Record<InstallProgressPayload['stage'], string> = {
|
|
mods: 'Обновляем сборку',
|
|
launch: 'Запускаем игру',
|
|
java: 'Готовим Java',
|
|
neoforge: 'Устанавливаем NeoForge',
|
|
libraries: 'Скачиваем библиотеки',
|
|
assets: 'Скачиваем ресурсы игры',
|
|
}
|
|
|
|
export function installPercent(progress: InstallProgressPayload | null): number | null {
|
|
if (!progress || progress.totalBytes <= 0 || !Number.isFinite(progress.totalBytes) ||
|
|
!Number.isFinite(progress.currentBytes)) return null
|
|
return Math.max(0, Math.min(100, Math.round(progress.currentBytes / progress.totalBytes * 100)))
|
|
}
|