feat: Unity creator settings

This commit is contained in:
2026-06-28 16:48:27 +07:00
parent 387fa18927
commit 27389914c6
14 changed files with 392 additions and 19 deletions
+4
View File
@@ -15,6 +15,8 @@ export const policies = {
representedGroup: { ttl: 15 * 60_000, staleWhileRevalidate: 60 * 60_000 }, representedGroup: { ttl: 15 * 60_000, staleWhileRevalidate: 60 * 60_000 },
group: { ttl: 30 * 60_000, staleWhileRevalidate: 2 * 60 * 60_000 }, group: { ttl: 30 * 60_000, staleWhileRevalidate: 2 * 60 * 60_000 },
instance: { ttl: 30_000, staleWhileRevalidate: 60_000 }, instance: { ttl: 30_000, staleWhileRevalidate: 60_000 },
apiConfig: { ttl: 6 * 60 * 60_000, staleWhileRevalidate: 24 * 60 * 60_000 },
unityChangeset: { ttl: 30 * 24 * 60 * 60_000 },
} satisfies Record<string, CachePolicy>; } satisfies Record<string, CachePolicy>;
export const cacheKeys = { export const cacheKeys = {
@@ -33,4 +35,6 @@ export const cacheKeys = {
representedGroup: (id: string) => `user:group:represented:${id}`, representedGroup: (id: string) => `user:group:represented:${id}`,
group: (id: string) => `group:${id}`, group: (id: string) => `group:${id}`,
instance: (location: string) => `instance:${location}`, instance: (location: string) => `instance:${location}`,
apiConfig: () => "api:config",
unityChangeset: (version: string) => `unity:changeset:${version}`,
}; };
+12 -3
View File
@@ -3,10 +3,11 @@ import { join } from "node:path";
import { existsSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { writeFileAtomicSync } from "../lib/atomicFile"; import { writeFileAtomicSync } from "../lib/atomicFile";
import type { AppConfig } from "../../shared/types/appConfig"; import type { AppConfig } from "../../shared/types/appConfig";
import { detectedGamePath } from "../game/steam";
const path = () => join(app.getPath("userData"), "app-config.json"); const path = () => join(app.getPath("userData"), "app-config.json");
type StoredConfig = Omit<AppConfig, "version">; type StoredConfig = Omit<AppConfig, "version" | "detectedGamePath">;
const DEFAULTS: StoredConfig = { gamePath: null }; const DEFAULTS: StoredConfig = { gamePath: null };
function readStored(): StoredConfig { function readStored(): StoredConfig {
@@ -17,8 +18,16 @@ function readStored(): StoredConfig {
} }
} }
function withDerived(stored: StoredConfig): AppConfig {
return { version: app.getVersion(), detectedGamePath: detectedGamePath(), ...stored };
}
export function gamePathOverride(): string | null {
return readStored().gamePath;
}
export function getConfig(): AppConfig { export function getConfig(): AppConfig {
return { version: app.getVersion(), ...readStored() }; return withDerived(readStored());
} }
export function setGamePath(gamePath: string | null): AppConfig { export function setGamePath(gamePath: string | null): AppConfig {
@@ -28,5 +37,5 @@ export function setGamePath(gamePath: string | null): AppConfig {
} }
const next: StoredConfig = { ...readStored(), gamePath: trimmed }; const next: StoredConfig = { ...readStored(), gamePath: trimmed };
writeFileAtomicSync(path(), JSON.stringify(next, null, 2)); writeFileAtomicSync(path(), JSON.stringify(next, null, 2));
return { version: app.getVersion(), ...next }; return withDerived(next);
} }
+11 -3
View File
@@ -1,7 +1,7 @@
import { homedir } from "node:os"; import { homedir } from "node:os";
import { join } from "node:path"; import { dirname, join } from "node:path";
import { existsSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { getConfig } from "../config/appConfig"; import { gamePathOverride } from "../config/appConfig";
export const VRCHAT_APPID = "438100"; export const VRCHAT_APPID = "438100";
@@ -29,7 +29,7 @@ export function steamLibraries(): string[] {
const bases = platformBases(); const bases = platformBases();
const libs = new Set<string>(); const libs = new Set<string>();
const override = getConfig().gamePath; const override = gamePathOverride();
if (override) { if (override) {
if (existsSync(join(override, "steamapps"))) libs.add(join(override, "steamapps")); if (existsSync(join(override, "steamapps"))) libs.add(join(override, "steamapps"));
if (existsSync(join(override, "compatdata"))) libs.add(override); if (existsSync(join(override, "compatdata"))) libs.add(override);
@@ -60,3 +60,11 @@ export function vrchatPrefix(): string | null {
} }
return null; return null;
} }
export function detectedGamePath(): string | null {
for (const lib of steamLibraries()) {
const manifest = join(lib, `appmanifest_${VRCHAT_APPID}.acf`);
if (existsSync(manifest)) return dirname(lib);
}
return null;
}
+95
View File
@@ -0,0 +1,95 @@
import { homedir } from "node:os";
import { join } from "node:path";
import { existsSync, readdirSync, readFileSync } from "node:fs";
function hubConfigDir(): string {
const home = homedir();
switch (process.platform) {
case "win32":
return join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "UnityHub");
case "darwin":
return join(home, "Library", "Application Support", "UnityHub");
default:
return join(home, ".config", "UnityHub");
}
}
function hubBinaries(): string[] {
const home = homedir();
switch (process.platform) {
case "win32":
return [
join(process.env.ProgramFiles ?? "C:\\Program Files", "Unity Hub", "Unity Hub.exe"),
join(
process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)",
"Unity Hub",
"Unity Hub.exe",
),
];
case "darwin":
return ["/Applications/Unity Hub.app"];
default:
return ["/usr/bin/unityhub", "/opt/unityhub/unityhub", join(home, ".local", "bin", "unityhub")];
}
}
export function hubInstalled(): boolean {
return hubBinaries().some(existsSync);
}
function defaultEditorRoot(): string {
const home = homedir();
switch (process.platform) {
case "win32":
return join(process.env.ProgramFiles ?? "C:\\Program Files", "Unity", "Hub", "Editor");
case "darwin":
return "/Applications/Unity/Hub/Editor";
default:
return join(home, "Unity", "Hub", "Editor");
}
}
function customEditorRoot(): string | null {
const file = join(hubConfigDir(), "secondaryInstallPath.json");
try {
const raw = JSON.parse(readFileSync(file, "utf8")) as unknown;
const path = typeof raw === "string" ? raw : null;
return path && path.trim() ? path.trim() : null;
} catch {
return null;
}
}
export function editorRoot(): string | null {
for (const root of [customEditorRoot(), defaultEditorRoot()]) {
if (root && existsSync(root)) return root;
}
return null;
}
function editorBinaryName(): string {
switch (process.platform) {
case "win32":
return join("Editor", "Unity.exe");
case "darwin":
return join("Unity.app");
default:
return join("Editor", "Unity");
}
}
export function installedVersions(): string[] {
const root = editorRoot();
if (!root) return [];
const binary = editorBinaryName();
const out: string[] = [];
try {
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (existsSync(join(root, entry.name, binary))) out.push(entry.name);
}
} catch {
return [];
}
return out.sort();
}
+8
View File
@@ -6,6 +6,7 @@ import * as users from "../vrchat/userService";
import * as friends from "../vrchat/friendsService"; import * as friends from "../vrchat/friendsService";
import * as worlds from "../vrchat/worldService"; import * as worlds from "../vrchat/worldService";
import * as instances from "../vrchat/instanceService"; import * as instances from "../vrchat/instanceService";
import * as unity from "../vrchat/unityService";
import * as avatars from "../vrchat/avatarService"; import * as avatars from "../vrchat/avatarService";
import * as groups from "../vrchat/groupService"; import * as groups from "../vrchat/groupService";
import * as settings from "../vrchat/settingsService"; import * as settings from "../vrchat/settingsService";
@@ -90,6 +91,13 @@ const handlers = {
return appConfig.setGamePath(res.filePaths[0]); return appConfig.setGamePath(res.filePaths[0]);
}), }),
"unity:status": () => guard(() => unity.unityStatus()),
"unity:install": (url) =>
guard(async () => {
if (!url.startsWith("unityhub://")) throw new Error("Invalid Unity Hub link");
await shell.openExternal(url);
}),
"game:status": () => guard(() => game.status()), "game:status": () => guard(() => game.status()),
"game:launch": () => guard(() => game.launch()), "game:launch": () => guard(() => game.launch()),
+59
View File
@@ -0,0 +1,59 @@
import type { UnityStatus } from "../../shared/types/unity";
import { cachedRead } from "./cachedRead";
import { cacheKeys, policies } from "../cache/policies";
import { getActiveClient } from "./client";
import { userCache } from "./userService";
import { hubInstalled, editorRoot, installedVersions } from "../game/unity";
async function requiredUnityVersion(): Promise<string | null> {
if (!getActiveClient()) return null;
try {
return await cachedRead(cacheKeys.apiConfig(), policies.apiConfig, async (vrc) => {
const { data } = await vrc.getConfig({ throwOnError: true });
return data.sdkUnityVersion ?? null;
});
} catch {
return null;
}
}
async function installUrl(version: string): Promise<string | null> {
try {
return await userCache.get(
cacheKeys.unityChangeset(version),
policies.unityChangeset,
async () => {
const res = await fetch(
`https://services.api.unity.com/unity/editor/release/v1/releases?version=${version}`,
);
if (!res.ok) return null;
const body = (await res.json()) as { results?: { unityHubDeepLink?: string }[] };
return body.results?.[0]?.unityHubDeepLink ?? null;
},
);
} catch {
return null;
}
}
export async function unityStatus(): Promise<UnityStatus> {
const installed = installedVersions();
const required = await requiredUnityVersion();
let match: UnityStatus["match"];
if (!required) match = "unknown";
else if (installed.length === 0) match = "no-editor";
else if (installed.includes(required)) match = "ok";
else match = "missing";
const url = required && match !== "ok" ? await installUrl(required) : null;
return {
hubInstalled: hubInstalled(),
editorRoot: editorRoot(),
installedVersions: installed,
requiredVersion: required,
installUrl: url,
match,
};
}
@@ -1,6 +1,9 @@
import { useEffect, useState, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { import {
AlertTriangle,
Boxes,
Check, Check,
Download,
Droplet, Droplet,
FolderOpen, FolderOpen,
Gamepad2, Gamepad2,
@@ -9,19 +12,23 @@ import {
Monitor, Monitor,
Moon, Moon,
Palette, Palette,
Search,
Sun, Sun,
X,
} from "lucide-react"; } from "lucide-react";
import { useTheme } from "../../lib/ThemeContext"; import { useTheme } from "../../lib/ThemeContext";
import { useI18n } from "../../lib/i18n"; import { useI18n } from "../../lib/i18n";
import { ACCENT_PRESETS, DEFAULT_ACCENT, type SchemeMode } from "../../lib/theme"; import { ACCENT_PRESETS, DEFAULT_ACCENT, type SchemeMode } from "../../lib/theme";
import type { AppConfig } from "../../../../shared/types/appConfig"; import type { AppConfig } from "../../../../shared/types/appConfig";
import type { UnityStatus } from "../../../../shared/types/unity";
import { api, errorMessage } from "../../lib/api"; import { api, errorMessage } from "../../lib/api";
import { useAsync } from "../../lib/useAsync";
import { Button, Field, Tabs } from "../../components/ui"; import { Button, Field, Tabs } from "../../components/ui";
const SHELL = "mx-auto flex w-full max-w-[760px] flex-col gap-[18px] px-12 pb-16 pt-10"; const SHELL = "mx-auto flex w-full max-w-[760px] flex-col gap-[18px] px-12 pb-16 pt-10";
const SECTIONS = "animate-rise flex flex-col gap-[18px]"; const SECTIONS = "animate-rise flex flex-col gap-[18px]";
type SettingsTab = "appearance" | "game" | "about"; type SettingsTab = "appearance" | "game" | "creator" | "about";
export function SettingsView() { export function SettingsView() {
const { t } = useI18n(); const { t } = useI18n();
@@ -37,6 +44,7 @@ export function SettingsView() {
tabs={[ tabs={[
{ id: "appearance", label: t("settings:tabs.appearance") }, { id: "appearance", label: t("settings:tabs.appearance") },
{ id: "game", label: t("settings:tabs.game") }, { id: "game", label: t("settings:tabs.game") },
{ id: "creator", label: t("settings:tabs.creator") },
{ id: "about", label: t("settings:tabs.about") }, { id: "about", label: t("settings:tabs.about") },
]} ]}
active={tab} active={tab}
@@ -57,6 +65,12 @@ export function SettingsView() {
</div> </div>
) : null} ) : null}
{tab === "creator" ? (
<div className={SECTIONS}>
<UnitySection />
</div>
) : null}
{tab === "about" ? ( {tab === "about" ? (
<div className={SECTIONS}> <div className={SECTIONS}>
<AboutSection /> <AboutSection />
@@ -312,6 +326,29 @@ function GameSection() {
{t("settings:game.browse")} {t("settings:game.browse")}
</Button> </Button>
</div> </div>
<div className="mt-3 flex items-center gap-2 text-[12.5px] text-muted">
<Search size={13} className="shrink-0 text-faint" />
<span className="shrink-0 font-semibold">{t("settings:game.detected")}:</span>
{config?.detectedGamePath ? (
<>
<span className="truncate font-mono text-[12px]" title={config.detectedGamePath}>
{config.detectedGamePath}
</span>
{config.detectedGamePath !== path.trim() ? (
<button
type="button"
onClick={() => setPath(config.detectedGamePath ?? "")}
className="ml-auto shrink-0 font-semibold text-accent hover:underline"
>
{t("settings:game.useDetected")}
</button>
) : null}
</>
) : (
<span className="text-faint">{t("settings:game.detectedNone")}</span>
)}
</div>
<div className="mt-3 flex flex-wrap items-center gap-2.5"> <div className="mt-3 flex flex-wrap items-center gap-2.5">
<Button <Button
onClick={() => onClick={() =>
@@ -343,6 +380,73 @@ function Notice({ error, ok }: { error?: string | null; ok?: string | null }) {
return null; return null;
} }
function UnitySection() {
const { t } = useI18n();
const status = useAsync(() => api.unity.status(), [], t("settings:unity.error"));
return (
<Section
title={t("settings:unity.title")}
icon={<Boxes size={16} />}
description={t("settings:unity.description")}
>
{status.status === "loading" ? (
<p className="text-[13px] text-muted">{t("settings:unity.loading")}</p>
) : status.status === "error" ? (
<p className="text-[12.5px] text-[var(--danger)]">{status.message}</p>
) : (
<UnityStatusBody status={status.data} />
)}
</Section>
);
}
function UnityStatusBody({ status }: { status: UnityStatus }) {
const { t } = useI18n();
const verdict = MATCH_META[status.match];
return (
<div className="flex flex-col gap-2 text-[13px]">
<Row
label={t("settings:unity.hub")}
value={status.hubInstalled ? t("settings:unity.installed") : t("settings:unity.notFound")}
/>
<Row label={t("settings:unity.required")} value={status.requiredVersion ?? "—"} />
<Row
label={t("settings:unity.installedVersions")}
value={status.installedVersions.length ? status.installedVersions.join(", ") : "—"}
/>
<div className="flex items-center justify-between py-2">
<span className="text-muted">{t("settings:unity.status")}</span>
<span
className="inline-flex items-center gap-1.5 font-semibold"
style={{ color: verdict.color }}
>
<verdict.icon size={15} />
{t(verdict.key)}
</span>
</div>
{status.match !== "ok" && status.installUrl ? (
<div className="mt-1">
<Button onClick={() => void api.unity.install(status.installUrl as string)}>
<Download size={14} />
{t("settings:unity.install", { version: status.requiredVersion ?? "" })}
</Button>
</div>
) : null}
</div>
);
}
const MATCH_META: Record<
UnityStatus["match"],
{ key: string; icon: typeof Check; color: string }
> = {
ok: { key: "settings:unity.match.ok", icon: Check, color: "var(--status-active)" },
missing: { key: "settings:unity.match.missing", icon: AlertTriangle, color: "var(--status-ask)" },
"no-editor": { key: "settings:unity.match.noEditor", icon: X, color: "var(--danger)" },
unknown: { key: "settings:unity.match.unknown", icon: AlertTriangle, color: "var(--muted)" },
};
function AboutSection() { function AboutSection() {
const { t } = useI18n(); const { t } = useI18n();
const [version, setVersion] = useState(""); const [version, setVersion] = useState("");
+4
View File
@@ -100,6 +100,10 @@ export const api = {
setGamePath: (gamePath: string | null) => call("config:setGamePath", { gamePath }), setGamePath: (gamePath: string | null) => call("config:setGamePath", { gamePath }),
pickGamePath: () => call("config:pickGamePath"), pickGamePath: () => call("config:pickGamePath"),
}, },
unity: {
status: () => call("unity:status"),
install: (url: string) => call("unity:install", url),
},
game: { game: {
status: () => call("game:status"), status: () => call("game:status"),
launch: () => call("game:launch"), launch: () => call("game:launch"),
@@ -4,6 +4,7 @@
"tabs": { "tabs": {
"appearance": "Appearance", "appearance": "Appearance",
"game": "Game", "game": "Game",
"creator": "Creator",
"about": "About" "about": "About"
}, },
"appearance": { "appearance": {
@@ -41,16 +42,38 @@
"description": "The language used across the app." "description": "The language used across the app."
}, },
"game": { "game": {
"title": "Game Install Path", "title": "Steam Path",
"description": "Where Steam or VRChat is installed. Leave empty to auto-detect. Set this if an enhancement can't find your install.", "description": "Your Steam library folder that contains VRChat. Leave empty to auto-detect. Set this if an enhancement can't find your install.",
"label": "Install folder", "label": "Steam folder",
"placeholder": "Auto-detect", "placeholder": "Auto-detect",
"browse": "Browse", "browse": "Browse",
"save": "Save", "save": "Save",
"reset": "Reset to auto-detect", "reset": "Reset to auto-detect",
"saved": "Path saved.", "saved": "Path saved.",
"resetDone": "Reset to auto-detect.", "resetDone": "Reset to auto-detect.",
"error": "Something went wrong." "error": "Something went wrong.",
"detected": "Auto-detected",
"detectedNone": "No Steam library with VRChat found automatically.",
"useDetected": "Use this"
},
"unity": {
"title": "Unity",
"description": "The Unity Editor version VRChat uses, and whether you have it installed via Unity Hub.",
"loading": "Checking your Unity install…",
"error": "Couldn't check your Unity install.",
"hub": "Unity Hub",
"installed": "Installed",
"notFound": "Not found",
"required": "VRChat's Unity version",
"installedVersions": "Installed editors",
"status": "Status",
"install": "Install {{version}} in Unity Hub",
"match": {
"ok": "Match",
"missing": "Version not installed",
"noEditor": "No editor installed",
"unknown": "Sign in to check"
}
}, },
"about": { "about": {
"title": "About", "title": "About",
@@ -4,6 +4,7 @@
"tabs": { "tabs": {
"appearance": "外観", "appearance": "外観",
"game": "ゲーム", "game": "ゲーム",
"creator": "クリエイター",
"about": "情報" "about": "情報"
}, },
"appearance": { "appearance": {
@@ -41,16 +42,38 @@
"description": "アプリ全体で使用される言語。" "description": "アプリ全体で使用される言語。"
}, },
"game": { "game": {
"title": "ゲームのインストール先", "title": "Steam のパス",
"description": "Steam または VRChat のインストール先です。空欄なら自動検出します。拡張機能がインストール先を見つけられないときに設定してください。", "description": "VRChat が入っている Steam ライブラリフォルダです。空欄なら自動検出します。拡張機能がインストール先を見つけられないときに設定してください。",
"label": "インストールフォルダ", "label": "Steam フォルダ",
"placeholder": "自動検出", "placeholder": "自動検出",
"browse": "参照", "browse": "参照",
"save": "保存", "save": "保存",
"reset": "自動検出に戻す", "reset": "自動検出に戻す",
"saved": "パスを保存しました。", "saved": "パスを保存しました。",
"resetDone": "自動検出に戻しました。", "resetDone": "自動検出に戻しました。",
"error": "問題が発生しました。" "error": "問題が発生しました。",
"detected": "自動検出",
"detectedNone": "VRChat を含む Steam ライブラリを自動で見つけられませんでした。",
"useDetected": "これを使う"
},
"unity": {
"title": "Unity",
"description": "VRChat が使用している Unity エディターのバージョンと、Unity Hub でインストール済みかどうかを表示します。",
"loading": "Unity のインストールを確認中…",
"error": "Unity のインストールを確認できませんでした。",
"hub": "Unity Hub",
"installed": "インストール済み",
"notFound": "見つかりません",
"required": "VRChat の Unity バージョン",
"installedVersions": "インストール済みエディター",
"status": "状態",
"install": "Unity Hub で {{version}} をインストール",
"match": {
"ok": "一致",
"missing": "そのバージョンが未インストール",
"noEditor": "エディター未インストール",
"unknown": "確認するにはサインイン"
}
}, },
"about": { "about": {
"title": "情報", "title": "情報",
@@ -4,6 +4,7 @@
"tabs": { "tabs": {
"appearance": "รูปลักษณ์", "appearance": "รูปลักษณ์",
"game": "เกม", "game": "เกม",
"creator": "ครีเอเตอร์",
"about": "เกี่ยวกับ" "about": "เกี่ยวกับ"
}, },
"appearance": { "appearance": {
@@ -41,16 +42,38 @@
"description": "ภาษาที่ใช้ทั่วทั้งแอป" "description": "ภาษาที่ใช้ทั่วทั้งแอป"
}, },
"game": { "game": {
"title": "ตำแหน่งติดตั้งเกม", "title": "ตำแหน่ง Steam",
"description": "ตำแหน่งที่ติดตั้ง Steam หรือ VRChat เว้นว่างไว้เพื่อตรวจหาอัตโนมัติ ตั้งค่านี้หากส่วนเสริมหาตำแหน่งติดตั้งไม่พบ", "description": "โฟลเดอร์ไลบรารี Steam ที่มี VRChat อยู่ เว้นว่างไว้เพื่อตรวจหาอัตโนมัติ ตั้งค่านี้หากส่วนเสริมหาตำแหน่งติดตั้งไม่พบ",
"label": "โฟลเดอร์ติดตั้ง", "label": "โฟลเดอร์ Steam",
"placeholder": "ตรวจหาอัตโนมัติ", "placeholder": "ตรวจหาอัตโนมัติ",
"browse": "เรียกดู", "browse": "เรียกดู",
"save": "บันทึก", "save": "บันทึก",
"reset": "รีเซ็ตเป็นตรวจหาอัตโนมัติ", "reset": "รีเซ็ตเป็นตรวจหาอัตโนมัติ",
"saved": "บันทึกตำแหน่งแล้ว", "saved": "บันทึกตำแหน่งแล้ว",
"resetDone": "รีเซ็ตเป็นตรวจหาอัตโนมัติแล้ว", "resetDone": "รีเซ็ตเป็นตรวจหาอัตโนมัติแล้ว",
"error": "เกิดข้อผิดพลาด" "error": "เกิดข้อผิดพลาด",
"detected": "ตรวจพบอัตโนมัติ",
"detectedNone": "ไม่พบไลบรารี Steam ที่มี VRChat โดยอัตโนมัติ",
"useDetected": "ใช้ตำแหน่งนี้"
},
"unity": {
"title": "Unity",
"description": "เวอร์ชัน Unity Editor ที่ VRChat ใช้ และคุณติดตั้งไว้ผ่าน Unity Hub หรือไม่",
"loading": "กำลังตรวจสอบการติดตั้ง Unity…",
"error": "ตรวจสอบการติดตั้ง Unity ไม่ได้",
"hub": "Unity Hub",
"installed": "ติดตั้งแล้ว",
"notFound": "ไม่พบ",
"required": "เวอร์ชัน Unity ของ VRChat",
"installedVersions": "เอดิเตอร์ที่ติดตั้ง",
"status": "สถานะ",
"install": "ติดตั้ง {{version}} ใน Unity Hub",
"match": {
"ok": "ตรงกัน",
"missing": "ยังไม่ได้ติดตั้งเวอร์ชันนี้",
"noEditor": "ยังไม่ได้ติดตั้งเอดิเตอร์",
"unknown": "ลงชื่อเข้าใช้เพื่อตรวจสอบ"
}
}, },
"about": { "about": {
"title": "เกี่ยวกับ", "title": "เกี่ยวกับ",
+4
View File
@@ -8,6 +8,7 @@ import type {
import type { SocialSnapshot, UserProfile, UserStatus } from "./types/user"; import type { SocialSnapshot, UserProfile, UserStatus } from "./types/user";
import type { FavoriteWorldFolder, World, WorldSnapshot } from "./types/world"; import type { FavoriteWorldFolder, World, WorldSnapshot } from "./types/world";
import type { Instance } from "./types/instance"; import type { Instance } from "./types/instance";
import type { UnityStatus } from "./types/unity";
import type { Avatar } from "./types/avatar"; import type { Avatar } from "./types/avatar";
import type { RepoStats, StoredEntity } from "./types/repository"; import type { RepoStats, StoredEntity } from "./types/repository";
import type { AccountSettings, ContentFilterKey, Pending2Fa, RecoveryCode } from "./types/settings"; import type { AccountSettings, ContentFilterKey, Pending2Fa, RecoveryCode } from "./types/settings";
@@ -91,6 +92,9 @@ export interface IpcRequests {
"config:setGamePath": (p: { gamePath: string | null }) => IpcResult<AppConfig>; "config:setGamePath": (p: { gamePath: string | null }) => IpcResult<AppConfig>;
"config:pickGamePath": () => IpcResult<AppConfig>; "config:pickGamePath": () => IpcResult<AppConfig>;
"unity:status": () => IpcResult<UnityStatus>;
"unity:install": (url: string) => IpcResult<void>;
"game:status": () => IpcResult<GameStatus>; "game:status": () => IpcResult<GameStatus>;
"game:launch": () => IpcResult<GameStatus>; "game:launch": () => IpcResult<GameStatus>;
+1
View File
@@ -1,4 +1,5 @@
export interface AppConfig { export interface AppConfig {
version: string; version: string;
gamePath: string | null; gamePath: string | null;
detectedGamePath: string | null;
} }
+8
View File
@@ -0,0 +1,8 @@
export interface UnityStatus {
hubInstalled: boolean;
editorRoot: string | null;
installedVersions: string[];
requiredVersion: string | null;
installUrl: string | null;
match: "ok" | "missing" | "no-editor" | "unknown";
}