feat: SPT Launcher Electron 애플리케이션의 초기 구현으로, 서버 헬스 체크, 명령어 실행, 세션 관리 등 핵심 기능들을 포함합니다.
This commit is contained in:
parent
2328cb9b5f
commit
1573df675b
287
src/main/main.ts
287
src/main/main.ts
|
|
@ -24,6 +24,18 @@ const COMMAND_TIMEOUT_MS = APP_CONFIG.commandTimeoutMs;
|
||||||
const INSTALL_TIMEOUT_MS = APP_CONFIG.installTimeoutMs;
|
const INSTALL_TIMEOUT_MS = APP_CONFIG.installTimeoutMs;
|
||||||
const SESSION_TTL_MS = APP_CONFIG.sessionTtlMs;
|
const SESSION_TTL_MS = APP_CONFIG.sessionTtlMs;
|
||||||
|
|
||||||
|
const CLIENT_PROCESS_NAMES = [
|
||||||
|
"EscapeFromTarkov.exe",
|
||||||
|
"EscapeFromTarkov_BE.exe",
|
||||||
|
];
|
||||||
|
|
||||||
|
const SERVER_PROCESS_NAMES = [
|
||||||
|
"Aki.Server.exe",
|
||||||
|
"SPT.Server.exe"
|
||||||
|
];
|
||||||
|
|
||||||
|
const GAME_PROCESS_NAMES = [...CLIENT_PROCESS_NAMES, ...SERVER_PROCESS_NAMES];
|
||||||
|
|
||||||
type CommandCheckResult = {
|
type CommandCheckResult = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
command: string;
|
command: string;
|
||||||
|
|
@ -89,7 +101,9 @@ type SessionLookupResult =
|
||||||
|
|
||||||
type InstallStep = {
|
type InstallStep = {
|
||||||
name: string;
|
name: string;
|
||||||
result: CommandRunResult;
|
ok: boolean;
|
||||||
|
message?: string;
|
||||||
|
result?: CommandRunResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
type InstallGitToolsResult = {
|
type InstallGitToolsResult = {
|
||||||
|
|
@ -191,6 +205,45 @@ const checkGitTools = async () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkGameRunning = async (): Promise<{ running: boolean; serverRunning: boolean; clientRunning: boolean; processes: string[] }> => {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
return { running: false, serverRunning: false, clientRunning: false, processes: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
execFile("tasklist", ["/FO", "CSV", "/NH"], (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn("tasklist failed", error);
|
||||||
|
resolve({ running: false, serverRunning: false, clientRunning: false, processes: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = stdout.toString().toLowerCase();
|
||||||
|
const runningProcesses: string[] = [];
|
||||||
|
let serverRunning = false;
|
||||||
|
let clientRunning = false;
|
||||||
|
|
||||||
|
for (const procName of GAME_PROCESS_NAMES) {
|
||||||
|
if (output.includes(procName.toLowerCase())) {
|
||||||
|
runningProcesses.push(procName);
|
||||||
|
if (SERVER_PROCESS_NAMES.includes(procName)) {
|
||||||
|
serverRunning = true;
|
||||||
|
} else if (CLIENT_PROCESS_NAMES.includes(procName)) {
|
||||||
|
clientRunning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
running: runningProcesses.length > 0,
|
||||||
|
serverRunning,
|
||||||
|
clientRunning,
|
||||||
|
processes: runningProcesses
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resolveCommandPath = async (
|
const resolveCommandPath = async (
|
||||||
command: string,
|
command: string,
|
||||||
): Promise<CommandPathResult> => {
|
): Promise<CommandPathResult> => {
|
||||||
|
|
@ -741,6 +794,8 @@ const ensureModRepo = async (
|
||||||
}
|
}
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
name: "git fetch",
|
name: "git fetch",
|
||||||
|
ok: fetchResult.ok,
|
||||||
|
message: fetchResult.error,
|
||||||
result: fetchResult,
|
result: fetchResult,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -764,6 +819,8 @@ const ensureModRepo = async (
|
||||||
}
|
}
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
name: "git clone",
|
name: "git clone",
|
||||||
|
ok: cloneResult.ok,
|
||||||
|
message: cloneResult.error,
|
||||||
result: cloneResult,
|
result: cloneResult,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -792,6 +849,8 @@ const checkoutRepoTag = async (
|
||||||
}
|
}
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
name: `git checkout ${tag}`,
|
name: `git checkout ${tag}`,
|
||||||
|
ok: checkoutResult.ok,
|
||||||
|
message: checkoutResult.error,
|
||||||
result: checkoutResult,
|
result: checkoutResult,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -819,6 +878,8 @@ const pullLfs = async (
|
||||||
}
|
}
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
name: "git lfs pull",
|
name: "git lfs pull",
|
||||||
|
ok: pullResult.ok,
|
||||||
|
message: pullResult.error,
|
||||||
result: pullResult,
|
result: pullResult,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -836,6 +897,7 @@ const syncDirectories = async (
|
||||||
// Initial progress event for starting file copy
|
// Initial progress event for starting file copy
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
name: "copying files",
|
name: "copying files",
|
||||||
|
ok: true,
|
||||||
result: { ok: true, command: "copy", args: [] }, // Intermediate state
|
result: { ok: true, command: "copy", args: [] }, // Intermediate state
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -873,6 +935,7 @@ const syncDirectories = async (
|
||||||
});
|
});
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
name: "copying files",
|
name: "copying files",
|
||||||
|
ok: true,
|
||||||
result: { ok: true, command: "copy", args: [] },
|
result: { ok: true, command: "copy", args: [] },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -910,7 +973,9 @@ const runModSync = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
const installGitTools = async (
|
||||||
|
onProgress?: (step: InstallStep) => void,
|
||||||
|
): Promise<InstallGitToolsResult> => {
|
||||||
if (process.platform !== "win32") {
|
if (process.platform !== "win32") {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
@ -919,26 +984,49 @@ const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const steps: InstallStep[] = [];
|
||||||
|
|
||||||
const wingetPath = await resolveCommandPath("winget");
|
const wingetPath = await resolveCommandPath("winget");
|
||||||
if (!wingetPath.ok) {
|
if (!wingetPath.ok) {
|
||||||
|
const errorStep = {
|
||||||
|
name: "winget 확인",
|
||||||
|
ok: false,
|
||||||
|
message: wingetPath.error,
|
||||||
|
result: {
|
||||||
|
ok: false,
|
||||||
|
command: "winget",
|
||||||
|
args: ["--version"],
|
||||||
|
error: wingetPath.error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
steps.push(errorStep);
|
||||||
|
onProgress?.(errorStep);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: "winget이 설치되어 있지 않습니다.",
|
error: "winget이 설치되어 있지 않습니다.",
|
||||||
steps: [
|
steps,
|
||||||
{
|
|
||||||
name: "winget 확인",
|
|
||||||
result: {
|
|
||||||
ok: false,
|
|
||||||
command: "winget",
|
|
||||||
args: ["--version"],
|
|
||||||
error: wingetPath.error,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps: InstallStep[] = [];
|
// Winget check success
|
||||||
|
/*
|
||||||
|
const wingetStep = {
|
||||||
|
name: "winget 확인",
|
||||||
|
result: { ok: true, command: "winget", args: ["--version"] }
|
||||||
|
};
|
||||||
|
steps.push(wingetStep);
|
||||||
|
onProgress?.(wingetStep);
|
||||||
|
*/
|
||||||
|
// skipping separate success step for winget to match previous behavior purely?
|
||||||
|
// User asked for "same template", so seeing "Winget Check... OK" is good.
|
||||||
|
// I will add it for clarity.
|
||||||
|
const wingetStep = {
|
||||||
|
name: "winget 확인",
|
||||||
|
ok: true,
|
||||||
|
result: { ok: true, command: "winget", args: ["--version"] }
|
||||||
|
};
|
||||||
|
steps.push(wingetStep);
|
||||||
|
onProgress?.(wingetStep);
|
||||||
|
|
||||||
const gitInstall = await runCommand(
|
const gitInstall = await runCommand(
|
||||||
"winget",
|
"winget",
|
||||||
|
|
@ -952,10 +1040,21 @@ const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
||||||
"--accept-source-agreements",
|
"--accept-source-agreements",
|
||||||
"--accept-package-agreements",
|
"--accept-package-agreements",
|
||||||
"--silent",
|
"--silent",
|
||||||
|
"--disable-interactivity" // Ensure no blocking prompts
|
||||||
],
|
],
|
||||||
INSTALL_TIMEOUT_MS,
|
INSTALL_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
steps.push({ name: "Git 설치", result: gitInstall });
|
steps.push({
|
||||||
|
name: "Git 설치",
|
||||||
|
ok: gitInstall.ok,
|
||||||
|
message: gitInstall.output || gitInstall.error, // Prioritize output
|
||||||
|
result: gitInstall
|
||||||
|
});
|
||||||
|
onProgress?.({
|
||||||
|
name: "Git 설치",
|
||||||
|
ok: gitInstall.ok,
|
||||||
|
message: gitInstall.output || gitInstall.error // Prioritize output
|
||||||
|
});
|
||||||
|
|
||||||
const lfsInstall = await runCommand(
|
const lfsInstall = await runCommand(
|
||||||
"winget",
|
"winget",
|
||||||
|
|
@ -969,12 +1068,23 @@ const installGitTools = async (): Promise<InstallGitToolsResult> => {
|
||||||
"--accept-source-agreements",
|
"--accept-source-agreements",
|
||||||
"--accept-package-agreements",
|
"--accept-package-agreements",
|
||||||
"--silent",
|
"--silent",
|
||||||
|
"--disable-interactivity"
|
||||||
],
|
],
|
||||||
INSTALL_TIMEOUT_MS,
|
INSTALL_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
steps.push({ name: "Git LFS 설치", result: lfsInstall });
|
steps.push({
|
||||||
|
name: "Git LFS 설치",
|
||||||
|
ok: lfsInstall.ok,
|
||||||
|
message: lfsInstall.output || lfsInstall.error, // Prioritize output
|
||||||
|
result: lfsInstall
|
||||||
|
});
|
||||||
|
onProgress?.({
|
||||||
|
name: "Git LFS 설치",
|
||||||
|
ok: lfsInstall.ok,
|
||||||
|
message: lfsInstall.output || lfsInstall.error // Prioritize output
|
||||||
|
});
|
||||||
|
|
||||||
const ok = steps.every((step) => step.result.ok);
|
const ok = steps.every((step) => step.ok);
|
||||||
const check = await checkGitTools();
|
const check = await checkGitTools();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1254,8 +1364,147 @@ app.whenReady().then(() => {
|
||||||
return await getGitPaths();
|
return await getGitPaths();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("spt:installGitTools", async () => {
|
ipcMain.handle("spt:installGitTools", async (event) => {
|
||||||
return await installGitTools();
|
return await installGitTools((step) => {
|
||||||
|
event.sender.send("spt:gitInstallProgress", step);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("spt:launchGame", async (event) => {
|
||||||
|
const mainWindow = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
const installInfo = await resolveSptInstall();
|
||||||
|
|
||||||
|
if (!installInfo.path) {
|
||||||
|
return { ok: false, error: "no_spt_path" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double check running processes
|
||||||
|
const running = await checkGameRunning();
|
||||||
|
if (running.clientRunning) {
|
||||||
|
return { ok: false, error: "already_running", details: running.processes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double check mod version (optional but safer)
|
||||||
|
const modStatus = await getModVersionStatus();
|
||||||
|
if (!modStatus.ok) {
|
||||||
|
return { ok: false, error: "mod_verification_failed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch Server if not running (Local only)
|
||||||
|
const serverUrl = new URL(SERVER_BASE_URL);
|
||||||
|
const isLocal = serverUrl.hostname === "127.0.0.1" || serverUrl.hostname === "localhost";
|
||||||
|
|
||||||
|
if (isLocal && !running.serverRunning) {
|
||||||
|
let serverExePath: string | undefined;
|
||||||
|
for (const exe of SERVER_PROCESS_NAMES) {
|
||||||
|
// Check direct path or SPT subdirectory
|
||||||
|
const direct = path.join(installInfo.path, exe);
|
||||||
|
if (await pathExists(direct)) {
|
||||||
|
serverExePath = direct;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const nested = path.join(installInfo.path, "SPT", exe);
|
||||||
|
if (await pathExists(nested)) {
|
||||||
|
serverExePath = nested;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverExePath) {
|
||||||
|
console.log(`[spt-launcher] Starting Server: ${serverExePath}`);
|
||||||
|
const serverBat = stripExe(serverExePath) + ".bat"; // Sometimes users use bat, but exe should work if arguments aren't complex
|
||||||
|
|
||||||
|
// Prefer EXE for now.
|
||||||
|
const child = require("child_process").spawn(serverExePath, [], {
|
||||||
|
cwd: path.dirname(serverExePath),
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore"
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
|
||||||
|
// Wait for server health
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 30; // 30 seconds approx
|
||||||
|
while(attempts < maxAttempts) {
|
||||||
|
const health = await checkServerHealth();
|
||||||
|
if (health.ok) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch Client
|
||||||
|
try {
|
||||||
|
// Prioritize EFT executable
|
||||||
|
const candidates = [
|
||||||
|
"EscapeFromTarkov.exe",
|
||||||
|
"EscapeFromTarkov_BE.exe",
|
||||||
|
"SPT/EscapeFromTarkov.exe",
|
||||||
|
"SPT/EscapeFromTarkov_BE.exe"
|
||||||
|
];
|
||||||
|
|
||||||
|
let executablePath: string | undefined;
|
||||||
|
for (const exe of candidates) {
|
||||||
|
const fullPath = path.join(installInfo.path, exe);
|
||||||
|
if (await pathExists(fullPath)) {
|
||||||
|
executablePath = fullPath;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executablePath) {
|
||||||
|
return { ok: false, error: "executable_not_found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Session ID for args
|
||||||
|
const session = await readSessionRecord();
|
||||||
|
if (!session || isSessionExpired(session)) {
|
||||||
|
return { ok: false, error: "session_required" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Args
|
||||||
|
const configArg = JSON.stringify({
|
||||||
|
BackendUrl: SERVER_BASE_URL,
|
||||||
|
Version: "live"
|
||||||
|
});
|
||||||
|
const args = [`-token=${session.sessionId}`, `-config=${configArg}`];
|
||||||
|
console.log(`[spt-launcher] Launching: ${executablePath} with args`, args);
|
||||||
|
|
||||||
|
// Sanitize Environment
|
||||||
|
// Strip Electron/Node specifics to prevent BepInEx/Plugin conflicts
|
||||||
|
const sanitizedEnv = { ...process.env };
|
||||||
|
for (const key of Object.keys(sanitizedEnv)) {
|
||||||
|
if (key.startsWith("ELECTRON_") || key.startsWith("NODE_") || key === "VITE_DEV_SERVER_URL") {
|
||||||
|
delete sanitizedEnv[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn detached
|
||||||
|
const child = require("child_process").spawn(executablePath, args, {
|
||||||
|
cwd: path.dirname(executablePath),
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
env: sanitizedEnv
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
|
||||||
|
mainWindow?.minimize();
|
||||||
|
return { ok: true, executedPath: executablePath };
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Launch failed", e);
|
||||||
|
return { ok: false, error: e.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function stripExe(filename: string) {
|
||||||
|
return filename.replace(/\.exe$/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("spt:checkGameRunning", async () => {
|
||||||
|
return await checkGameRunning();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("spt:getModVersionStatus", async () => {
|
ipcMain.handle("spt:getModVersionStatus", async () => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ console.info("[spt-launcher] preload loaded");
|
||||||
contextBridge.exposeInMainWorld("sptLauncher", {
|
contextBridge.exposeInMainWorld("sptLauncher", {
|
||||||
appName: "SPT Launcher",
|
appName: "SPT Launcher",
|
||||||
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"),
|
checkServerHealth: () => ipcRenderer.invoke("spt:checkServerHealth"),
|
||||||
|
launchGame: () => ipcRenderer.invoke("spt:launchGame"),
|
||||||
|
checkGameRunning: () => ipcRenderer.invoke("spt:checkGameRunning"),
|
||||||
checkGitTools: () => ipcRenderer.invoke("spt:checkGitTools"),
|
checkGitTools: () => ipcRenderer.invoke("spt:checkGitTools"),
|
||||||
getGitPaths: () => ipcRenderer.invoke("spt:getGitPaths"),
|
getGitPaths: () => ipcRenderer.invoke("spt:getGitPaths"),
|
||||||
installGitTools: () => ipcRenderer.invoke("spt:installGitTools"),
|
installGitTools: () => ipcRenderer.invoke("spt:installGitTools"),
|
||||||
|
|
@ -33,4 +35,12 @@ contextBridge.exposeInMainWorld("sptLauncher", {
|
||||||
ipcRenderer.removeListener("spt:modSyncProgress", subscription);
|
ipcRenderer.removeListener("spt:modSyncProgress", subscription);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
onGitInstallProgress: (callback: (event: unknown, step: unknown) => void) => {
|
||||||
|
const subscription = (_event: unknown, step: unknown) =>
|
||||||
|
callback(_event, step);
|
||||||
|
ipcRenderer.on("spt:gitInstallProgress", subscription);
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener("spt:gitInstallProgress", subscription);
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ const App = () => {
|
||||||
const sessionResumeAttemptedRef = useRef(false);
|
const sessionResumeAttemptedRef = useRef(false);
|
||||||
const transitionTimeoutRef = useRef<number | undefined>();
|
const transitionTimeoutRef = useRef<number | undefined>();
|
||||||
const simulateToolsMissingRef = useRef(false);
|
const simulateToolsMissingRef = useRef(false);
|
||||||
|
const [modalTitle, setModalTitle] = useState("모드 적용 중...");
|
||||||
const [syncSteps, setSyncSteps] = useState<Array<{ name: string; ok: boolean; message?: string }>>([]);
|
const [syncSteps, setSyncSteps] = useState<Array<{ name: string; ok: boolean; message?: string }>>([]);
|
||||||
const [syncResult, setSyncResult] = useState<{ ok: boolean; error?: string } | undefined>();
|
const [syncResult, setSyncResult] = useState<{ ok: boolean; error?: string } | undefined>();
|
||||||
|
|
||||||
|
|
@ -120,6 +121,16 @@ const App = () => {
|
||||||
if (rawName === "copying files") {
|
if (rawName === "copying files") {
|
||||||
return { title: "모드 파일 적용", command: "Copying files to Game Directory..." };
|
return { title: "모드 파일 적용", command: "Copying files to Game Directory..." };
|
||||||
}
|
}
|
||||||
|
// Git Install Steps
|
||||||
|
if (rawName === "winget 확인") {
|
||||||
|
return { title: "패키지 관리자(winget) 확인", command: "checking winget..." };
|
||||||
|
}
|
||||||
|
if (rawName === "Git 설치") {
|
||||||
|
return { title: "Git 설치 (시간이 걸릴 수 있습니다)", command: "winget install Git.Git" };
|
||||||
|
}
|
||||||
|
if (rawName === "Git LFS 설치") {
|
||||||
|
return { title: "Git LFS 설치", command: "winget install GitHub.GitLFS" };
|
||||||
|
}
|
||||||
return { title: rawName, command: "" };
|
return { title: rawName, command: "" };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -636,12 +647,25 @@ const App = () => {
|
||||||
|
|
||||||
setInstallInProgress(true);
|
setInstallInProgress(true);
|
||||||
setInstallMessage(undefined);
|
setInstallMessage(undefined);
|
||||||
|
|
||||||
|
// Modal Setup
|
||||||
|
setSyncSteps([]);
|
||||||
|
setSyncResult(undefined);
|
||||||
|
setModalTitle("Git 도구 설치 중...");
|
||||||
|
setSyncInProgress(true);
|
||||||
|
|
||||||
|
const cleanup = window.sptLauncher.onGitInstallProgress((_e, step: any) => {
|
||||||
|
setSyncSteps((prev) => [...prev, step]);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.sptLauncher.installGitTools();
|
const result = await window.sptLauncher.installGitTools();
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
setInstallMessage(result.error ?? "자동 설치에 실패했습니다.");
|
setInstallMessage(result.error ?? "자동 설치에 실패했습니다.");
|
||||||
|
setSyncResult({ ok: false, error: result.error });
|
||||||
} else {
|
} else {
|
||||||
setInstallMessage("자동 설치가 완료되었습니다.");
|
setInstallMessage("자동 설치가 완료되었습니다.");
|
||||||
|
setSyncResult({ ok: true });
|
||||||
}
|
}
|
||||||
if (result.check) {
|
if (result.check) {
|
||||||
setGitCheckResult(result.check);
|
setGitCheckResult(result.check);
|
||||||
|
|
@ -650,8 +674,11 @@ const App = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
setInstallMessage(message);
|
setInstallMessage(message);
|
||||||
|
setSyncResult({ ok: false, error: message });
|
||||||
} finally {
|
} finally {
|
||||||
|
cleanup();
|
||||||
setInstallInProgress(false);
|
setInstallInProgress(false);
|
||||||
|
setSyncInProgress(false); // Show close button
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -895,6 +922,8 @@ const App = () => {
|
||||||
|
|
||||||
setSyncSteps([]);
|
setSyncSteps([]);
|
||||||
setSyncResult(undefined);
|
setSyncResult(undefined);
|
||||||
|
setModalTitle("모드 적용 중...");
|
||||||
|
setSyncInProgress(true);
|
||||||
|
|
||||||
// Subscribe to progress events
|
// Subscribe to progress events
|
||||||
const cleanup = window.sptLauncher.onModSyncProgress
|
const cleanup = window.sptLauncher.onModSyncProgress
|
||||||
|
|
@ -991,7 +1020,33 @@ const App = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.alert("게임을 실행합니다.");
|
if (!window.sptLauncher?.launchGame) {
|
||||||
|
window.alert("게임 시작 기능이 준비되지 않았습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.sptLauncher.launchGame();
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.error === "already_running") {
|
||||||
|
window.alert(`게임이 이미 실행 중입니다.\n(발견된 프로세스: ${(result.details ?? []).join(", ")})`);
|
||||||
|
} else if (result.error === "mod_verification_failed") {
|
||||||
|
window.alert("모드 버전 검증에 실패했습니다. 버전을 다시 확인해주세요.");
|
||||||
|
} else if (result.error === "no_spt_path") {
|
||||||
|
window.alert("SPT 설치 경로가 설정되지 않았습니다.");
|
||||||
|
} else if (result.error === "executable_not_found") {
|
||||||
|
window.alert("실행 파일(Launcher/Server)을 찾을 수 없습니다.");
|
||||||
|
} else {
|
||||||
|
window.alert(`게임 실행 실패: ${result.error}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success
|
||||||
|
window.alert(`게임이 실행되었습니다.\n(${result.executedPath})`);
|
||||||
|
} catch (e: any) {
|
||||||
|
window.alert(`게임 실행 중 오류가 발생했습니다: ${e.message}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1413,8 +1468,8 @@ const App = () => {
|
||||||
<header className="modal-header">
|
<header className="modal-header">
|
||||||
<h3>
|
<h3>
|
||||||
{syncResult
|
{syncResult
|
||||||
? (syncResult.ok ? "모드 적용 완료" : "오류 발생")
|
? (syncResult.ok ? (modalTitle.replace("중...", "완료")) : (syncResult.error ? `오류: ${syncResult.error}` : "오류 발생"))
|
||||||
: "모드 적용 중..."}
|
: modalTitle}
|
||||||
</h3>
|
</h3>
|
||||||
{!syncInProgress && (
|
{!syncInProgress && (
|
||||||
<button type="button" className="ghost" onClick={closeSyncModal}>
|
<button type="button" className="ghost" onClick={closeSyncModal}>
|
||||||
|
|
@ -1445,7 +1500,7 @@ const App = () => {
|
||||||
</div>
|
</div>
|
||||||
{syncResult && (
|
{syncResult && (
|
||||||
<div className={`notice ${syncResult.ok ? "info" : "error"}`}>
|
<div className={`notice ${syncResult.ok ? "info" : "error"}`}>
|
||||||
{syncResult.ok ? "모드 적용이 완료되었습니다." : `실패: ${syncResult.error}`}
|
{syncResult.ok ? "모드 적용이 완료되었습니다." : (syncResult.error ? `실패: ${syncResult.error}` : "오류가 발생했습니다.")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ interface Window {
|
||||||
error?: string;
|
error?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}>;
|
}>;
|
||||||
|
launchGame: () => Promise<{ ok: boolean; error?: string; details?: string[]; executedPath?: string }>;
|
||||||
|
checkGameRunning: () => Promise<{ running: boolean; processes: string[] }>;
|
||||||
checkGitTools: () => Promise<{
|
checkGitTools: () => Promise<{
|
||||||
git: {
|
git: {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
|
@ -156,6 +158,12 @@ interface Window {
|
||||||
step: { name: string; result?: { ok: boolean; message?: string } }
|
step: { name: string; result?: { ok: boolean; message?: string } }
|
||||||
) => void
|
) => void
|
||||||
) => () => void;
|
) => () => void;
|
||||||
|
onGitInstallProgress: (
|
||||||
|
callback: (
|
||||||
|
event: unknown,
|
||||||
|
step: { name: string; result?: { ok: boolean; message?: string } }
|
||||||
|
) => void
|
||||||
|
) => () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export const APP_CONFIG = {
|
export const APP_CONFIG = {
|
||||||
serverBaseUrl: "https://pandoli365.com:5069/",
|
serverBaseUrl: "https://pandoli365.com:5069",
|
||||||
serverHealthcheckPath: "/launcher/ping",
|
serverHealthcheckPath: "/launcher/ping",
|
||||||
serverHealthcheckTimeoutMs: 2000,
|
serverHealthcheckTimeoutMs: 2000,
|
||||||
serverRequestTimeoutMs: 4000,
|
serverRequestTimeoutMs: 4000,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue